Add `gen_ai_latest_experimental` to the Sem Conv stability flag. Add `ContentCapturingMode` enum (#3716)
* initial commit * lint * Fix tests * Fix typecheck * Update opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> * Update util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> * Use ValueError * Rename enum * Update util/opentelemetry-util-genai/tests/test_utils.py Co-authored-by: Aaron Abbott <aaronabbott@google.com> * Update util/opentelemetry-util-genai/tests/test_utils.py Co-authored-by: Aaron Abbott <aaronabbott@google.com> * Update util/opentelemetry-util-genai/tests/test_utils.py Co-authored-by: Aaron Abbott <aaronabbott@google.com> * Update util/opentelemetry-util-genai/tests/test_utils.py Co-authored-by: Aaron Abbott <aaronabbott@google.com> * Default env var to NO_CONTENT when invalid envvar * Address comments * Address comment * Fix typecheck * don't change typecheck, * Fix linter * Address comments * Fix order of args.. --------- Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> Co-authored-by: Aaron Abbott <aaronabbott@google.com>
This commit is contained in:
		
							parent
							
								
									b1b9505043
								
							
						
					
					
						commit
						2ecc2d2404
					
				|  | @ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| 
 | ||||
| ### Added | ||||
| 
 | ||||
| - `opentelemetry-util-genai` Add a utility to parse the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. | ||||
|   Add `gen_ai_latest_experimental` as a new value to the Sem Conv stability flag ([#3716](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3716)). | ||||
| - `opentelemetry-instrumentation-confluent-kafka` Add support for confluent-kafka <=2.11.0 | ||||
|   ([#3685](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3685)) | ||||
| - `opentelemetry-instrumentation-system-metrics`: Add `cpython.gc.collected_objects` and `cpython.gc.uncollectable_objects` metrics | ||||
|  |  | |||
|  | @ -162,9 +162,10 @@ _server_active_requests_count_attrs_new = [ | |||
| OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN" | ||||
| 
 | ||||
| 
 | ||||
| class _OpenTelemetryStabilitySignalType: | ||||
| class _OpenTelemetryStabilitySignalType(Enum): | ||||
|     HTTP = "http" | ||||
|     DATABASE = "database" | ||||
|     GEN_AI = "gen_ai" | ||||
| 
 | ||||
| 
 | ||||
| class _StabilityMode(Enum): | ||||
|  | @ -173,6 +174,7 @@ class _StabilityMode(Enum): | |||
|     HTTP_DUP = "http/dup" | ||||
|     DATABASE = "database" | ||||
|     DATABASE_DUP = "database/dup" | ||||
|     GEN_AI_LATEST_EXPERIMENTAL = "gen_ai_latest_experimental" | ||||
| 
 | ||||
| 
 | ||||
| def _report_new(mode: _StabilityMode): | ||||
|  | @ -195,7 +197,7 @@ class _OpenTelemetrySemanticConventionStability: | |||
|                 return | ||||
| 
 | ||||
|             # Users can pass in comma delimited string for opt-in options | ||||
|             # Only values for http and database stability are supported for now | ||||
|             # Only values for http, gen ai, and database stability are supported for now | ||||
|             opt_in = os.environ.get(OTEL_SEMCONV_STABILITY_OPT_IN) | ||||
| 
 | ||||
|             if not opt_in: | ||||
|  | @ -203,6 +205,7 @@ class _OpenTelemetrySemanticConventionStability: | |||
|                 cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = { | ||||
|                     _OpenTelemetryStabilitySignalType.HTTP: _StabilityMode.DEFAULT, | ||||
|                     _OpenTelemetryStabilitySignalType.DATABASE: _StabilityMode.DEFAULT, | ||||
|                     _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.DEFAULT, | ||||
|                 } | ||||
|                 cls._initialized = True | ||||
|                 return | ||||
|  | @ -215,6 +218,14 @@ class _OpenTelemetrySemanticConventionStability: | |||
|                 opt_in_list, _StabilityMode.HTTP, _StabilityMode.HTTP_DUP | ||||
|             ) | ||||
| 
 | ||||
|             cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ | ||||
|                 _OpenTelemetryStabilitySignalType.GEN_AI | ||||
|             ] = cls._filter_mode( | ||||
|                 opt_in_list, | ||||
|                 _StabilityMode.DEFAULT, | ||||
|                 _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, | ||||
|             ) | ||||
| 
 | ||||
|             cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ | ||||
|                 _OpenTelemetryStabilitySignalType.DATABASE | ||||
|             ] = cls._filter_mode( | ||||
|  | @ -222,7 +233,6 @@ class _OpenTelemetrySemanticConventionStability: | |||
|                 _StabilityMode.DATABASE, | ||||
|                 _StabilityMode.DATABASE_DUP, | ||||
|             ) | ||||
| 
 | ||||
|             cls._initialized = True | ||||
| 
 | ||||
|     @staticmethod | ||||
|  |  | |||
|  | @ -54,6 +54,12 @@ class TestOpenTelemetrySemConvStability(TestCase): | |||
|             ), | ||||
|             _StabilityMode.DEFAULT, | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( | ||||
|                 _OpenTelemetryStabilitySignalType.GEN_AI | ||||
|             ), | ||||
|             _StabilityMode.DEFAULT, | ||||
|         ) | ||||
| 
 | ||||
|     @stability_mode("http") | ||||
|     def test_http_stable_mode(self): | ||||
|  | @ -91,7 +97,16 @@ class TestOpenTelemetrySemConvStability(TestCase): | |||
|             _StabilityMode.DATABASE_DUP, | ||||
|         ) | ||||
| 
 | ||||
