bryancall commented on code in PR #13506:
URL: https://github.com/apache/trafficserver/pull/13506#discussion_r3854592636


##########
doc/admin-guide/files/records.yaml.en.rst:
##########
@@ -2007,6 +2007,80 @@ Origin Server Connect Attempts
    the connection. Useful when the origin supports keep-alive, removing the 
time needed to set up a
    new connection from the next request at the expense of added (inactive) 
connections.
 
+.. ts:cv:: CONFIG proxy.config.http.per_server.connection.metric_enabled INT 0
+   :reloadable:
+   :overridable:
+
+   Enable per upstream server connection metrics. These metrics are 
dynamically named, one set per
+   upstream server group, so the number of them scales with the number of 
distinct upstream servers
+   seen. See :ref:`per-server-connection-metrics`.
+
+   ===== 
======================================================================================
+   Value Effect
+   ===== 
======================================================================================
+   ``0`` No per server connection metrics.
+   ``1`` Per server connection metrics are collected for each upstream server 
group.
+   ===== 
======================================================================================
+
+   What is published from them is controlled separately by
+   :ts:cv:`proxy.config.http.per_server.connection.metric_aggregate`, which by 
default publishes the
+   per group metrics themselves.
+
+   Because this is overridable, metrics can be enabled for the upstreams of 
interest and left off
+   for the rest, for example with :ref:`admin-plugins-conf-remap` on a 
specific mapping.
+
+   The value is applied when a connection group is created. Where two mappings 
that disagree about
+   this setting resolve to the same group -- that is, the same key under
+   :ts:cv:`proxy.config.http.per_server.connection.match` -- the transaction 
that creates the group
+   determines its metrics, and later transactions do not change them. A group 
is discarded once its
+   connection count reaches zero, so the choice is made again the next time 
that upstream is
+   reopened. This affects only which metrics exist; enforcement of

Review Comment:
   This is the sentence I would change. "the choice is made again the next time 
that upstream is reopened" reads as full reversibility, and it only holds for 
increasing publication. Turning metrics off, or switching `metric_aggregate` to 
`2`, leaves the already-published names in place for the life of the process.
   
   ```suggestion
      determines its metrics, and later transactions do not change them. A 
group is discarded once its
      connection count reaches zero, so *raising* the level of publication is 
picked up the next time
      that upstream is reopened: enabling metrics, or enabling the aggregates, 
takes effect as upstreams
      reconnect. Lowering it does not. Metrics are never retired once 
published, so disabling this
      setting, or switching
      :ts:cv:`proxy.config.http.per_server.connection.metric_aggregate` to 
``2``, leaves the names that
      are already published in place, frozen at their last sampled value, until 
Traffic Server is
      restarted. This affects only which metrics exist; enforcement of
   ```
   
   The same caveat is needed on `metric_aggregate` below, which currently just 
points back here with "the same consequence for mappings that disagree and 
resolve to the same group". It is worth spelling out there too, since `2` is 
the value an operator reaches for specifically to reduce a metric count, and 
that is the one direction a reload cannot deliver.



##########
doc/admin-guide/monitoring/statistics/core/http-connection.en.rst:
##########
@@ -201,6 +201,64 @@ HTTP Connection
    Current number of TCP connections for tunnels where the far end is the 
server,
    except for those counted by 
``proxy.process.tunnel.current_server_connections_tls``
 
+.. _per-server-connection-metrics:
+
+Per Server Connection Metrics
+-----------------------------
+
+Unlike the metrics above these do not have fixed names. They are created 
dynamically, one set per
+upstream server group as defined by 
:ts:cv:`proxy.config.http.per_server.connection.match`, and,
+when the match type is ``both``, one aggregate set per hostname. Whether they 
are collected at all is
+controlled by :ts:cv:`proxy.config.http.per_server.connection.metric_enabled`, 
and which of them are
+published by 
:ts:cv:`proxy.config.http.per_server.connection.metric_aggregate`. An optional
+:ts:cv:`proxy.config.http.per_server.connection.metric_prefix` can be inserted 
into the names.
+
+Per group names are ``proxy.process.http.per_server.<counter>.<group>``, where 
``<group>`` depends on
+the match type: an IP address, an ``address:port`` pair, a hostname, or, for 
``both``,
+``<hostname>.<address:port>``. Per hostname names are
+``proxy.process.http.per_server.<counter>.<hostname>``. Aggregates exist only 
for match type
+``both``, because that is the only match type with more than one group per 
hostname; for match type
+``host`` the group name is already the bare hostname, so an aggregate would 
carry the same name as
+the single group it summarises.
+
+For a group, ``<counter>`` is one of:
+
+current_connection
+   Gauge. The number of connections currently open to the group.
+
+total_connection
+   Counter. The total number of connections ever opened to the group. Never 
decreases.
+
+blocked_connection
+   Counter. The total number of connection attempts to the group blocked by
+   :ts:cv:`proxy.config.http.per_server.connection.max`. Never decreases.
+
+For a hostname aggregate, ``<counter>`` is one of those three, each summed 
across the groups of that
+hostname, plus:

