This is an automated email from the ASF dual-hosted git repository.

bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 4c21c45319 Add proxy.process.http.429_responses metric (#12878)
4c21c45319 is described below

commit 4c21c453192e86d72e721d77764d91f30158746e
Author: Brian Neradt <[email protected]>
AuthorDate: Thu Feb 19 12:24:13 2026 -0600

    Add proxy.process.http.429_responses metric (#12878)
    
    Add a new HTTP stat to track 429 (Too Many Requests) responses sent to
    clients. This is useful for monitoring rate-limiting behavior, as the
    rate_limit plugin defaults to returning 429 when limits are exceeded.
    The metric follows the same pattern as existing per-status-code counters
    (400-416) across HttpConfig, HttpTransact, traffic_top, and
    traffic_logstats.
    
    Also enhance the ATSReplayTest framework with a metric_checks feature
    that allows replay YAML files to declare expected metric values. After
    traffic completes, follow-up test runs automatically verify the metrics
    via traffic_ctl. This required creating the ATS process on the Test
    object (rather than a TestRun) when post-traffic verification is needed,
    so that ATS persists across test runs.
---
 .../statistics/core/http-response-code.en.rst      |  3 +
 doc/developer-guide/testing/autests.en.rst         | 22 ++++++
 include/proxy/http/HttpConfig.h                    |  1 +
 src/proxy/http/HttpConfig.cc                       |  1 +
 src/proxy/http/HttpTransact.cc                     |  3 +
 src/traffic_logstats/logstats.cc                   |  5 ++
 src/traffic_logstats/tests/logstats.json           |  3 +
 src/traffic_logstats/tests/logstats.summary        |  1 +
 src/traffic_top/stats.h                            |  1 +
 tests/gold_tests/autest-site/ats_replay.test.ext   | 44 ++++++++++-
 .../statistics/metric_response_429.test.py         | 20 +++++
 .../replay/metric_response_429.replay.yaml         | 85 ++++++++++++++++++++++
 12 files changed, 188 insertions(+), 1 deletion(-)

diff --git 
a/doc/admin-guide/monitoring/statistics/core/http-response-code.en.rst 
b/doc/admin-guide/monitoring/statistics/core/http-response-code.en.rst
index 5efeada65c..56904f284c 100644
--- a/doc/admin-guide/monitoring/statistics/core/http-response-code.en.rst
+++ b/doc/admin-guide/monitoring/statistics/core/http-response-code.en.rst
@@ -136,6 +136,9 @@ code means.
 .. ts:stat:: global proxy.process.http.416_responses integer
    :type: counter
 
+.. ts:stat:: global proxy.process.http.429_responses integer
+   :type: counter
+
 .. ts:stat:: global proxy.process.http.4xx_responses integer
    :type: counter
 
diff --git a/doc/developer-guide/testing/autests.en.rst 
b/doc/developer-guide/testing/autests.en.rst
index 64cf40fe4f..04a02e5712 100644
--- a/doc/developer-guide/testing/autests.en.rst
+++ b/doc/developer-guide/testing/autests.en.rst
@@ -251,6 +251,11 @@ YAML node). Here is an example:
              - expression: "Unwanted message in diags.log"
                description: "Verify this does NOT appear in diags.log"
 
+       # Optional: Verify ATS metric values after traffic completes.
+       metric_checks:
+         - metric: "proxy.process.http.200_responses"
+           value: 1
+
    # Traffic specification using Proxy Verifier format
    # client-request and server-response generate request and response traffic
    #   toward the ATS proxy.
@@ -353,6 +358,7 @@ The ``autest`` section configures the test environment:
   - **remap_config**: List of remap rules (string or dict format)
   - **copy_to_config_dir**: List of files/directories to copy to ATS config 
directory
   - **log_validation**: Log validation rules for ``traffic_out`` and 
``diags_log``
+  - **metric_checks**: List of metric name/value pairs to verify after traffic 
completes
 
 Log Validation
 ~~~~~~~~~~~~~~
@@ -375,6 +381,22 @@ The ``log_validation`` section allows you to verify the 
contents of
          - expression: "Plugin initialized"
            description: "Verify plugin loaded"
 
+Metric Verification
+~~~~~~~~~~~~~~~~~~~~
+
+The ``metric_checks`` section allows you to verify |TS| metric values after all
+traffic in the test has completed. Each entry specifies a metric name and its
+expected value. After the traffic test run, ``ATSReplayTest`` automatically 
adds
+follow-up test runs that use ``traffic_ctl metric get`` to verify each metric.
+
+.. code-block:: yaml
+
+   metric_checks:
+     - metric: "proxy.process.http.429_responses"
+       value: 1
+     - metric: "proxy.process.http.200_responses"
+       value: 2
+
 Sessions and Transactions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/include/proxy/http/HttpConfig.h b/include/proxy/http/HttpConfig.h
index 85cb7ea433..4ac5e07183 100644
--- a/include/proxy/http/HttpConfig.h
+++ b/include/proxy/http/HttpConfig.h
@@ -245,6 +245,7 @@ struct HttpStatsBlock {
   Metrics::Counter::AtomicType *response_status_414_count;
   Metrics::Counter::AtomicType *response_status_415_count;
   Metrics::Counter::AtomicType *response_status_416_count;
+  Metrics::Counter::AtomicType *response_status_429_count;
   Metrics::Counter::AtomicType *response_status_4xx_count;
   Metrics::Counter::AtomicType *response_status_500_count;
   Metrics::Counter::AtomicType *response_status_501_count;
diff --git a/src/proxy/http/HttpConfig.cc b/src/proxy/http/HttpConfig.cc
index d58f7d7c01..a34f77d21e 100644
--- a/src/proxy/http/HttpConfig.cc
+++ b/src/proxy/http/HttpConfig.cc
@@ -436,6 +436,7 @@ register_stat_callbacks()
   http_rsb.response_status_414_count         = 
Metrics::Counter::createPtr("proxy.process.http.414_responses");
   http_rsb.response_status_415_count         = 
Metrics::Counter::createPtr("proxy.process.http.415_responses");
   http_rsb.response_status_416_count         = 
Metrics::Counter::createPtr("proxy.process.http.416_responses");
+  http_rsb.response_status_429_count         = 
Metrics::Counter::createPtr("proxy.process.http.429_responses");
   http_rsb.response_status_4xx_count         = 
Metrics::Counter::createPtr("proxy.process.http.4xx_responses");
   http_rsb.response_status_500_count         = 
Metrics::Counter::createPtr("proxy.process.http.500_responses");
   http_rsb.response_status_501_count         = 
Metrics::Counter::createPtr("proxy.process.http.501_responses");
diff --git a/src/proxy/http/HttpTransact.cc b/src/proxy/http/HttpTransact.cc
index 3cc8d3dc54..08cccee0ef 100644
--- a/src/proxy/http/HttpTransact.cc
+++ b/src/proxy/http/HttpTransact.cc
@@ -8590,6 +8590,9 @@ HttpTransact::client_result_stat(State *s, ink_hrtime 
total_time, ink_hrtime req
     case 416:
       Metrics::Counter::increment(http_rsb.response_status_416_count);
       break;
+    case 429:
+      Metrics::Counter::increment(http_rsb.response_status_429_count);
+      break;
     case 500:
       Metrics::Counter::increment(http_rsb.response_status_500_count);
       break;
diff --git a/src/traffic_logstats/logstats.cc b/src/traffic_logstats/logstats.cc
index fec49eb02b..f9099b8c9c 100644
--- a/src/traffic_logstats/logstats.cc
+++ b/src/traffic_logstats/logstats.cc
@@ -209,6 +209,7 @@ struct OriginStats {
     StatsCounter c_415;
     StatsCounter c_416;
     StatsCounter c_417;
+    StatsCounter c_429;
     StatsCounter c_4xx;
     StatsCounter c_500;
     StatsCounter c_501;
@@ -1083,6 +1084,9 @@ update_codes(OriginStats *stat, int code, int size)
   case 417:
     update_counter(stat->codes.c_417, size);
     break;
+  case 429:
+    update_counter(stat->codes.c_429, size);
+    break;
 
   // 500's
   case 500:
@@ -2116,6 +2120,7 @@ print_detail_stats(const OriginStats *stat, bool json, 
bool concise)
   format_line(json ? "status.415" : "415 Unsupported Media Type", 
stat->codes.c_415, stat->total, json, concise);
   format_line(json ? "status.416" : "416 Req Range Not Satisfiable", 
stat->codes.c_416, stat->total, json, concise);
   format_line(json ? "status.417" : "417 Expectation Failed", 
stat->codes.c_417, stat->total, json, concise);
+  format_line(json ? "status.429" : "429 Too Many Requests", 
stat->codes.c_429, stat->total, json, concise);
   format_line(json ? "status.4xx" : "4xx Total", stat->codes.c_4xx, 
stat->total, json, concise);
 
   if (!json) {
diff --git a/src/traffic_logstats/tests/logstats.json 
b/src/traffic_logstats/tests/logstats.json
index f12731142b..15c5893cfa 100644
--- a/src/traffic_logstats/tests/logstats.json
+++ b/src/traffic_logstats/tests/logstats.json
@@ -53,6 +53,7 @@
     "status.415" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.416" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.417" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
+    "status.429" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.4xx" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.500" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.501" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
@@ -181,6 +182,7 @@
     "status.415" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.416" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.417" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
+    "status.429" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.4xx" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.500" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.501" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
@@ -309,6 +311,7 @@
     "status.415" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.416" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.417" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
+    "status.429" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.4xx" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.500" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
     "status.501" : { "req": "0", "req_pct": "0.00", "bytes": "0", "bytes_pct": 
"0.00" },
diff --git a/src/traffic_logstats/tests/logstats.summary 
b/src/traffic_logstats/tests/logstats.summary
index 111812f9a4..9a297455d0 100644
--- a/src/traffic_logstats/tests/logstats.summary
+++ b/src/traffic_logstats/tests/logstats.summary
@@ -66,6 +66,7 @@ HTTP return codes                      Count    Percent       
Bytes    Percent
 415 Unsupported Media Type                 0      0.00%      0.00KB      0.00%
 416 Req Range Not Satisfiable              0      0.00%      0.00KB      0.00%
 417 Expectation Failed                     0      0.00%      0.00KB      0.00%
+429 Too Many Requests                      0      0.00%      0.00KB      0.00%
 4xx Total                                  0      0.00%      0.00KB      0.00%
 
 500 Internal Server Error                  0      0.00%      0.00KB      0.00%
diff --git a/src/traffic_top/stats.h b/src/traffic_top/stats.h
index 035151eb5e..fa0aae1b69 100644
--- a/src/traffic_top/stats.h
+++ b/src/traffic_top/stats.h
@@ -231,6 +231,7 @@ public:
     lookup_table.insert(make_pair("414", LookupItem("414", 
"proxy.process.http.414_responses", 5)));
     lookup_table.insert(make_pair("415", LookupItem("415", 
"proxy.process.http.415_responses", 5)));
     lookup_table.insert(make_pair("416", LookupItem("416", 
"proxy.process.http.416_responses", 5)));
+    lookup_table.insert(make_pair("429", LookupItem("429", 
"proxy.process.http.429_responses", 5)));
     lookup_table.insert(make_pair("4xx", LookupItem("4xx", 
"proxy.process.http.4xx_responses", 5)));
     lookup_table.insert(make_pair("500", LookupItem("500", 
"proxy.process.http.500_responses", 5)));
     lookup_table.insert(make_pair("501", LookupItem("501", 
"proxy.process.http.501_responses", 5)));
diff --git a/tests/gold_tests/autest-site/ats_replay.test.ext 
b/tests/gold_tests/autest-site/ats_replay.test.ext
index c041c42770..b731f8a5ea 100644
--- a/tests/gold_tests/autest-site/ats_replay.test.ext
+++ b/tests/gold_tests/autest-site/ats_replay.test.ext
@@ -19,6 +19,7 @@ Implement general-purpose ATS test extensions using proxy 
verifier replay files.
 
 from typing import Optional
 import os
+import re
 import yaml
 
 
@@ -126,6 +127,24 @@ def configure_ats(obj: 'TestRun', server: 'Process', 
ats_config: dict, dns: Opti
     return ts
 
 
+def _requires_persistent_ats(ats_config: dict) -> bool:
+    '''Determine whether ATS must span multiple test runs.
+
+    Some features, such as metric verification, require additional test runs
+    using the ATS process after traffic completes. In those cases the ATS
+    process must be owned by Test rather than a TestRun so it persists across
+    test runs.
+
+    :param ats_config: The autest.ats configuration from the replay file.
+    :returns: True if ATS must span multiple test runs.
+    '''
+
+    # This will become more complicated over time. For example, it would be 
nice
+    # to add transaction log support which would require a longer-lived ATS
+    # process.
+    return bool(ats_config.get('metric_checks'))
+
+
 def ATSReplayTest(obj, replay_file: str):
     '''Create a TestRun that configures ATS and runs HTTP traffic using the 
replay file.
 
@@ -195,7 +214,10 @@ def ATSReplayTest(obj, replay_file: str):
         raise ValueError(f"Replay file {replay_file} does not contain 
'autest.ats' section")
     ats_config = autest_config['ats']
     enable_tls = ats_config.get('enable_tls', False)
-    ts = configure_ats(tr, server=server, ats_config=ats_config, dns=dns)
+    metric_checks = ats_config.get('metric_checks', [])
+
+    ats_owner = obj if _requires_persistent_ats(ats_config) else tr
+    ts = configure_ats(ats_owner, server=server, ats_config=ats_config, 
dns=dns)
 
     # Proxy Verifier Client configuration.
     if not 'client' in autest_config:
@@ -230,6 +252,26 @@ def ATSReplayTest(obj, replay_file: str):
     ts.StartBefore(server)
     client.StartBefore(ts)
 
+    # Metric verification test runs.
+    if metric_checks:
+        tr.StillRunningAfter = ts
+
+        wait_tr = obj.AddTestRun('Wait for stats to propagate')
+        wait_tr.Processes.Default.Command = 'sleep 2'
+        wait_tr.Processes.Default.ReturnCode = 0
+        wait_tr.StillRunningAfter = ts
+
+        for check in metric_checks:
+            metric_name = check['metric']
+            expected_value = check['value']
+            check_tr = obj.AddTestRun(f'Verify {metric_name} == 
{expected_value}')
+            check_tr.Processes.Default.Command = f'traffic_ctl metric get 
{metric_name}'
+            check_tr.Processes.Default.Env = ts.Env
+            check_tr.Processes.Default.ReturnCode = 0
+            check_tr.Processes.Default.Streams.All = 
Testers.ContainsExpression(
+                f'^{re.escape(metric_name)}\\s+{expected_value}$', 
f'{metric_name} should be {expected_value}')
+            check_tr.StillRunningAfter = ts
+
     return tr
 
 
diff --git a/tests/gold_tests/statistics/metric_response_429.test.py 
b/tests/gold_tests/statistics/metric_response_429.test.py
new file mode 100644
index 0000000000..ae4e5e5838
--- /dev/null
+++ b/tests/gold_tests/statistics/metric_response_429.test.py
@@ -0,0 +1,20 @@
+'''Verify the proxy.process.http.429_responses metric.'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you 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.
+
+Test.Summary = __doc__
+
+Test.ATSReplayTest(replay_file="replay/metric_response_429.replay.yaml")
diff --git a/tests/gold_tests/statistics/replay/metric_response_429.replay.yaml 
b/tests/gold_tests/statistics/replay/metric_response_429.replay.yaml
new file mode 100644
index 0000000000..b326bdac06
--- /dev/null
+++ b/tests/gold_tests/statistics/replay/metric_response_429.replay.yaml
@@ -0,0 +1,85 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you 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.
+
+meta:
+  version: "1.0"
+
+autest:
+  description: 'Verify proxy.process.http.429_responses metric is incremented'
+
+  dns:
+    name: 'dns'
+
+  server:
+    name: 'server'
+
+  client:
+    name: 'client'
+
+  ats:
+    name: 'ts'
+
+    process_config:
+      enable_cache: false
+
+    remap_config:
+      - from: "http://example.com/";
+        to: "http://backend.example.com:{SERVER_HTTP_PORT}/";
+
+    metric_checks:
+      - metric: "proxy.process.http.429_responses"
+        value: 1
+
+sessions:
+- transactions:
+
+  - client-request:
+      method: GET
+      url: /rate-limited
+      version: '1.1'
+      headers:
+        fields:
+        - [Host, example.com]
+        - [uuid, 429-response]
+
+    server-response:
+      status: 429
+      reason: Too Many Requests
+      headers:
+        fields:
+        - [Content-Length, "0"]
+
+    proxy-response:
+      status: 429
+
+  - client-request:
+      method: GET
+      url: /ok
+      version: '1.1'
+      headers:
+        fields:
+        - [Host, example.com]
+        - [uuid, 200-response]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [Content-Length, "0"]
+
+    proxy-response:
+      status: 200

Reply via email to