|     @stability_mode("database,http") | ||||
|     @stability_mode("gen_ai_latest_experimental") | ||||
|     def test_genai_latest_experimental(self): | ||||
|         self.assertEqual( | ||||
|             _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( | ||||
|                 _OpenTelemetryStabilitySignalType.GEN_AI | ||||
|             ), | ||||
|             _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, | ||||
|         ) | ||||
| 
 | ||||
|     @stability_mode("database,http,gen_ai_latest_experimental") | ||||
|     def test_multiple_stability_database_http_modes(self): | ||||
|         self.assertEqual( | ||||
|             _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( | ||||
|  | @ -105,6 +120,12 @@ class TestOpenTelemetrySemConvStability(TestCase): | |||
|             ), | ||||
|             _StabilityMode.HTTP, | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( | ||||
|                 _OpenTelemetryStabilitySignalType.GEN_AI | ||||
|             ), | ||||
|             _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, | ||||
|         ) | ||||
| 
 | ||||
|     @stability_mode("database,http/dup") | ||||
|     def test_multiple_stability_database_http_dup_modes(self): | ||||
|  |  | |||
|  | @ -6,3 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | |||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||
| 
 | ||||
| ## Unreleased | ||||
| 
 | ||||
| Repurpose the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable when GEN AI stability mode is set to `gen_ai_latest_experimental`, | ||||
| to take on an enum (`NO_CONTENT/SPAN_ONLY/EVENT_ONLY/SPAN_AND_EVENT`) instead of a boolean. Add a utility function to help parse this environment variable. | ||||
|  | @ -0,0 +1,17 @@ | |||
| # Copyright The OpenTelemetry Authors | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| #     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
| 
 | ||||
| OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = ( | ||||
|     "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" | ||||
| ) | ||||
|  | @ -14,22 +14,34 @@ | |||
| 
 | ||||
| 
 | ||||
| from dataclasses import dataclass | ||||
| from enum import Enum | ||||
| from typing import Any, Literal, Optional, Union | ||||
| 
 | ||||
| 
 | ||||
| class ContentCapturingMode(Enum): | ||||
|     # Do not capture content (default). | ||||
|     NO_CONTENT = 0 | ||||
|     # Only capture content in spans. | ||||
|     SPAN_ONLY = 1 | ||||
|     # Only capture content in events. | ||||
|     EVENT_ONLY = 2 | ||||
|     # Capture content in both spans and events. | ||||
|     SPAN_AND_EVENT = 3 | ||||
| 
 | ||||
| 
 | ||||
| @dataclass() | ||||
| class ToolCall: | ||||
|     type: Literal["tool_call"] = "tool_call" | ||||
|     arguments: Any | ||||
|     name: str | ||||
|     id: Optional[str] | ||||
|     type: Literal["tool_call"] = "tool_call" | ||||
| 
 | ||||
| 
 | ||||
| @dataclass() | ||||
| class ToolCallResponse: | ||||
|     type: Literal["tool_call_response"] = "tool_call_response" | ||||
|     response: Any | ||||
|     id: Optional[str] | ||||
|     type: Literal["tool_call_response"] = "tool_call_response" | ||||
| 
 | ||||
| 
 | ||||
