diff --git a/manifests/php.yml b/manifests/php.yml index 049e49fb250..10eca76b562 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -1659,7 +1659,7 @@ manifest: ? tests/parametric/test_otel_span_methods.py::Test_Otel_Span_Methods::test_otel_span_extended_reserved_attributes_overrides_analytics_event : - declaration: missing_feature (Implemented in 0.96.0) component_version: <=0.95.0 - tests/parametric/test_otel_span_methods.py::Test_Otel_Span_Methods::test_otel_span_link_attribute_handling: missing_feature (Not implemented, does not break out arrays into dot notation) + tests/parametric/test_otel_span_methods.py::Test_Otel_Span_Methods::test_otel_span_link_attribute_handling: v1.26.0-dev tests/parametric/test_otel_span_methods.py::Test_Otel_Span_Methods::test_otel_span_started_with_link_from_another_span: - declaration: missing_feature (Implemented in 0.97.0) component_version: <0.97.0 @@ -1721,9 +1721,24 @@ manifest: tests/parametric/test_sampling_span_tags.py::Test_Sampling_Span_Tags::test_tags_defaults_rate_1_and_rate_limit_3_sst010: bug (APMAPI-737) tests/parametric/test_sampling_span_tags.py::Test_Sampling_Span_Tags::test_tags_defaults_rate_1_and_rule_0_sst006: bug (APMAPI-737) tests/parametric/test_sampling_span_tags.py::Test_Sampling_Span_Tags::test_tags_defaults_rate_tiny_sst004: bug (APMAPI-737) - tests/parametric/test_span_events.py: missing_feature - tests/parametric/test_span_events.py::Test_Span_Events::test_span_with_event_v05: '>=1.16.0' - tests/parametric/test_span_links.py: missing_feature + tests/parametric/test_sampling_span_tags.py::Test_Sampling_Span_Tags::test_tags_defaults_sst002: + - declaration: bug (APMAPI-1545) + component_version: '>=1.26.0-dev' + tests/parametric/test_span_events.py::Test_Span_Events::test_span_with_event_v04: v1.26.0-dev + tests/parametric/test_span_events.py::Test_Span_Events::test_span_with_event_v05: + - declaration: missing_feature + component_version: '<1.16.0' + - declaration: irrelevant (PHP does not support v0.5; V1 sends span events natively) + component_version: '>=1.26.0-dev' + tests/parametric/test_span_events.py::Test_Span_Events::test_span_with_event_v07: v1.26.0-dev + tests/parametric/test_span_events.py::Test_Span_Events::test_span_with_invalid_event_attributes: missing_feature + tests/parametric/test_span_links.py::Test_Span_Links::test_span_link_from_distributed_datadog_headers: missing_feature + tests/parametric/test_span_links.py::Test_Span_Links::test_span_link_from_distributed_w3c_headers: missing_feature + tests/parametric/test_span_links.py::Test_Span_Links::test_span_link_propagated_sampling_decisions: missing_feature + tests/parametric/test_span_links.py::Test_Span_Links::test_span_started_with_link_v04: v1.26.0-dev + tests/parametric/test_span_links.py::Test_Span_Links::test_span_started_with_link_v05: missing_feature + tests/parametric/test_span_links.py::Test_Span_Links::test_span_started_with_link_v05_span_id_padding: missing_feature + tests/parametric/test_span_links.py::Test_Span_Links::test_span_with_attached_links: v1.26.0-dev ? tests/parametric/test_span_sampling.py::Test_Span_Sampling::test_child_span_selected_and_root_dropped_by_sss_when_dropping_policy_is_active017 : missing_feature (The PHP tracer always sends the full trace to the agent.) tests/parametric/test_span_sampling.py::Test_Span_Sampling::test_entire_trace_dropped_when_dropping_policy_is_active018: missing_feature (The PHP tracer always sends the full trace to the agent.) @@ -2477,4 +2492,5 @@ manifest: tests/test_telemetry.py::Test_TelemetrySCAEnvVar: v0.99.1 tests/test_telemetry.py::Test_TelemetryV2: v0.90 tests/test_telemetry.py::Test_TelemetryV2::test_app_started_product_info: missing_feature (Product started missing (both in libdatadog and php)) - tests/test_v1_payloads.py: missing_feature + tests/test_v1_payloads.py: v1.26.0-dev + tests/test_v1_payloads.py::Test_V1SpanLinks: missing_feature (no span link is created from conflicting datadog and tracecontext contexts, see Test_Span_Links_From_Conflicting_Contexts) diff --git a/tests/appsec/api_security/test_endpoints.py b/tests/appsec/api_security/test_endpoints.py index 85a08a04ed7..85feb34bac3 100644 --- a/tests/appsec/api_security/test_endpoints.py +++ b/tests/appsec/api_security/test_endpoints.py @@ -7,7 +7,7 @@ def assert_llm_span(request: HttpResponse, model: str) -> None: span = interfaces.library.get_root_span(request) assert span["meta"]["appsec.events.llm.call.provider"] == "openai" assert span["meta"]["appsec.events.llm.call.model"] == model - assert span["metrics"]["_sampling_priority_v1"] == 2 + assert span.get_sampling_priority() == 2 @rfc( diff --git a/tests/appsec/test_automated_payment_events.py b/tests/appsec/test_automated_payment_events.py index 51b910bf7c4..7443c4caf9a 100644 --- a/tests/appsec/test_automated_payment_events.py +++ b/tests/appsec/test_automated_payment_events.py @@ -49,7 +49,7 @@ def _validator(span: DataDogLibrarySpan): return False assert span["meta"]["appsec.events.payments.integration"] == "stripe" - assert span["metrics"]["_sampling_priority_v1"] == 2 + assert span.get_sampling_priority() == 2 return validator(span) diff --git a/tests/appsec/test_trace_tagging.py b/tests/appsec/test_trace_tagging.py index c2c01893775..d01d659084b 100644 --- a/tests/appsec/test_trace_tagging.py +++ b/tests/appsec/test_trace_tagging.py @@ -33,7 +33,9 @@ def validate(span: DataDogLibrarySpan): assert span["meta"]["_dd.appsec.trace.agent"].startswith("TraceTagging/v1") assert span["metrics"]["_dd.appsec.trace.integer"] == 662607015 - assert span["metrics"].get("_sampling_priority_v1") < SamplingPriority.USER_KEEP + sampling_priority = span.get_sampling_priority() + assert sampling_priority is not None + assert sampling_priority < SamplingPriority.USER_KEEP return True @@ -55,7 +57,7 @@ def validate(span: DataDogLibrarySpan): assert span["meta"]["_dd.appsec.trace.agent"].startswith("TraceTagging/v2") assert span["metrics"]["_dd.appsec.trace.integer"] == 602214076 - assert span["metrics"].get("_sampling_priority_v1") == SamplingPriority.USER_KEEP + assert span.get_sampling_priority() == SamplingPriority.USER_KEEP return True @@ -77,7 +79,7 @@ def validate(span: DataDogLibrarySpan): assert span["meta"]["_dd.appsec.trace.agent"].startswith("TraceTagging/v3") assert span["metrics"]["_dd.appsec.trace.integer"] == 299792458 - assert span["metrics"].get("_sampling_priority_v1") == SamplingPriority.USER_KEEP + assert span.get_sampling_priority() == SamplingPriority.USER_KEEP return True @@ -100,7 +102,9 @@ def validate(span: DataDogLibrarySpan): assert span["meta"]["_dd.appsec.trace.agent"].startswith("TraceTagging/v4") assert span["metrics"]["_dd.appsec.trace.integer"] == 1729 - assert span["metrics"].get("_sampling_priority_v1") < SamplingPriority.USER_KEEP + sampling_priority = span.get_sampling_priority() + assert sampling_priority is not None + assert sampling_priority < SamplingPriority.USER_KEEP return True diff --git a/tests/integrations/test_db_integrations_sql.py b/tests/integrations/test_db_integrations_sql.py index b8cf0399936..d13ae1e3e19 100644 --- a/tests/integrations/test_db_integrations_sql.py +++ b/tests/integrations/test_db_integrations_sql.py @@ -83,8 +83,10 @@ def test_db_name(self): def test_span_kind(self, excluded_operations: tuple[str, ...] = ()): """Describes the relationship between the Span, its parents, and its children in a Trace.""" - for _, span_meta in self.get_spans_meta(excluded_operations): - assert span_meta["span.kind"] == "client" + for _, lib_span, agent_span in self.get_spans(excluded_operations): + assert lib_span.meta["span.kind"] == "client" + # in the efficient trace payload format, the agent reports the kind as a top-level enum field + assert agent_span.get_span_kind() in ("client", "SPAN_KIND_CLIENT") def test_runtime_id(self): """Unique identifier for the current process.""" diff --git a/tests/parametric/test_otel_api_interoperability.py b/tests/parametric/test_otel_api_interoperability.py index a96416cb0b7..00a95258d7a 100644 --- a/tests/parametric/test_otel_api_interoperability.py +++ b/tests/parametric/test_otel_api_interoperability.py @@ -4,6 +4,7 @@ from opentelemetry.trace import SpanKind from utils.docker_fixtures.spec.trace import find_trace, find_span, retrieve_span_links, find_only_span, find_root_span from utils.docker_fixtures import TestAgentAPI, ParametricTestClientApi as APMLibrary +from utils.dd_types import is_same_boolean # this global mark applies to all tests in this file. @@ -447,7 +448,8 @@ def test_set_attribute_from_otel(self, test_agent: TestAgentAPI, test_library: A assert root is not None assert root["metrics"]["int"] == 1 assert root["metrics"]["float"] == 1.0 - assert root["meta"]["bool"] == "true" + # With v1.0, tracers can send typed booleans, which dd-apm-test-agent converts to numeric 1/0. + assert is_same_boolean(actual={**root["meta"], **root["metrics"]}.get("bool"), expected="true") assert root["meta"]["str"] == "string" assert "none" not in root["meta"] assert root["meta"]["str_array.0"] == "a" diff --git a/tests/parametric/test_tracer.py b/tests/parametric/test_tracer.py index fae0f54c895..77c108a917d 100644 --- a/tests/parametric/test_tracer.py +++ b/tests/parametric/test_tracer.py @@ -68,10 +68,7 @@ def test_tracer_repository_url_environment_variable( assert len(trace) == 2 first_span = find_first_span_in_trace_payload(trace) - # the repository url should be injected ONLY in the first span of the trace - spans_with_git = [span for span in trace if span.get("meta", {}).get("_dd.git.repository_url")] - assert len(spans_with_git) == 1 - assert first_span == spans_with_git[0] + # v1 sets git tags at the payload level, so other spans may carry them too assert first_span["meta"]["_dd.git.repository_url"] == library_env["DD_GIT_REPOSITORY_URL"] @parametrize("library_env", [{"DD_GIT_COMMIT_SHA": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]) @@ -95,11 +92,7 @@ def test_tracer_commit_sha_environment_variable( assert len(trace) == 2 first_span = find_first_span_in_trace_payload(trace) - # the repository url should be injected ONLY in the first span of the trace - spans_with_git = [span for span in trace if span.get("meta", {}).get("_dd.git.commit.sha")] - assert len(spans_with_git) == 1 - assert first_span == spans_with_git[0] - + # v1 sets git tags at the payload level, so other spans may carry them too assert first_span["meta"]["_dd.git.commit.sha"] == library_env["DD_GIT_COMMIT_SHA"] @parametrize( diff --git a/tests/test_data_integrity.py b/tests/test_data_integrity.py index f2ad793f64c..d237ef4a65b 100644 --- a/tests/test_data_integrity.py +++ b/tests/test_data_integrity.py @@ -48,7 +48,7 @@ def test_trace_header_diagnostic_check(self): def validator(data: dict): request_headers = {h[0].lower() for h in data["request"]["headers"]} - if "x-datadog-diagnostic-check" in request_headers and len(data["request"]["content"]) != 0: + if "x-datadog-diagnostic-check" in request_headers and _trace_count(data) != 0: raise ValueError("Tracer sent a dignostic request with traces in it") interfaces.library.validate_all_traces(validator=validator, allow_no_trace=True) @@ -64,7 +64,7 @@ def validator(data: dict): except ValueError as e: raise ValueError(f"'x-datadog-trace-count' request header is not an integer: {value}") from e - if trace_count != len(data["request"]["content"]): + if trace_count != _trace_count(data): raise ValueError("x-datadog-trace-count request header didn't match the number of traces") interfaces.library.validate_all_traces(validator=validator, allow_no_trace=True) @@ -234,7 +234,7 @@ def get_span_with_sampling_data(trace: DataDogLibraryTrace): # sampling priority tag will be returned. This is the same logic found on the trace-agent. span_with_sampling_data = None for span in trace: - if span.get("metrics", {}).get("_sampling_priority_v1", None) is not None: + if span.get_sampling_priority() is not None: if span.get("parent_id") in (0, None): return span elif span_with_sampling_data is None: @@ -250,8 +250,7 @@ def get_span_with_sampling_data(trace: DataDogLibraryTrace): if not span: continue - metrics = span["metrics"] - sampling_priority = metrics.get("_sampling_priority_v1") + sampling_priority = span.get_sampling_priority() if sampling_priority in (SamplingPriority.AUTO_KEEP, SamplingPriority.USER_KEEP): trace_ids_reported_by_tracer.add(trace.trace_id_as_int) if trace.trace_id_as_int not in trace_ids_reported_by_agent: @@ -266,5 +265,13 @@ def get_span_with_sampling_data(trace: DataDogLibraryTrace): raise ValueError("Some traces have not been reported by the agent. See logs for more details") +def _trace_count(data: dict) -> int: + """Number of traces in a /traces request: v1.0 payloads carry them as chunks""" + content = data["request"]["content"] + if data["path"] == "/v1.0/traces": + return len(content.get("chunks") or []) + return len(content) + + def _empty_request(data: dict): return "content" not in data["request"] or not data["request"]["content"] diff --git a/tests/test_sampling_rates.py b/tests/test_sampling_rates.py index 7f51d35ca4e..d65d057f564 100644 --- a/tests/test_sampling_rates.py +++ b/tests/test_sampling_rates.py @@ -77,17 +77,6 @@ def trace_should_be_kept(sampling_rate: float, trace_id: int): return ((trace_id * SAMPLING_KNUTH_FACTOR) % SAMPLING_MODULO) <= (sampling_rate * MAX_UINT64) -def _spans_with_parent(traces: list, parent_ids: list): - if not isinstance(traces, list): - logger.error("Traces should be an array") - yield from [] # do not fail here, it's schema's job - else: - for trace in traces: - for span in trace: - if span.get("parent_id") in parent_ids: - yield span - - def generate_request_id() -> Generator[int, Any, Any]: i = 0 while True: @@ -260,13 +249,16 @@ def test_sampling_determinism(self): traces = {trace["parent_id"]: trace for trace in self.traces_determinism} sampling_decisions_per_trace_id = defaultdict(list) - def validator(data: dict): - for span in _spans_with_parent(data["request"]["content"], list(traces.keys())): - expected_trace_id = traces[(span["parent_id"])]["trace_id"] - sampling_priority = span["metrics"].get("_sampling_priority_v1") - sampling_decisions_per_trace_id[span["trace_id"]].append(sampling_priority) + for data, trace in interfaces.library.get_traces(): + for span in trace: + if span.get("parent_id") not in traces: + continue + + expected_trace_id = traces[span["parent_id"]]["trace_id"] + sampling_priority = span.get_sampling_priority() + sampling_decisions_per_trace_id[expected_trace_id].append(sampling_priority) - assert span["trace_id"] == expected_trace_id, ( + assert span.trace_id_equals(expected_trace_id), ( f"Message: {data['log_filename']}: If parent_id matches, " f"trace_id should match too expected trace_id {expected_trace_id} " f"span trace_id : {span['trace_id']}, span parent_id : {span['parent_id']}", @@ -276,8 +268,6 @@ def validator(data: dict): f"Message: {data['log_filename']}: sampling priority should be set" ) - interfaces.library.validate_all(validator, path_filters=["/v0.4/traces", "/v0.5/traces"], allow_no_data=True) - for trace_id, decisions in sampling_decisions_per_trace_id.items(): if len(decisions) < 2: continue diff --git a/utils/build/docker/php/common/rewrite-rules.conf b/utils/build/docker/php/common/rewrite-rules.conf index f718e47a2fb..8481b72f2a3 100644 --- a/utils/build/docker/php/common/rewrite-rules.conf +++ b/utils/build/docker/php/common/rewrite-rules.conf @@ -36,6 +36,7 @@ RewriteRule "^/trace/sql$" "/trace_sql/" RewriteRule "^/trace/mongo$" "/trace_mongo/" RewriteRule "^/e2e_otel_span$" "/e2e_otel_span/" RewriteRule "^/e2e_single_span$" "/e2e_single_span/" +RewriteRule "^/add_event$" "/add_event/" RewriteRule "^/crashme$" "/crashme/" RewriteRule "^/ffe$" "/ffe/" RewriteRule "^/exceptionreplay/(.*)$" "/debugger/exceptionreplay/$1" [QSA] diff --git a/utils/build/docker/php/weblogs/laravel11x/routes/web.php b/utils/build/docker/php/weblogs/laravel11x/routes/web.php index 9ad126951ea..4e413d98bd7 100644 --- a/utils/build/docker/php/weblogs/laravel11x/routes/web.php +++ b/utils/build/docker/php/weblogs/laravel11x/routes/web.php @@ -48,6 +48,17 @@ return response('', $code); }); +Route::get('/add_event', function () { + $span = \DDTrace\root_span(); + if ($span === null) { + return response('root span not found', 500); + } + + $span->events[] = new \DDTrace\SpanEvent('span.event', ['string' => 'value', 'int' => 1]); + + return response('[Event added]', 200); +}); + Route::get('/trace/manual_keep_drop', function (Request $request) { $decision = $request->query('decision', ''); if ($decision !== 'keep' && $decision !== 'drop') { diff --git a/utils/build/docker/php/weblogs/plain/add_event.php b/utils/build/docker/php/weblogs/plain/add_event.php new file mode 100644 index 00000000000..4b5e88b9df7 --- /dev/null +++ b/utils/build/docker/php/weblogs/plain/add_event.php @@ -0,0 +1,13 @@ +events[] = new \DDTrace\SpanEvent('span.event', ['string' => 'value', 'int' => 1]); + +echo '[Event added]'; diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php index 33a413d9b51..3881bf6ef5d 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php @@ -75,6 +75,19 @@ public function status(Request $request): Response return new Response('', $code); } + #[Route('/add_event', name: 'add_event', methods: ['GET'])] + public function addEvent(): Response + { + $span = \DDTrace\root_span(); + if ($span === null) { + return new Response('root span not found', 500); + } + + $span->events[] = new \DDTrace\SpanEvent('span.event', ['string' => 'value', 'int' => 1]); + + return new Response('[Event added]', 200); + } + #[Route('/trace/manual_keep_drop', name: 'trace_manual_keep_drop', methods: ['GET'])] public function traceManualKeepDrop(Request $request): Response { diff --git a/utils/dd_types/_datadog_library_trace.py b/utils/dd_types/_datadog_library_trace.py index 384307d29d3..2e9f86f3ce5 100644 --- a/utils/dd_types/_datadog_library_trace.py +++ b/utils/dd_types/_datadog_library_trace.py @@ -184,7 +184,7 @@ def metrics(self) -> dict[str, Any]: return self.raw_span.get("metrics", {}) def get_sampling_priority(self) -> int | None: - return self["metrics"].get("_sampling_priority_v1") + return self.metrics.get("_sampling_priority_v1") def get_span_links(self) -> list[DataDogSpanLink]: if "span_links" in self.raw_span: diff --git a/utils/interfaces/_library/core.py b/utils/interfaces/_library/core.py index e89689520d0..3b5e61d243f 100644 --- a/utils/interfaces/_library/core.py +++ b/utils/interfaces/_library/core.py @@ -426,7 +426,7 @@ def add_appsec_reported_header(self, request: HttpResponse, header_name: str): self.validate_one_appsec(request, validator=validator.validate, legacy_validator=validator.validate_legacy) def validate_all_traces(self, validator: Callable[[dict], None], *, allow_no_trace: bool = False): - self.validate_all(validator=validator, allow_no_data=allow_no_trace, path_filters=r"/v0\.[1-9]+/traces") + self.validate_all(validator=validator, allow_no_data=allow_no_trace, path_filters=r"/v[0-9]+\.[0-9]+/traces") def validate_one_trace(self, request: HttpResponse, validator: Callable[[DataDogLibraryTrace], bool]): """Will call validator() on all traces trigerred by request. validator() returns a boolean :