diff --git a/ddtrace/appsec/_asm_request_context.py b/ddtrace/appsec/_asm_request_context.py index c29b059fc35..57ff604f917 100644 --- a/ddtrace/appsec/_asm_request_context.py +++ b/ddtrace/appsec/_asm_request_context.py @@ -15,6 +15,7 @@ from ddtrace.appsec._constants import APPSEC from ddtrace.appsec._constants import SPAN_DATA_NAMES from ddtrace.appsec._constants import Constant_Class +from ddtrace.appsec._metrics import UNKNOWN_VERSION from ddtrace.appsec._metrics import report_waf_run_error from ddtrace.appsec._metrics import report_waf_truncation from ddtrace.appsec._metrics import set_waf_request_metrics @@ -24,9 +25,11 @@ from ddtrace.appsec._utils import is_inferred_span from ddtrace.contrib.internal.trace_utils_base import _normalize_tag_name from ddtrace.internal import core +from ddtrace.internal import telemetry from ddtrace.internal._exceptions import BlockingException import ddtrace.internal.logger as ddlogger from ddtrace.internal.settings.asm import config as asm_config +from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE if TYPE_CHECKING: @@ -276,10 +279,20 @@ def flush_waf_triggers(env: ASM_Environment) -> None: entry_span._set_attribute(APPSEC.WAF_VERSION, asm_config._ddwaf_version) if env.downstream_requests: update_span_metrics(entry_span, APPSEC.DOWNSTREAM_REQUESTS, env.downstream_requests) + tags = ( + ("event_rules_version", telemetry_results.version or UNKNOWN_VERSION), + ("waf_version", asm_config._ddwaf_version), + ) if telemetry_results.total_duration: update_span_metrics(entry_span, APPSEC.WAF_DURATION, telemetry_results.duration) + telemetry.telemetry_writer.add_distribution_metric( + TELEMETRY_NAMESPACE.APPSEC, "waf.duration", telemetry_results.duration, tags=tags + ) telemetry_results.duration = 0.0 update_span_metrics(entry_span, APPSEC.WAF_DURATION_EXT, telemetry_results.total_duration) + telemetry.telemetry_writer.add_distribution_metric( + TELEMETRY_NAMESPACE.APPSEC, "waf.duration_ext", telemetry_results.total_duration, tags=tags + ) telemetry_results.total_duration = 0.0 if telemetry_results.timeout: update_span_metrics(entry_span, APPSEC.WAF_TIMEOUTS, telemetry_results.timeout) @@ -290,6 +303,12 @@ def flush_waf_triggers(env: ASM_Environment) -> None: update_span_metrics(entry_span, APPSEC.RASP_DURATION, telemetry_results.rasp.duration) update_span_metrics(entry_span, APPSEC.RASP_DURATION_EXT, telemetry_results.rasp.total_duration) update_span_metrics(entry_span, APPSEC.RASP_RULE_EVAL, telemetry_results.rasp.sum_eval) + telemetry.telemetry_writer.add_distribution_metric( + TELEMETRY_NAMESPACE.APPSEC, "rasp.duration", telemetry_results.rasp.duration, tags=tags + ) + telemetry.telemetry_writer.add_distribution_metric( + TELEMETRY_NAMESPACE.APPSEC, "rasp.duration_ext", telemetry_results.rasp.total_duration, tags=tags + ) if telemetry_results.truncation.string_length: entry_span._set_attribute(APPSEC.TRUNCATION_STRING_LENGTH, max(telemetry_results.truncation.string_length)) if telemetry_results.truncation.container_size: diff --git a/releasenotes/notes/appsec-waf-rasp-duration-telemetry-9d214f6730b34ad1.yaml b/releasenotes/notes/appsec-waf-rasp-duration-telemetry-9d214f6730b34ad1.yaml new file mode 100644 index 00000000000..30efa990158 --- /dev/null +++ b/releasenotes/notes/appsec-waf-rasp-duration-telemetry-9d214f6730b34ad1.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + aap: Adds instrumentation telemetry distributions for WAF and RASP execution + durations, including ``waf.duration``, ``waf.duration_ext``, + ``rasp.duration``, and ``rasp.duration_ext``. diff --git a/tests/appsec/appsec/test_telemetry.py b/tests/appsec/appsec/test_telemetry.py index af30d35f6d9..2193d4fa99c 100644 --- a/tests/appsec/appsec/test_telemetry.py +++ b/tests/appsec/appsec/test_telemetry.py @@ -7,11 +7,14 @@ import ddtrace.appsec._asm_request_context as asm_request_context from ddtrace.appsec._constants import APPSEC +from ddtrace.appsec._constants import EXPLOIT_PREVENTION import ddtrace.appsec._ddwaf.ddwaf_types import ddtrace.appsec._ddwaf.waf from ddtrace.appsec._deduplications import deduplication from ddtrace.appsec._processor import AppSecSpanProcessor from ddtrace.appsec._remoteconfiguration import enable_asm +from ddtrace.appsec._utils import DDWaf_result +from ddtrace.appsec._utils import _observator from ddtrace.constants import APPSEC_ENV from ddtrace.contrib.internal.trace_utils import set_http_meta from ddtrace.ext import SpanTypes @@ -76,22 +79,6 @@ def _assert_generate_metrics(metrics_result, is_rule_triggered=False, is_blocked pytest.fail("Unexpected generate_metrics {}".format(metric_name)) -def _assert_distributions_metrics(metrics_result, is_rule_triggered=False, is_blocked_request=False): - distributions_metrics = metrics_result[TELEMETRY_EVENT_TYPE.DISTRIBUTIONS][TELEMETRY_NAMESPACE.APPSEC.value] - - assert len(distributions_metrics) == 2, "Expected 2 distributions_metrics" - for metric in distributions_metrics: - if metric["metric"] in ["waf.duration", "waf.duration_ext"]: - assert len(metric["points"]) >= 1 - assert isinstance(metric["points"][0], float) - assert f"rule_triggered:{str(is_rule_triggered).lower()}" in metric["tags"] - assert f"request_blocked:{str(is_blocked_request).lower()}" in metric["tags"] - assert f"waf_version:{asm_config._ddwaf_version}" in metric["tags"] - assert any("event_rules_version" in t for t in metric["tags"]) - else: - pytest.fail("Unexpected distributions_metrics {}".format(metric["metric"])) - - def test_metrics_when_appsec_doesnt_runs(telemetry_writer, tracer): with override_global_config(dict(_asm_enabled=False)): tracer.configure(appsec_enabled=False) @@ -193,6 +180,60 @@ def test_report_user_auth_missing(telemetry_writer, user_id, user_login, report_ for metric in user_auth_metrics: assert "framework:django" in metric["tags"] assert "event_type:login_failure" in metric["tags"] + + +def test_waf_duration_distribution_metrics(telemetry_writer, tracer): + telemetry_writer._namespace.flush() + with asm_context(tracer=tracer, span_name="test", config=config_asm) as span: + set_http_meta(span, rules.Config()) + + distributions_metrics = telemetry_writer._namespace.flush()[TELEMETRY_EVENT_TYPE.DISTRIBUTIONS][ + TELEMETRY_NAMESPACE.APPSEC.value + ] + waf_metrics = {metric["metric"]: metric for metric in distributions_metrics if metric["metric"].startswith("waf.")} + + assert set(waf_metrics) == {"waf.duration", "waf.duration_ext"} + for metric in waf_metrics.values(): + assert len(metric["points"]) >= 1 + assert isinstance(metric["points"][0], float) + assert f"waf_version:{asm_config._ddwaf_version}" in metric["tags"] + assert any(tag.startswith("event_rules_version:") for tag in metric["tags"]) + assert len(metric["tags"]) == 2 + + +def test_rasp_duration_distribution_metrics(telemetry_writer, tracer): + telemetry_writer._namespace.flush() + with asm_context(tracer=tracer, span_name="test", config=config_asm): + waf_result = DDWaf_result(0, [], {}, 12.5, 20.25, False, _observator(), {}) + asm_request_context.set_waf_telemetry_results( + "rules_rasp", + False, + waf_result, + EXPLOIT_PREVENTION.TYPE.SQLI, + False, + ) + waf_result = DDWaf_result(0, [], {}, 3.0, 4.0, False, _observator(), {}) + asm_request_context.set_waf_telemetry_results( + "rules_rasp", + False, + waf_result, + EXPLOIT_PREVENTION.TYPE.LFI, + False, + ) + + distributions_metrics = telemetry_writer._namespace.flush()[TELEMETRY_EVENT_TYPE.DISTRIBUTIONS][ + TELEMETRY_NAMESPACE.APPSEC.value + ] + rasp_metrics = { + metric["metric"]: metric for metric in distributions_metrics if metric["metric"].startswith("rasp.") + } + + assert set(rasp_metrics) == {"rasp.duration", "rasp.duration_ext"} + assert rasp_metrics["rasp.duration"]["points"] == [15.5] + assert rasp_metrics["rasp.duration_ext"]["points"] == [24.25] + for metric in rasp_metrics.values(): + assert f"waf_version:{asm_config._ddwaf_version}" in metric["tags"] + assert any(tag.startswith("event_rules_version:") for tag in metric["tags"]) assert len(metric["tags"]) == 2