| FinishReason = Literal[ | ||||
|  | @ -39,8 +51,8 @@ FinishReason = Literal[ | |||
| 
 | ||||
| @dataclass() | ||||
| class Text: | ||||
|     type: Literal["text"] = "text" | ||||
|     content: str | ||||
|     type: Literal["text"] = "text" | ||||
| 
 | ||||
| 
 | ||||
| MessagePart = Union[Text, ToolCall, ToolCallResponse, Any] | ||||
|  |  | |||
|  | @ -0,0 +1,56 @@ | |||
| # Copyright The OpenTelemetry Authors | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| #     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
| 
 | ||||
| import logging | ||||
| import os | ||||
| 
 | ||||
| from opentelemetry.instrumentation._semconv import ( | ||||
|     _OpenTelemetrySemanticConventionStability, | ||||
|     _OpenTelemetryStabilitySignalType, | ||||
|     _StabilityMode, | ||||
| ) | ||||
| from opentelemetry.util.genai.environment_variables import ( | ||||
|     OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, | ||||
| ) | ||||
| from opentelemetry.util.genai.types import ContentCapturingMode | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def get_content_capturing_mode() -> ContentCapturingMode: | ||||
|     """This function should not be called when GEN_AI stability mode is set to DEFAULT. | ||||
| 
 | ||||
|     When the GEN_AI stability mode is DEFAULT this function will raise a ValueError -- see the code below.""" | ||||
|     envvar = os.environ.get(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT) | ||||
|     if ( | ||||
|         _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( | ||||
|             _OpenTelemetryStabilitySignalType.GEN_AI, | ||||
|         ) | ||||
|         == _StabilityMode.DEFAULT | ||||
|     ): | ||||
|         raise ValueError( | ||||
|             "This function should never be called when StabilityMode is default." | ||||
|         ) | ||||
|     if not envvar: | ||||
|         return ContentCapturingMode.NO_CONTENT | ||||
|     try: | ||||
|         return ContentCapturingMode[envvar.upper()] | ||||
|     except KeyError: | ||||
|         logger.warning( | ||||
|             "%s is not a valid option for `%s` environment variable. Must be one of %s. Defaulting to `NO_CONTENT`.", | ||||
|             envvar, | ||||
|             OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, | ||||
|             ", ".join(e.name for e in ContentCapturingMode), | ||||
|         ) | ||||
|         return ContentCapturingMode.NO_CONTENT | ||||
|  | @ -0,0 +1,83 @@ | |||
| # Copyright The OpenTelemetry Authors | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| #     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
| 
 | ||||
| import os | ||||
| import unittest | ||||
| from unittest.mock import patch | ||||
| 
 | ||||
| from opentelemetry.instrumentation._semconv import ( | ||||
|     OTEL_SEMCONV_STABILITY_OPT_IN, | ||||
|     _OpenTelemetrySemanticConventionStability, | ||||
| ) | ||||
| from opentelemetry.util.genai.environment_variables import ( | ||||
|     OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, | ||||
| ) | ||||
| from opentelemetry.util.genai.types import ContentCapturingMode | ||||
| from opentelemetry.util.genai.utils import get_content_capturing_mode | ||||
| 
 | ||||
| 
 | ||||
| def patch_env_vars(stability_mode, content_capturing): | ||||
|     def decorator(test_case): | ||||
|         @patch.dict( | ||||
|             os.environ, | ||||
|             { | ||||
|                 OTEL_SEMCONV_STABILITY_OPT_IN: stability_mode, | ||||
|                 OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: content_capturing, | ||||
|             }, | ||||
|         ) | ||||
|         def wrapper(*args, **kwargs): | ||||
|             # Reset state. | ||||
|             _OpenTelemetrySemanticConventionStability._initialized = False | ||||
|             _OpenTelemetrySemanticConventionStability._initialize() | ||||
|             return test_case(*args, **kwargs) | ||||
| 
 | ||||
|         return wrapper | ||||
| 
 | ||||
|     return decorator | ||||
| 
 | ||||
| 
 | ||||
| class TestVersion(unittest.TestCase): | ||||
|     @patch_env_vars( | ||||
|         stability_mode="gen_ai_latest_experimental", | ||||
|         content_capturing="SPAN_ONLY", | ||||
|     ) | ||||
|     def test_get_content_capturing_mode_parses_valid_envvar(self):  # pylint: disable=no-self-use | ||||
|         assert get_content_capturing_mode() == ContentCapturingMode.SPAN_ONLY | ||||
| 
 | ||||
|     @patch_env_vars( | ||||
|         stability_mode="gen_ai_latest_experimental", content_capturing="" | ||||
|     ) | ||||
|     def test_empty_content_capturing_envvar(self):  # pylint: disable=no-self-use | ||||
|         assert get_content_capturing_mode() == ContentCapturingMode.NO_CONTENT | ||||
| 
 | ||||
|     @patch_env_vars(stability_mode="default", content_capturing="True") | ||||
|     def test_get_content_capturing_mode_raises_exception_when_semconv_stability_default( | ||||
|         self, | ||||
|     ):  # pylint: disable=no-self-use | ||||
|         with self.assertRaises(ValueError): | ||||
|             get_content_capturing_mode() | ||||
| 
 | ||||
|     @patch_env_vars( | ||||
|         stability_mode="gen_ai_latest_experimental", | ||||
|         content_capturing="INVALID_VALUE", | ||||
|     ) | ||||
|     def test_get_content_capturing_mode_raises_exception_on_invalid_envvar( | ||||
|         self, | ||||
|     ):  # pylint: disable=no-self-use | ||||
|         with self.assertLogs(level="WARNING") as cm: | ||||
|             assert ( | ||||
|                 get_content_capturing_mode() == ContentCapturingMode.NO_CONTENT | ||||
|             ) | ||||
|         self.assertEqual(len(cm.output), 1) | ||||
|         self.assertIn("INVALID_VALUE is not a valid option for ", cm.output[0]) | ||||
		Loading…
	
		Reference in New Issue