Review Comment:
   "each summed across the groups of that hostname" is what an operator will 
rely on, and it can be false. Aggregate membership is registered per group, 
gated on the `metric_aggregate` value of the transaction that created that 
group, so under `match: both` a mapping with `metric_aggregate: 1` joins the 
aggregate and a mapping with `0` for the same hostname does not. The sum then 
covers a strict subset, and `current_connection_max` takes its maximum over 
that subset, with nothing indicating it.
   
   ```suggestion
   For a hostname aggregate, ``<counter>`` is one of those three, each summed 
across the groups of that
   hostname which have aggregation enabled, plus:
   ```
   
   I would add a short paragraph after the `current_connection_max` entry as 
well, along the lines of: because `metric_aggregate` is overridable, a group 
joins its hostname's aggregate only if the mapping that first opened that 
upstream had aggregation enabled, so mappings that disagree for one hostname 
produce an aggregate over part of it. Making membership a per-hostname decision 
is the better fix, but that can go with the `remove_source` follow-up.



##########
doc/admin-guide/monitoring/statistics/core/http-connection.en.rst:
##########
@@ -201,6 +201,64 @@ HTTP Connection
    Current number of TCP connections for tunnels where the far end is the 
server,
    except for those counted by 
``proxy.process.tunnel.current_server_connections_tls``
 
+.. _per-server-connection-metrics:
+
+Per Server Connection Metrics
+-----------------------------
+
+Unlike the metrics above these do not have fixed names. They are created 
dynamically, one set per
+upstream server group as defined by 
:ts:cv:`proxy.config.http.per_server.connection.match`, and,
+when the match type is ``both``, one aggregate set per hostname. Whether they 
are collected at all is
+controlled by :ts:cv:`proxy.config.http.per_server.connection.metric_enabled`, 
and which of them are
+published by 
:ts:cv:`proxy.config.http.per_server.connection.metric_aggregate`. An optional
+:ts:cv:`proxy.config.http.per_server.connection.metric_prefix` can be inserted 
into the names.
+
+Per group names are ``proxy.process.http.per_server.<counter>.<group>``, where 
``<group>`` depends on
+the match type: an IP address, an ``address:port`` pair, a hostname, or, for 
``both``,
+``<hostname>.<address:port>``. Per hostname names are
+``proxy.process.http.per_server.<counter>.<hostname>``. Aggregates exist only 
for match type
+``both``, because that is the only match type with more than one group per 
hostname; for match type
+``host`` the group name is already the bare hostname, so an aggregate would 
carry the same name as
+the single group it summarises.

Review Comment:
   "the only match type with more than one group per hostname" is not accurate, 
and the same claim appears in `records.yaml.en.rst`, in the `MetricAggregate` 
doc comment, and on `host_metric_name`. `Group::hash` and `Group::equal` key 
`MATCH_IP` on the address alone, so one hostname with several A records already 
yields several groups.
   
   The real reason is narrower and, I think, more useful to a future reader: 
`ip` and `port` keys carry no hostname at all, and one such group is shared by 
every hostname that resolves to that address, so there is nothing to aggregate 
it under. As written, someone will eventually relax the restriction on the 
strength of a premise that is false.
   
   ```suggestion
   ``proxy.process.http.per_server.<counter>.<hostname>``. Aggregates exist 
only for match type
   ``both``, because that is the only match type whose group key carries the 
hostname. An ``ip`` or
   ``port`` group is keyed on the address alone and is shared by every hostname 
that resolves to it, so
   there is no single hostname to aggregate it under. For match type ``host`` 
the group name is already
   the bare hostname, so an aggregate would carry the same name as the single 
group it summarises.
   ```



