diff --git a/src/splunk_ao/converter/attribute_mapping.py b/src/splunk_ao/converter/attribute_mapping.py index 77aa2efb..39b5dde9 100644 --- a/src/splunk_ao/converter/attribute_mapping.py +++ b/src/splunk_ao/converter/attribute_mapping.py @@ -16,7 +16,6 @@ SPLUNK_AO_SYSTEM = "splunk_ao.system" SPLUNK_AO_SYSTEM_VALUE = "splunk_ao_python" -SPLUNK_AO_SIMULATION_RUN_ID = "splunk_ao.simulation.run_id" CONTENT_ALIAS_BY_GEN_AI: Mapping[str, str] = { "gen_ai.input.messages": "splunk_ao.input.messages", diff --git a/src/splunk_ao/converter/span_converter.py b/src/splunk_ao/converter/span_converter.py index 33da32a7..6b748975 100644 --- a/src/splunk_ao/converter/span_converter.py +++ b/src/splunk_ao/converter/span_converter.py @@ -11,8 +11,8 @@ from opentelemetry.trace import SpanContext, SpanKind, Status, StatusCode from galileo_core.schemas.logging.step import BaseStep, StepType -from splunk_ao.converter.attribute_mapping import SPLUNK_AO_SIMULATION_RUN_ID, build_span_attributes -from splunk_ao.simulation_run_context import validate_simulation_run_id +from splunk_ao.converter.attribute_mapping import build_span_attributes +from splunk_ao.simulation_run_context import SPLUNK_AO_SIMULATION_RUN_ID, validate_simulation_run_id from splunk_ao.utils.headers_data import get_package_version _EPOCH = datetime(1970, 1, 1, tzinfo=UTC) diff --git a/src/splunk_ao/otel.py b/src/splunk_ao/otel.py index 522e194f..d89f42bf 100644 --- a/src/splunk_ao/otel.py +++ b/src/splunk_ao/otel.py @@ -17,7 +17,6 @@ from galileo_core.schemas.logging.span import Span as SplunkAOSpan from splunk_ao.config import SplunkAOConfig from splunk_ao.converter import build_span_attributes -from splunk_ao.converter.attribute_mapping import SPLUNK_AO_SIMULATION_RUN_ID from splunk_ao.decorator import ( _agent_stream_context, _dataset_input_context, @@ -36,7 +35,7 @@ ) from splunk_ao.exporter.diagnostics import get_export_health from splunk_ao.session_context import get_effective_session_id -from splunk_ao.simulation_run_context import get_simulation_run_id +from splunk_ao.simulation_run_context import SPLUNK_AO_SIMULATION_RUN_ID, get_simulation_run_id logger = logging.getLogger(__name__) diff --git a/src/splunk_ao/session_context.py b/src/splunk_ao/session_context.py index 98bd89f2..6717cf6f 100644 --- a/src/splunk_ao/session_context.py +++ b/src/splunk_ao/session_context.py @@ -11,8 +11,11 @@ from opentelemetry.context import Context from opentelemetry.propagators import textmap -from splunk_ao.converter.attribute_mapping import SPLUNK_AO_SIMULATION_RUN_ID -from splunk_ao.simulation_run_context import get_baggage_simulation_run_id, get_simulation_run_id +from splunk_ao.simulation_run_context import ( + SPLUNK_AO_SIMULATION_RUN_ID, + get_baggage_simulation_run_id, + get_simulation_run_id, +) GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" diff --git a/src/splunk_ao/simulation_run_context.py b/src/splunk_ao/simulation_run_context.py index 9296d402..09735e5b 100644 --- a/src/splunk_ao/simulation_run_context.py +++ b/src/splunk_ao/simulation_run_context.py @@ -5,16 +5,16 @@ from opentelemetry import baggage from opentelemetry.context import Context -from splunk_ao.converter.attribute_mapping import SPLUNK_AO_SIMULATION_RUN_ID +SPLUNK_AO_SIMULATION_RUN_ID = "splunk_ao.simulation.run_id" _simulation_run_id_context: ContextVar[str | None] = ContextVar("simulation_run_id_context", default=None) def validate_simulation_run_id(simulation_run_id: object) -> str: - """Validate a public simulation run ID and return it unchanged.""" + """Validate a public simulation run ID and return its normalized value.""" if not isinstance(simulation_run_id, str) or not simulation_run_id.strip(): raise ValueError("simulation_run_id must be a non-empty string") - return simulation_run_id + return simulation_run_id.strip() def get_baggage_simulation_run_id(context: Context | None = None) -> str | None: diff --git a/tests/test_http_instrumentation.py b/tests/test_http_instrumentation.py index 6f10e030..c7997bc1 100644 --- a/tests/test_http_instrumentation.py +++ b/tests/test_http_instrumentation.py @@ -32,7 +32,6 @@ from yarl import URL from splunk_ao import configure_distributed_tracing, instrument_distributed_tracing, splunk_ao_context -from splunk_ao.converter.attribute_mapping import SPLUNK_AO_SIMULATION_RUN_ID from splunk_ao.http_instrumentation import ( _client_providers, _configured_providers, @@ -41,6 +40,7 @@ ) from splunk_ao.logger.logger import SplunkAOLogger from splunk_ao.session_context import GEN_AI_CONVERSATION_ID, SplunkAOSessionPropagator, _session_id_context +from splunk_ao.simulation_run_context import SPLUNK_AO_SIMULATION_RUN_ID class FakeTracerProvider: diff --git a/tests/test_logger_otel_egress.py b/tests/test_logger_otel_egress.py index eb59070a..427a006e 100644 --- a/tests/test_logger_otel_egress.py +++ b/tests/test_logger_otel_egress.py @@ -7,13 +7,12 @@ from opentelemetry import context, propagate, trace from opentelemetry.sdk.trace import ReadableSpan -from splunk_ao.converter.attribute_mapping import SPLUNK_AO_SIMULATION_RUN_ID from splunk_ao.deployment import DeploymentMode from splunk_ao.exceptions import SplunkAOLoggerException from splunk_ao.exporter.span_transform import copy_span_for_export from splunk_ao.logger import SplunkAOLogger from splunk_ao.shared.exceptions import MissingConfigurationError -from splunk_ao.simulation_run_context import _simulation_run_id_context +from splunk_ao.simulation_run_context import SPLUNK_AO_SIMULATION_RUN_ID, _simulation_run_id_context from splunk_ao.utils.singleton import SplunkAOLoggerSingleton diff --git a/tests/test_otel_native_paths.py b/tests/test_otel_native_paths.py index 01949d88..bc161ed7 100644 --- a/tests/test_otel_native_paths.py +++ b/tests/test_otel_native_paths.py @@ -14,7 +14,6 @@ from galileo_core.schemas.logging.span import WorkflowSpan from splunk_ao import get_tracing_headers -from splunk_ao.converter.attribute_mapping import SPLUNK_AO_SIMULATION_RUN_ID from splunk_ao.decorator import ( _agent_stream_context, _dataset_input_context, @@ -35,6 +34,7 @@ start_splunk_ao_span, ) from splunk_ao.shared.exceptions import MissingConfigurationError +from splunk_ao.simulation_run_context import SPLUNK_AO_SIMULATION_RUN_ID ROUTING_KEYS = { "splunk_ao.project.name", diff --git a/tests/test_span_converter.py b/tests/test_span_converter.py index ab825e36..ea41543c 100644 --- a/tests/test_span_converter.py +++ b/tests/test_span_converter.py @@ -13,9 +13,10 @@ from galileo_core.schemas.logging.trace import Trace from galileo_core.schemas.shared.document import Document from splunk_ao.converter import SpanConverter, span_converter -from splunk_ao.converter.attribute_mapping import SPLUNK_AO_SIMULATION_RUN_ID, build_span_attributes +from splunk_ao.converter.attribute_mapping import build_span_attributes from splunk_ao.logger.control import ControlAppliesTo, ControlCheckStage, ControlResult, ControlSpan from splunk_ao.schema.logged import LoggedControlSpan +from splunk_ao.simulation_run_context import SPLUNK_AO_SIMULATION_RUN_ID from splunk_ao.utils.headers_data import get_package_version TRACE_ID = 0x1234567890ABCDEF1234567890ABCDEF @@ -192,6 +193,15 @@ def test_simulation_run_id_is_a_direct_attribute_for_every_span_type( assert "splunk_ao.experiment.id" not in (result.attributes or {}) +def test_converter_normalizes_simulation_run_id_before_export() -> None: + result = convert( + WorkflowSpan(input="work", output="done", name="workflow", created_at=CREATED_AT), + simulation_run_id=" run-1 ", + ) + + assert (result.attributes or {})[SPLUNK_AO_SIMULATION_RUN_ID] == "run-1" + + def test_fully_populated_logged_control_span_preserves_otlp_envelope_and_attributes() -> None: parent_context = make_context(span_id=PARENT_SPAN_ID) span = LoggedControlSpan( diff --git a/tests/test_splunk_ao_context.py b/tests/test_splunk_ao_context.py index e0d70da7..14af4177 100644 --- a/tests/test_splunk_ao_context.py +++ b/tests/test_splunk_ao_context.py @@ -84,6 +84,16 @@ def test_simulation_run_scope_restores_the_previous_value() -> None: _simulation_run_id_context.reset(outer_token) +def test_simulation_run_id_is_normalized_at_public_context_boundaries(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(splunk_ao_context, "get_logger_instance", Mock()) + + with splunk_ao_context.simulation_run(" run-1\n"): + assert _simulation_run_id_context.get() == "run-1" + + with splunk_ao_context(simulation_run_id="\trun-2 "): + assert _simulation_run_id_context.get() == "run-2" + + def test_init_clears_active_simulation_run(monkeypatch: pytest.MonkeyPatch) -> None: logger = Mock() singleton = Mock() diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 99685474..04e6f666 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -6,12 +6,11 @@ from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags, TraceState from splunk_ao import extract_tracing_context, get_tracing_headers, splunk_ao_context -from splunk_ao.converter.attribute_mapping import SPLUNK_AO_SIMULATION_RUN_ID from splunk_ao.exceptions import SplunkAOLoggerException from splunk_ao.logger import SplunkAOLogger from splunk_ao.logger.logger import _otel_context_state from splunk_ao.session_context import GEN_AI_CONVERSATION_ID, _session_id_context -from splunk_ao.simulation_run_context import _simulation_run_id_context +from splunk_ao.simulation_run_context import SPLUNK_AO_SIMULATION_RUN_ID, _simulation_run_id_context class RecordingSink: