From ec423392f3a15e55bacb263b3d1ca469f9547513 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 28 Aug 2025 13:56:22 +0000 Subject: [PATCH 01/20] initial commit --- .../opentelemetry/instrumentation/_semconv.py | 13 +++- .../tests/test_semconv.py | 15 ++++ .../util/genai/environment_variables.py | 17 +++++ .../src/opentelemetry/util/genai/types.py | 26 +++++++ .../src/opentelemetry/util/genai/utils.py | 48 ++++++++++++ .../tests/test_utils.py | 74 +++++++++++++++++++ 6 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 util/opentelemetry-util-genai/src/opentelemetry/util/genai/environment_variables.py create mode 100644 util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py create mode 100644 util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py create mode 100644 util/opentelemetry-util-genai/tests/test_utils.py diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index c443fcbfdd..4488aef649 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -162,9 +162,10 @@ 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 = "gen_ai_latest_experimental" def _report_new(mode: _StabilityMode): @@ -195,7 +197,7 @@ def _initialize(cls): 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 @@ def _initialize(cls): cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = { _OpenTelemetryStabilitySignalType.HTTP: _StabilityMode.DEFAULT, _OpenTelemetryStabilitySignalType.DATABASE: _StabilityMode.DEFAULT, + _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.DEFAULT, } cls._initialized = True return @@ -210,9 +213,11 @@ def _initialize(cls): opt_in_list = [s.strip() for s in opt_in.split(",")] cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ - _OpenTelemetryStabilitySignalType.HTTP + _OpenTelemetryStabilitySignalType.GEN_AI ] = cls._filter_mode( - opt_in_list, _StabilityMode.HTTP, _StabilityMode.HTTP_DUP + opt_in_list, + _StabilityMode.DEFAULT, + _StabilityMode.GEN_AI_LATEST, ) cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ diff --git a/opentelemetry-instrumentation/tests/test_semconv.py b/opentelemetry-instrumentation/tests/test_semconv.py index 6a56efcc37..6cef036701 100644 --- a/opentelemetry-instrumentation/tests/test_semconv.py +++ b/opentelemetry-instrumentation/tests/test_semconv.py @@ -54,6 +54,12 @@ def test_default_mode(self): ), _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,6 +97,15 @@ def test_database_dup_mode(self): _StabilityMode.DATABASE_DUP, ) + @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, + ) + @stability_mode("database,http") def test_multiple_stability_database_http_modes(self): self.assertEqual( diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/environment_variables.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/environment_variables.py new file mode 100644 index 0000000000..2f939772c6 --- /dev/null +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/environment_variables.py @@ -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" +) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py new file mode 100644 index 0000000000..97b4be42ab --- /dev/null +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -0,0 +1,26 @@ +# 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. + +from enum import Enum + + +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 diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py new file mode 100644 index 0000000000..05b987d885 --- /dev/null +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py @@ -0,0 +1,48 @@ +# 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 + +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 + + +def get_content_capturing_mode() -> ContentCapturingMode: + envvar = os.environ.get(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT) + if ( + _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.GEN_AI, + ) + == _StabilityMode.DEFAULT + ): + raise RuntimeError( + "This function should never be called when StabilityMode is default.." + ) + if not envvar: + return ContentCapturingMode.NO_CONTENT + try: + return ContentCapturingMode[envvar.upper()] + except KeyError: + raise RuntimeError( + "{} is not a valid option for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. Must be one of {}".format( + envvar, ", ".join([e.name for e in ContentCapturingMode]) + ) + ) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py new file mode 100644 index 0000000000..0e1c91189a --- /dev/null +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -0,0 +1,74 @@ +# 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("gen_ai_latest_experimental", "SPAN_ONLY") + def test_get_content_caputring_mode_parses_valid_envvar(self): # pylint: disable=no-self-use + _OpenTelemetrySemanticConventionStability._initialized = False + assert get_content_capturing_mode() == ContentCapturingMode.SPAN_ONLY + + @patch_env_vars("gen_ai_latest_experimental", "") + def test_empty_content_capturing_envvar(self): # pylint: disable=no-self-use + _OpenTelemetrySemanticConventionStability._initialized = False + assert get_content_capturing_mode() == ContentCapturingMode.NO_CONTENT + + @patch_env_vars("default", "True") + def test_get_content_caputring_mode_raises_exception_when_semconv_stability_default( + self, + ): # pylint: disable=no-self-use + _OpenTelemetrySemanticConventionStability._initialized = False + with self.assertRaises(RuntimeError): + get_content_capturing_mode() + + @patch_env_vars("gen_ai_latest_experimental", "INVALID_VALUE") + def test_get_content_caputring_mode_raises_exception_on_invalid_envvar( + self, + ): # pylint: disable=no-self-use + with self.assertRaises(RuntimeError): + get_content_capturing_mode() From 353243a09ba1d2db393c79e94a32853bad5d5a99 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 28 Aug 2025 13:56:34 +0000 Subject: [PATCH 02/20] lint --- .../src/opentelemetry/util/genai/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py index 05b987d885..0bffb6b68e 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py @@ -42,7 +42,5 @@ def get_content_capturing_mode() -> ContentCapturingMode: return ContentCapturingMode[envvar.upper()] except KeyError: raise RuntimeError( - "{} is not a valid option for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. Must be one of {}".format( - envvar, ", ".join([e.name for e in ContentCapturingMode]) - ) + f"{envvar} is not a valid option for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. Must be one of {', '.join(e.name for e in ContentCapturingMode)}" ) From 01cabcd486abbafb645d0d6dc1bd7d7086f1a723 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 28 Aug 2025 14:21:18 +0000 Subject: [PATCH 03/20] Fix tests --- .../src/opentelemetry/instrumentation/_semconv.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index 4488aef649..fbff49b26a 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -162,7 +162,7 @@ OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN" -class _OpenTelemetryStabilitySignalType(Enum): +class _OpenTelemetryStabilitySignalType: HTTP = "http" DATABASE = "database" GEN_AI = "gen_ai" @@ -212,6 +212,12 @@ def _initialize(cls): opt_in_list = [s.strip() for s in opt_in.split(",")] + cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ + _OpenTelemetryStabilitySignalType.HTTP + ] = cls._filter_mode( + opt_in_list, _StabilityMode.HTTP, _StabilityMode.HTTP_DUP + ) + cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ _OpenTelemetryStabilitySignalType.GEN_AI ] = cls._filter_mode( @@ -227,7 +233,7 @@ def _initialize(cls): _StabilityMode.DATABASE, _StabilityMode.DATABASE_DUP, ) - + print(cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING) cls._initialized = True @staticmethod From 45cdaff820dd3ad9fc1ed1f40047bef7915594c5 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 28 Aug 2025 14:31:58 +0000 Subject: [PATCH 04/20] Fix typecheck --- .../src/opentelemetry/instrumentation/_semconv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index fbff49b26a..317b4ea727 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -162,7 +162,7 @@ OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN" -class _OpenTelemetryStabilitySignalType: +class _OpenTelemetryStabilitySignalType(Enum): HTTP = "http" DATABASE = "database" GEN_AI = "gen_ai" From b2c5b19046a6957112a9e4df29f3cc27e88dc25a Mon Sep 17 00:00:00 2001 From: DylanRussell Date: Fri, 29 Aug 2025 13:33:37 +0000 Subject: [PATCH 05/20] Update opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> --- .../src/opentelemetry/instrumentation/_semconv.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index 317b4ea727..100eb82653 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -233,7 +233,6 @@ def _initialize(cls): _StabilityMode.DATABASE, _StabilityMode.DATABASE_DUP, ) - print(cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING) cls._initialized = True @staticmethod From 96f205243b1e09eb55c4dd8a21afc8d4df23fab7 Mon Sep 17 00:00:00 2001 From: DylanRussell Date: Fri, 29 Aug 2025 14:46:26 +0000 Subject: [PATCH 06/20] Update util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py Co-authored-by: Riccardo Magliocchetti --- .../src/opentelemetry/util/genai/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py index 0bffb6b68e..89b35c0335 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py @@ -34,7 +34,7 @@ def get_content_capturing_mode() -> ContentCapturingMode: == _StabilityMode.DEFAULT ): raise RuntimeError( - "This function should never be called when StabilityMode is default.." + "This function should never be called when StabilityMode is default." ) if not envvar: return ContentCapturingMode.NO_CONTENT From 748f1ca0fa61eb1bb2da2a065be91dcc076b0266 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 29 Aug 2025 14:50:35 +0000 Subject: [PATCH 07/20] Use ValueError --- .../src/opentelemetry/util/genai/utils.py | 2 +- util/opentelemetry-util-genai/tests/test_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py index 89b35c0335..68ff015e16 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py @@ -33,7 +33,7 @@ def get_content_capturing_mode() -> ContentCapturingMode: ) == _StabilityMode.DEFAULT ): - raise RuntimeError( + raise ValueError( "This function should never be called when StabilityMode is default." ) if not envvar: diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index 0e1c91189a..dac128ecd9 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -63,7 +63,7 @@ def test_get_content_caputring_mode_raises_exception_when_semconv_stability_defa self, ): # pylint: disable=no-self-use _OpenTelemetrySemanticConventionStability._initialized = False - with self.assertRaises(RuntimeError): + with self.assertRaises(ValueError): get_content_capturing_mode() @patch_env_vars("gen_ai_latest_experimental", "INVALID_VALUE") From 185d7824528612292c97ad52608c853314388a19 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Tue, 2 Sep 2025 14:53:30 +0000 Subject: [PATCH 08/20] Rename enum --- .../src/opentelemetry/instrumentation/_semconv.py | 4 ++-- opentelemetry-instrumentation/tests/test_semconv.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index 100eb82653..1b1748e206 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -174,7 +174,7 @@ class _StabilityMode(Enum): HTTP_DUP = "http/dup" DATABASE = "database" DATABASE_DUP = "database/dup" - GEN_AI_LATEST = "gen_ai_latest_experimental" + GEN_AI_LATEST_EXPERIMENTAL = "gen_ai_latest_experimental" def _report_new(mode: _StabilityMode): @@ -223,7 +223,7 @@ def _initialize(cls): ] = cls._filter_mode( opt_in_list, _StabilityMode.DEFAULT, - _StabilityMode.GEN_AI_LATEST, + _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, ) cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ diff --git a/opentelemetry-instrumentation/tests/test_semconv.py b/opentelemetry-instrumentation/tests/test_semconv.py index 6cef036701..4c88a782bb 100644 --- a/opentelemetry-instrumentation/tests/test_semconv.py +++ b/opentelemetry-instrumentation/tests/test_semconv.py @@ -103,7 +103,7 @@ def test_genai_latest_experimental(self): _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.GEN_AI ), - _StabilityMode.GEN_AI_LATEST, + _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, ) @stability_mode("database,http") From 06bbeb963d197687a374624548e801d7cc91e5e1 Mon Sep 17 00:00:00 2001 From: DylanRussell Date: Wed, 3 Sep 2025 11:10:11 -0400 Subject: [PATCH 09/20] Update util/opentelemetry-util-genai/tests/test_utils.py Co-authored-by: Aaron Abbott --- util/opentelemetry-util-genai/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index dac128ecd9..ac30676ba3 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -27,7 +27,7 @@ from opentelemetry.util.genai.utils import get_content_capturing_mode -def patch_env_vars(stability_mode, content_capturing): +def patch_env_vars(*, stability_mode, content_capturing): def decorator(test_case): @patch.dict( os.environ, From 80eeea825e2c307fe2c2ce1114e542ae5302038c Mon Sep 17 00:00:00 2001 From: DylanRussell Date: Wed, 3 Sep 2025 11:10:20 -0400 Subject: [PATCH 10/20] Update util/opentelemetry-util-genai/tests/test_utils.py Co-authored-by: Aaron Abbott --- util/opentelemetry-util-genai/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index ac30676ba3..5ae57e0d5a 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -49,7 +49,7 @@ def wrapper(*args, **kwargs): class TestVersion(unittest.TestCase): @patch_env_vars("gen_ai_latest_experimental", "SPAN_ONLY") - def test_get_content_caputring_mode_parses_valid_envvar(self): # pylint: disable=no-self-use + def test_get_content_capturing_mode_parses_valid_envvar(self): # pylint: disable=no-self-use _OpenTelemetrySemanticConventionStability._initialized = False assert get_content_capturing_mode() == ContentCapturingMode.SPAN_ONLY From 338bfd9653ce4a77b30062d09bd3a0d31e77e28c Mon Sep 17 00:00:00 2001 From: DylanRussell Date: Wed, 3 Sep 2025 11:10:27 -0400 Subject: [PATCH 11/20] Update util/opentelemetry-util-genai/tests/test_utils.py Co-authored-by: Aaron Abbott --- util/opentelemetry-util-genai/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index 5ae57e0d5a..9777ec0569 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -59,7 +59,7 @@ def test_empty_content_capturing_envvar(self): # pylint: disable=no-self-use assert get_content_capturing_mode() == ContentCapturingMode.NO_CONTENT @patch_env_vars("default", "True") - def test_get_content_caputring_mode_raises_exception_when_semconv_stability_default( + def test_get_content_capturing_mode_raises_exception_when_semconv_stability_default( self, ): # pylint: disable=no-self-use _OpenTelemetrySemanticConventionStability._initialized = False From 695740ae9dfa98a57e8d7e791613d467a16b21c1 Mon Sep 17 00:00:00 2001 From: DylanRussell Date: Wed, 3 Sep 2025 11:10:36 -0400 Subject: [PATCH 12/20] Update util/opentelemetry-util-genai/tests/test_utils.py Co-authored-by: Aaron Abbott --- util/opentelemetry-util-genai/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index 9777ec0569..8caf900ced 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -67,7 +67,7 @@ def test_get_content_capturing_mode_raises_exception_when_semconv_stability_defa get_content_capturing_mode() @patch_env_vars("gen_ai_latest_experimental", "INVALID_VALUE") - def test_get_content_caputring_mode_raises_exception_on_invalid_envvar( + def test_get_content_capturing_mode_raises_exception_on_invalid_envvar( self, ): # pylint: disable=no-self-use with self.assertRaises(RuntimeError): From a75d51b5bdd7eee8ae9efc064f7ed69ca418906e Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Wed, 3 Sep 2025 15:13:00 +0000 Subject: [PATCH 13/20] Default env var to NO_CONTENT when invalid envvar --- .../src/opentelemetry/util/genai/utils.py | 8 ++++++-- util/opentelemetry-util-genai/tests/test_utils.py | 14 +++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py index 68ff015e16..75f5ea414b 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import os from opentelemetry.instrumentation._semconv import ( @@ -24,6 +25,8 @@ ) from opentelemetry.util.genai.types import ContentCapturingMode +logger = logging.getLogger(__name__) + def get_content_capturing_mode() -> ContentCapturingMode: envvar = os.environ.get(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT) @@ -41,6 +44,7 @@ def get_content_capturing_mode() -> ContentCapturingMode: try: return ContentCapturingMode[envvar.upper()] except KeyError: - raise RuntimeError( - f"{envvar} is not a valid option for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. Must be one of {', '.join(e.name for e in ContentCapturingMode)}" + logger.warning( + f"{envvar} is not a valid option for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. Must be one of {', '.join(e.name for e in ContentCapturingMode)}. Defaulting to `NO_COTENT`" ) + return ContentCapturingMode.NO_CONTENT diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index dac128ecd9..a15fd6af5e 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -24,10 +24,10 @@ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ) from opentelemetry.util.genai.types import ContentCapturingMode -from opentelemetry.util.genai.utils import get_content_capturing_mode +from opentelemetry.util.genai.utils import get_content_capturing_mode, logger -def patch_env_vars(stability_mode, content_capturing): +def patch_env_vars(*, stability_mode, content_capturing): def decorator(test_case): @patch.dict( os.environ, @@ -49,7 +49,7 @@ def wrapper(*args, **kwargs): class TestVersion(unittest.TestCase): @patch_env_vars("gen_ai_latest_experimental", "SPAN_ONLY") - def test_get_content_caputring_mode_parses_valid_envvar(self): # pylint: disable=no-self-use + def test_get_content_capturing_mode_parses_valid_envvar(self): # pylint: disable=no-self-use _OpenTelemetrySemanticConventionStability._initialized = False assert get_content_capturing_mode() == ContentCapturingMode.SPAN_ONLY @@ -70,5 +70,9 @@ def test_get_content_caputring_mode_raises_exception_when_semconv_stability_defa def test_get_content_caputring_mode_raises_exception_on_invalid_envvar( self, ): # pylint: disable=no-self-use - with self.assertRaises(RuntimeError): - get_content_capturing_mode() + with self.assertLogs(logger, 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]) From 6a66251a86b132ddc3a30911dde34c3abf0606cf Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Wed, 3 Sep 2025 15:29:25 +0000 Subject: [PATCH 14/20] Address comments --- .../tests/test_utils.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index ecfda16c84..675b6eba5f 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -24,10 +24,10 @@ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ) from opentelemetry.util.genai.types import ContentCapturingMode -from opentelemetry.util.genai.utils import get_content_capturing_mode, logger +from opentelemetry.util.genai.utils import get_content_capturing_mode -def patch_env_vars(*, stability_mode, content_capturing): +def patch_env_vars(stability_mode, content_capturing): def decorator(test_case): @patch.dict( os.environ, @@ -48,29 +48,34 @@ def wrapper(*args, **kwargs): class TestVersion(unittest.TestCase): - @patch_env_vars("gen_ai_latest_experimental", "SPAN_ONLY") + @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 - _OpenTelemetrySemanticConventionStability._initialized = False assert get_content_capturing_mode() == ContentCapturingMode.SPAN_ONLY - @patch_env_vars("gen_ai_latest_experimental", "") + @patch_env_vars( + stability_mode="gen_ai_latest_experimental", content_capturing="" + ) def test_empty_content_capturing_envvar(self): # pylint: disable=no-self-use - _OpenTelemetrySemanticConventionStability._initialized = False assert get_content_capturing_mode() == ContentCapturingMode.NO_CONTENT - @patch_env_vars("default", "True") + @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 - _OpenTelemetrySemanticConventionStability._initialized = False with self.assertRaises(ValueError): get_content_capturing_mode() - @patch_env_vars("gen_ai_latest_experimental", "INVALID_VALUE") + @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(logger, level="WARNING") as cm: + with self.assertLogs(level="WARNING") as cm: assert ( get_content_capturing_mode() == ContentCapturingMode.NO_CONTENT ) From 1b60ec1cde3d4b7be16ab5fbae6579de6631e2a1 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Wed, 3 Sep 2025 15:32:37 +0000 Subject: [PATCH 15/20] Address comment --- opentelemetry-instrumentation/tests/test_semconv.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/opentelemetry-instrumentation/tests/test_semconv.py b/opentelemetry-instrumentation/tests/test_semconv.py index 4c88a782bb..98befb32b7 100644 --- a/opentelemetry-instrumentation/tests/test_semconv.py +++ b/opentelemetry-instrumentation/tests/test_semconv.py @@ -106,7 +106,7 @@ def test_genai_latest_experimental(self): _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, ) - @stability_mode("database,http") + @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( @@ -120,6 +120,12 @@ def test_multiple_stability_database_http_modes(self): ), _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): From 1387c6c67d7fcc7dd0fe7536ffd6152e6e629949 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Wed, 3 Sep 2025 15:34:45 +0000 Subject: [PATCH 16/20] Fix typecheck --- .../src/opentelemetry/instrumentation/vertexai/events.py | 1 - 1 file changed, 1 deletion(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/events.py b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/events.py index 990af83eaf..48afcb69df 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/events.py +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/events.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# type: ignore[reportUnknownDeprecated] """ Factories for event types described in From a8640cc6a6c273144b96461e6218f29cbbf4aed9 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Wed, 3 Sep 2025 15:36:42 +0000 Subject: [PATCH 17/20] don't change typecheck, --- .../src/opentelemetry/instrumentation/vertexai/events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/events.py b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/events.py index 48afcb69df..990af83eaf 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/events.py +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/events.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +# type: ignore[reportUnknownDeprecated] """ Factories for event types described in From e9adb53d2ef0b14c7041dcc4ba315863849ae02f Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Wed, 3 Sep 2025 15:49:43 +0000 Subject: [PATCH 18/20] Fix linter --- .../src/opentelemetry/util/genai/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py index 75f5ea414b..04247a0fcf 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py @@ -45,6 +45,8 @@ def get_content_capturing_mode() -> ContentCapturingMode: return ContentCapturingMode[envvar.upper()] except KeyError: logger.warning( - f"{envvar} is not a valid option for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. Must be one of {', '.join(e.name for e in ContentCapturingMode)}. Defaulting to `NO_COTENT`" + "%s is not a valid option for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. Must be one of %s. Defaulting to `NO_COTENT`.", + envvar, + ", ".join(e.name for e in ContentCapturingMode), ) return ContentCapturingMode.NO_CONTENT From f6aef69633baebabd301922a2ea398dc766f7f7d Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 4 Sep 2025 21:31:37 +0000 Subject: [PATCH 19/20] Address comments --- CHANGELOG.md | 2 ++ util/opentelemetry-util-genai/CHANGELOG.md | 5 ++++- .../src/opentelemetry/util/genai/utils.py | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a840094e..4a95525734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/util/opentelemetry-util-genai/CHANGELOG.md b/util/opentelemetry-util-genai/CHANGELOG.md index 6209a70d6f..8a6b7ec6df 100644 --- a/util/opentelemetry-util-genai/CHANGELOG.md +++ b/util/opentelemetry-util-genai/CHANGELOG.md @@ -5,4 +5,7 @@ All notable changes to this project will be documented in this file. 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 \ No newline at end of file +## 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. \ No newline at end of file diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py index 04247a0fcf..91cb9221f1 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py @@ -29,6 +29,9 @@ 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( @@ -45,8 +48,9 @@ def get_content_capturing_mode() -> ContentCapturingMode: return ContentCapturingMode[envvar.upper()] except KeyError: logger.warning( - "%s is not a valid option for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. Must be one of %s. Defaulting to `NO_COTENT`.", + "%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 From 691eeec9d146a4932b7c64404f94619fcd2411d6 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 4 Sep 2025 21:48:27 +0000 Subject: [PATCH 20/20] Fix order of args.. --- .../src/opentelemetry/util/genai/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py index 6a845a20b6..569e7e7e00 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -31,17 +31,17 @@ class ContentCapturingMode(Enum): @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[ @@ -51,8 +51,8 @@ class ToolCallResponse: @dataclass() class Text: - type: Literal["text"] = "text" content: str + type: Literal["text"] = "text" MessagePart = Union[Text, ToolCall, ToolCallResponse, Any]