##########
src/iocore/net/ConnectionTracker.cc:
##########
@@ -440,11 +475,45 @@ ConnectionTracker::Group::Group(DirectionType direction, 
Key const &key, std::st
 {
   Metrics::Gauge::increment(net_rsb.connection_tracker_table_size);
   // only add metrics for server connections
-  if (_global_config->metric_enabled && direction == DirectionType::OUTBOUND) {
+  if (metric_enabled && direction == DirectionType::OUTBOUND) {
     std::string _metric_name = metric_name(key, fqdn, 
_global_config->metric_prefix);
-    _count_metric            = 
Metrics::Gauge::createPtr("proxy.process.http.per_server.current_connection.", 
_metric_name);
-    _count_total_metric      = 
Metrics::Counter::createPtr("proxy.process.http.per_server.total_connection.", 
_metric_name);
-    _blocked_metric          = 
Metrics::Counter::createPtr("proxy.process.http.per_server.blocked_connection.",
 _metric_name);
+    // Per group metrics always live in the hidden store. metric_aggregate 
controls what is
+    // published from them (see MetricAggregate), not whether they exist.
+    _count_metric       = 
Metrics::Gauge::createHiddenPtr("proxy.process.http.per_server.current_connection.",
 _metric_name);
+    _count_total_metric = 
Metrics::Counter::createHiddenPtr("proxy.process.http.per_server.total_connection.",
 _metric_name);
+    _blocked_metric     = 
Metrics::Counter::createHiddenPtr("proxy.process.http.per_server.blocked_connection.",
 _metric_name);
+
+    // Only MATCH_BOTH groups have siblings sharing a hostname to aggregate 
across.
+    std::string _host_metric_name = host_metric_name(key, fqdn, 
_global_config->metric_prefix);
+    bool const  has_aggregate     = !_host_metric_name.empty();
+
+    if (has_aggregate && metric_aggregate != AGGREGATE_NONE) {
+      
Metrics::Derived::add_source("proxy.process.http.per_server.current_connection."
 + _host_metric_name,
+                                   Metrics::MetricType::GAUGE, _count_metric, 
Metrics::Derived::Op::SUM);
+      
Metrics::Derived::add_source("proxy.process.http.per_server.total_connection." 
+ _host_metric_name,
+                                   Metrics::MetricType::COUNTER, 
_count_total_metric, Metrics::Derived::Op::SUM);
+      
Metrics::Derived::add_source("proxy.process.http.per_server.blocked_connection."
 + _host_metric_name,
+                                   Metrics::MetricType::COUNTER, 
_blocked_metric, Metrics::Derived::Op::SUM);
+      // The largest current count among this hostname's groups, sampled. 
Deliberately taken over
+      // the instantaneous gauge rather than each group's all time peak, so 
the value falls again
+      // and a maximum over time can be computed by whatever scrapes it.
+      
Metrics::Derived::add_source("proxy.process.http.per_server.current_connection_max."
 + _host_metric_name,
+                                   Metrics::MetricType::GAUGE, _count_metric, 
Metrics::Derived::Op::MAX);
+    }
+
+    // AGGREGATE_ONLY suppresses the per group metrics to keep the published 
count proportional to
+    // hostnames. Without an aggregate to stand in for them there would be 
nothing at all reported
+    // for this group, so in that case publish them regardless.
+    if (metric_aggregate != AGGREGATE_ONLY || !has_aggregate) {
+      // Mirror the per group metrics into the published store under their own 
name. A single
+      // source SUM is an identity: the published value always equals the 
hidden source.

Review Comment:
   "the published value always equals the hidden source" is the one claim in 
this change that contradicts its own design. The published copy is stale 
between derived ticks and reads 0 from creation until the first one, which is 
precisely the property `http-connection.en.rst` takes care to explain. A 
single-source SUM does not combine anything, but it is still a sample rather 
than an identity.
   
   ```suggestion
         // Mirror the per group metrics into the published store under their 
own name. A single
         // source SUM combines nothing, but the published value is still a 
sample: it is whatever
         // the last derived tick read, and it reads 0 from creation until that 
first tick.
   ```



##########
src/iocore/net/ConnectionTracker.cc:
##########
@@ -70,10 +72,28 @@ const MgmtConverter ConnectionTracker::SERVER_MATCH_CONV{
     }
   }};
 
+// Neither of these clamps, for the same reason as SERVER_MATCH_CONV above: 
the InkAPITest
+// regression test requires an arbitrary integer to round trip through the 
setter and getter. The
+// records paths do the range checking instead -- records.yaml validates the 
value and the reload
+// callbacks below clamp -- so an out of range value is only reachable by a 
plugin that sets one
+// deliberately. Both settings degrade safely if that happens: any non-zero 
metric_enabled enables
+// metrics, and any metric_aggregate outside 0..2 publishes both the aggregate 
and the per group
+// metrics, the same as AGGREGATE_GROUP.
+const MgmtConverter ConnectionTracker::METRIC_ENABLED_CONV{

Review Comment:
   Not blocking, and I accept the reasoning in the comment above for why 
neither converter clamps. What I would still like is a diagnostic somewhere on 
this path, because right now there is none anywhere in the new code.
   
   `SERVER_MATCH_CONV`, three converters up, has a formatter and calls 
`Warning_Bad_Match_Type`. Both new reload callbacks clamp silently, and their 
`return false` on a wrong `dtype` is swallowed, since the `RecCore` lambda 
returns `REC_ERR_OKAY` unconditionally and `traffic_ctl` reports success either 
way. The sibling `Config_Update_Conntrack_Match` does at least emit 
`Warning("Invalid type for '%s'")`. The net effect is that a plugin setting 
`metric_aggregate = 7` reads back 7, behaves as `AGGREGATE_GROUP`, and leaves 
no trace of either fact.
   
   Related, and the same size of fix: the debug print at line 520 says 
`Registered per_server_connection.{}` without saying what was actually 
registered, hidden or published, aggregate or per group. That line is the first 
place an operator will look to answer "why can I not see my metric", which is 
going to be a common question given how many ways this feature can decline to 
publish something.



##########
include/iocore/net/ConnectionTracker.h:
##########
@@ -171,6 +206,20 @@ class ConnectionTracker
     std::time_t        get_last_alert_epoch_time() const;
     static std::string metric_name(const Key &key, std::string_view fqdn, 
std::string metric_prefix);
 
+    /** Name of the metric which aggregates a value across all groups of a 
hostname.
+     *
+     * Only @c MATCH_BOTH groups have more than one group per hostname. For @c 
MATCH_HOST there is
+     * exactly one group per hostname, so an aggregate would be over a set of 
one, and
+     * @c Group::metric_name already returns the FQDN alone for that match 
type - identical to what
+     * this would return, so publishing both would collide on one name.

Review Comment:
   Two problems in this paragraph. The first sentence repeats the inaccurate 
reason described on the monitoring page. The second is that the collision 
argument is right within one match type and does not hold across two, which 
matters because `server_match` is itself overridable: foo.com can be 
`MATCH_HOST` on one mapping and `MATCH_BOTH` on another, at which point 
`metric_name` for the host group and `host_metric_name` for the aggregate 
produce the identical string. `add_source` then finds the existing entry by id, 
appends the source, and ignores the later caller's op, so one published name 
silently carries the union of a per-group mirror and a cross-group aggregate.
   
   ```suggestion
        * Only @c MATCH_BOTH keys carry both a hostname and an address, so it 
is the only match type
        * whose groups can be gathered by hostname at all. @c MATCH_IP and @c 
MATCH_PORT key on the
        * address alone and one such group is shared by every hostname 
resolving to it, so there is no
        * single hostname to aggregate it under. For @c MATCH_HOST there is 
exactly one group per
        * hostname, so an aggregate would be over a set of one, and @c 
Group::metric_name already
        * returns the FQDN alone for that match type - identical to what this 
would return, so
        * publishing both would collide on one name.
        *
        * Note that reasoning holds within a single match type. @c 
TxnConfig::server_match is
        * overridable, so one hostname can be @c MATCH_HOST on one mapping and 
@c MATCH_BOTH on
        * another, and then that group's own published name and this aggregate 
name are the same
        * string and are merged into one derived metric.
   ```
   
   Documenting it is enough for this PR. Including the match type in the 
aggregate name, or warning on the collision, is the actual fix and can go with 
the follow-up.



##########
include/iocore/net/ConnectionTracker.h:
##########
@@ -155,8 +187,11 @@ class ConnectionTracker
      * @param key A populated @c Key structure - values are copied to the @c 
Group.
      * @param fqdn The full FQDN.
      * @param min_keep_alive The minimum number of origin keep alive 
connections to maintain.
+     * @param metric_enabled Whether the transaction creating this group wants 
per server metrics.
+     * @param metric_aggregate What that transaction wants published, see @c 
MetricAggregate.
      */
-    Group(DirectionType direction, Key const &key, std::string_view fqdn, int 
min_keep_alive);
+    Group(DirectionType direction, Key const &key, std::string_view fqdn, int 
min_keep_alive, int metric_enabled = 0,
+          MetricAggregate metric_aggregate = AGGREGATE_NONE);

Review Comment:
   Suggestion only. I would drop both default arguments. There are two 
construction sites, and only the INBOUND one 21 lines away in `obtain_inbound` 
uses the defaults, so the defaults buy one short call and cost a silent failure 
mode: a future outbound site that forgets these arguments compiles cleanly and 
gets no metrics.
   
   The sharper version is that `min_keep_alive` and `metric_enabled` are 
adjacent, both `int`, so transposing them also compiles and produces a group 
with metrics keyed off a keep-alive count. Taking `bool metric_enabled` would 
remove that particular hazard, and it matches how the value is actually used 
here, which is a plain truth test.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to