Skip to content

feat(stream): per-listen metrics zone and $stream_session_reason - #123

Open
AlinsRan wants to merge 6 commits into
mainfrom
feat/stream-metrics
Open

feat(stream): per-listen metrics zone and $stream_session_reason#123
AlinsRan wants to merge 6 commits into
mainfrom
feat/stream-metrics

Conversation

@AlinsRan

@AlinsRan AlinsRan commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Implements the runtime half of rfcs#245 (TCP stream proxy observability).

Why

A TCP proxy cannot be monitored from Lua alone:

  • nginx keeps the stream $status at 200 for every failure that happens after the upstream connection is established. Idle timeouts, upstream resets and client resets are all indistinguishable from a clean close (ngx_stream_proxy_module.c, all post-connect paths end in ngx_stream_proxy_finalize(s, NGX_STREAM_OK)).
  • The byte counters of a live session are unreachable. $bytes_received and friends only exist at log time, so a long-lived connection reports nothing until it ends.

What

A new ngx_stream_apisix_metrics_module:

  • apisix_stream_metrics_zone <size> reserves a shared memory zone holding, per stream listening address, the active session count and bytes moved in the four directions (downstream/upstream × ingress/egress). A session accumulates locally and merges into the zone at most once per second, plus a final flush, so long-lived connections keep the counters moving without an atomic operation per read or write. Slots are claimed in init_module from cycle->listening, so the hot path is an index lookup and an atomic add — no locking.
  • $stream_session_reason reports why a session ended: closed, client_rst, client_error, upstream_rst, upstream_error, connect_timeout, recv_timeout, send_timeout, upstream_timeout, shutdown.
  • $stream_listen_addr is the configured listening address, which is what the zone keys its slots by. $server_addr is the address the connection was accepted on and differs on a wildcard listen.
  • resty.apisix.stream.metrics exposes the zone to Lua over FFI.

Both are inert unless configured: without the zone directive nothing is collected, and $stream_session_reason still works on its own.

About the nginx patch

The patch stays deliberately small — it only records what nginx destroys on its own:

  • ngx_stream_proxy_process_connection clears ev->timedout before finalizing, so a proxy_timeout is unrecoverable afterwards.
  • ngx_stream_proxy_finalize frees u->peer.connection before the log phase runs, so both the upstream-side reason and the last byte delta towards the upstream have to be taken at its entry.

Everything else is derived from the connection flags: a reset leaves read->error set even though the proxy module rewrites it into an EOF, and a failed write leaves error set on the destination connection.

The patch adds no include hunk — nginx-tcp_over_tls.patch owns the include block of that file and pulls in ngx_stream_apisix_module.h, which now brings in the metrics header. Touching those lines from a second patch would break its context.

Testing

  • Compiles with -Werror against nginx 1.21.4, 1.25.3, 1.27.1 and 1.29.2; patches added for all four patch/ directories.
  • t/stream/metrics.t: 16 subtests covering normal close, receive timeout, native 502, the four byte directions and the active gauge over FFI, plus the no-zone-configured paths.
  • Behaviour additionally verified against a plain patched nginx: closed, recv_timeout, client_rst, upstream_rst and the native 502 are each distinguished correctly.

Follow-ups

Needs a 1.19.9 tag; api7/apisix-build-tools then bumps apisix_nginx_module_ver on both runtime lines, and the gateway consumes it through the released runtime.

Summary by CodeRabbit

  • New Features

    • Added stream metrics by listening address, including active connections and inbound/outbound byte counts.
    • Added TCP and UDP traffic accounting with live-session updates.
    • Added Lua and stream-variable access to session termination reasons and listening addresses.
    • Added tracking for timeouts, resets, errors, closes, and connection failures.
    • Added shared stream metrics zone configuration with validation and clear errors for invalid or missing zones.
    • Excluded internal Unix-socket traffic from reported metrics.
  • Documentation

    • Documented configuration, shared metrics behavior, limitations, counters, and Lua access.

nginx keeps the stream $status at 200 for every failure that happens after
the upstream connection is established, and it exposes no way to read the
byte counters of a live session, so a TCP proxy cannot report connection
outcomes or real-time throughput.

Add ngx_stream_apisix_metrics_module:

- apisix_stream_metrics_zone reserves a shared memory zone holding, per
  stream listening address, the active session count and the bytes moved in
  the four directions. Each session accumulates locally and merges into the
  zone at most once per second, plus a final flush, so long-lived connections
  keep the counters moving without an atomic operation per read or write.
- $stream_session_reason reports why a session ended: normal close, client
  or upstream reset, connect/receive/send timeout, worker shutdown.
- resty.apisix.stream.metrics exposes the zone to Lua over FFI.

The ngx_stream_proxy_module patch only records what nginx destroys on its
own: it clears ev->timedout before finalizing, and it frees
u->peer.connection before the log phase runs, which is also why the last
byte delta towards the upstream has to be taken in the finalize hook.
Everything else, including reset detection, is derived from the connection
flags.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e13c5ad6-e57d-4574-b939-dcbea3ebac17

📥 Commits

Reviewing files that changed from the base of the PR and between 18a270a and e823861.

📒 Files selected for processing (4)
  • README.md
  • src/stream/ngx_stream_apisix_metrics_module.c
  • src/stream/ngx_stream_apisix_metrics_module.h
  • t/stream/metrics.t
📝 Walkthrough

Walkthrough

The PR adds shared-memory stream metrics, Lua metrics access, session termination variables, listening-address reporting, proxy lifecycle hooks for supported Nginx versions, documentation, and integration tests.

Changes

Stream metrics and session reasons

Layer / File(s) Summary
Metrics module foundation
src/stream/config, src/stream/ngx_stream_apisix_metrics_module.h, src/stream/ngx_stream_apisix_metrics_module.c, src/stream/ngx_stream_apisix_module.h
The build registers the metrics module. The module validates and allocates the shared zone, maps listening addresses, defines public metrics and reason types, and installs stream lifecycle handlers.
Metrics collection and Lua dump
src/stream/ngx_stream_apisix_metrics_module.c, lib/resty/apisix/stream/metrics.lua, t/stream/metrics.t
The module tracks active sessions and directional byte counters. Lua can dump metrics entries or return a missing-zone error. Tests cover TCP, UDP, active-session, byte-counter, and Unix-socket behavior.
Session reason lifecycle
src/stream/ngx_stream_apisix_metrics_module.c, patch/*/nginx-stream_metrics.patch, README.md, t/stream/metrics.t
The module records and derives session reasons and exposes $stream_session_reason and $stream_listen_addr. Supported proxy patches invoke timeout, update, peer-closing, and finalization hooks. Documentation describes the directive, variables, counters, and limitations. Tests cover closed sessions, receive timeouts, and native 502 preservation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StreamProxy
  participant MetricsModule
  participant SharedMetricsZone
  participant LuaMetrics
  StreamProxy->>MetricsModule: update session counters
  MetricsModule->>SharedMetricsZone: flush address metrics
  StreamProxy->>MetricsModule: finalize session
  MetricsModule->>SharedMetricsZone: store final counters
  LuaMetrics->>MetricsModule: dump metrics entries
  MetricsModule-->>LuaMetrics: return address and counter totals
Loading
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning FAIL: t/stream/metrics.t has 9 E2E cases, omits most documented termination and configuration-boundary scenarios, and ignores socket and metrics.dump return values. Check every socket and dump result. Add E2E cases for client/upstream resets, send/upstream/connect timeouts, invalid zones, reload/retry behavior, and exact per-listen counters.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes: a per-listen stream metrics zone and the $stream_session_reason variable.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed New code returns only listen addresses, counters, and termination reasons; review found no secret logging/storage, mutating endpoints, TLS flags, ownership checks, or secret-reference handling.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stream-metrics

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
t/stream/metrics.t (1)

65-75: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the reason for the connect timeout.

TEST 3 checks the 502 status but not $stream_session_reason. connect_timeout is the one reason produced by the ctx->connect_timeout plus NGX_STREAM_BAD_GATEWAY path in ngx_stream_apisix_derive_reason, and no test covers it. Add the reason to the log line and to the expected output.

💚 Proposed coverage addition
     log_by_lua_block {
-        ngx.log(ngx.WARN, "status: ", ngx.var.status)
+        ngx.log(ngx.WARN, "status: ", ngx.var.status,
+                ", reason: ", ngx.var.stream_session_reason)
     }
 --- stream_request eval
 "GET /"
 --- error_log
-status: 502
+status: 502, reason: connect_timeout
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@t/stream/metrics.t` around lines 65 - 75, Update TEST 3’s log_by_lua_block to
log both ngx.var.status and ngx.var.stream_session_reason, then extend the
expected error_log output to assert the 502 status includes the connect_timeout
reason. Keep the existing unreachable-upstream setup and native 502 assertion
unchanged.
src/stream/ngx_stream_apisix_metrics_module.c (3)

294-299: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Publish the new slot with a write barrier.

On reload the master appends slots while the old workers still read the same zone. A reader can observe the incremented sh->nused before it observes the addr bytes of that slot. Add ngx_memory_barrier() before the increment so the slot contents are visible first.

♻️ Proposed ordering fix
     slot->addr_len = (uint32_t) addr->len;
     ngx_memcpy(slot->addr, addr->data, addr->len);
 
+    ngx_memory_barrier();
+
     sh->nused++;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stream/ngx_stream_apisix_metrics_module.c` around lines 294 - 299, In the
slot append logic, add ngx_memory_barrier() after copying the new slot’s address
and immediately before incrementing sh->nused, ensuring readers see fully
initialized slot contents before the published count increases.

330-332: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Log the skipped listening addresses.

An address longer than NGX_STREAM_APISIX_METRICS_ADDR_LEN produces no metrics and no diagnostic. A unix socket path can exceed 128 bytes. Emit a warning, as the adjacent too-small-zone branch does.

♻️ Proposed diagnostic
         if (addr.len == 0 || addr.len > NGX_STREAM_APISIX_METRICS_ADDR_LEN) {
+            ngx_log_error(NGX_LOG_WARN, cycle->log, 0,
+                          "apisix stream metrics skips listening address "
+                          "\"%V\": length %uz exceeds %d",
+                          &addr, addr.len,
+                          NGX_STREAM_APISIX_METRICS_ADDR_LEN);
             continue;
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stream/ngx_stream_apisix_metrics_module.c` around lines 330 - 332, Update
the addr validation branch in the stream metrics address-processing logic to
emit a warning before continuing when addr.len exceeds
NGX_STREAM_APISIX_METRICS_ADDR_LEN, matching the diagnostic behavior of the
adjacent too-small-zone branch; preserve the existing handling for zero-length
addresses and the continue behavior.

691-709: 🩺 Stability & Availability | 🔵 Trivial

The decrement path is correct. One operational note.

ctx->counted guarantees exactly one decrement per session, and it implies ctx->slot is non-NULL.

active lives in shared memory that survives reloads. If a worker is killed while sessions are live, the log phase never runs for those sessions and the gauge stays inflated for the lifetime of the zone. Consider documenting this, or tracking active counts per worker so a dead worker's contribution can be dropped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stream/ngx_stream_apisix_metrics_module.c` around lines 691 - 709,
Document the shared-memory lifecycle limitation near
ngx_stream_apisix_metrics_log_handler: sessions belonging to a worker killed
before log phase execution do not decrement slot->active, so the gauge may
remain inflated for the lifetime of the zone. Do not alter the existing
ctx->counted decrement path.
lib/resty/apisix/stream/metrics.lua (1)

26-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the slot cap into the public header.

Line 26 says this value must stay in sync with ngx_stream_apisix_metrics_module.h, but NGX_STREAM_APISIX_METRICS_MAX_SLOTS is defined in ngx_stream_apisix_metrics_module.c. If the C cap grows later, dump() truncates silently. Move the macro next to NGX_STREAM_APISIX_METRICS_ADDR_LEN and NGX_STREAM_APISIX_METRICS_DIRECTIONS in the header, so the three constants the Lua side mirrors live in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/resty/apisix/stream/metrics.lua` around lines 26 - 33, Move
NGX_STREAM_APISIX_METRICS_MAX_SLOTS from ngx_stream_apisix_metrics_module.c into
the public ngx_stream_apisix_metrics_module.h beside
NGX_STREAM_APISIX_METRICS_ADDR_LEN and NGX_STREAM_APISIX_METRICS_DIRECTIONS,
removing the duplicate C definition. Update the Lua MAX_ENTRIES mirror to use
the header’s synchronized value so dump() reflects the shared slot cap.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/stream/ngx_stream_apisix_metrics_module.c`:
- Around line 501-516: Validate the reason argument at the start of
ngx_stream_apisix_set_session_reason, rejecting values outside the valid
NGX_STREAM_APISIX_REASON enum range before storing them in ctx->reason. Preserve
the existing null-context handling and first-terminating-event-wins behavior for
valid reasons.

---

Nitpick comments:
In `@lib/resty/apisix/stream/metrics.lua`:
- Around line 26-33: Move NGX_STREAM_APISIX_METRICS_MAX_SLOTS from
ngx_stream_apisix_metrics_module.c into the public
ngx_stream_apisix_metrics_module.h beside NGX_STREAM_APISIX_METRICS_ADDR_LEN and
NGX_STREAM_APISIX_METRICS_DIRECTIONS, removing the duplicate C definition.
Update the Lua MAX_ENTRIES mirror to use the header’s synchronized value so
dump() reflects the shared slot cap.

In `@src/stream/ngx_stream_apisix_metrics_module.c`:
- Around line 294-299: In the slot append logic, add ngx_memory_barrier() after
copying the new slot’s address and immediately before incrementing sh->nused,
ensuring readers see fully initialized slot contents before the published count
increases.
- Around line 330-332: Update the addr validation branch in the stream metrics
address-processing logic to emit a warning before continuing when addr.len
exceeds NGX_STREAM_APISIX_METRICS_ADDR_LEN, matching the diagnostic behavior of
the adjacent too-small-zone branch; preserve the existing handling for
zero-length addresses and the continue behavior.
- Around line 691-709: Document the shared-memory lifecycle limitation near
ngx_stream_apisix_metrics_log_handler: sessions belonging to a worker killed
before log phase execution do not decrement slot->active, so the gauge may
remain inflated for the lifetime of the zone. Do not alter the existing
ctx->counted decrement path.

In `@t/stream/metrics.t`:
- Around line 65-75: Update TEST 3’s log_by_lua_block to log both ngx.var.status
and ngx.var.stream_session_reason, then extend the expected error_log output to
assert the 502 status includes the connect_timeout reason. Keep the existing
unreachable-upstream setup and native 502 assertion unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de85b043-48d5-45d9-a175-6ec1a9f050eb

📥 Commits

Reviewing files that changed from the base of the PR and between e878476 and 1a6041c.

📒 Files selected for processing (11)
  • README.md
  • lib/resty/apisix/stream/metrics.lua
  • patch/1.21.4/nginx-stream_metrics.patch
  • patch/1.25.3.1/nginx-stream_metrics.patch
  • patch/1.27.1.1/nginx-stream_metrics.patch
  • patch/1.29.2.4/nginx-stream_metrics.patch
  • src/stream/config
  • src/stream/ngx_stream_apisix_metrics_module.c
  • src/stream/ngx_stream_apisix_metrics_module.h
  • src/stream/ngx_stream_apisix_module.h
  • t/stream/metrics.t

Comment thread src/stream/ngx_stream_apisix_metrics_module.c
…t correctly

Review follow-ups on the stream metrics module:

- The zone pointer was cached when the zone was created and never cleared. A
  reload that removed the directive (or the whole stream block) left it
  pointing at shared memory ngx_init_cycle had already unmapped, so the next
  worker segfaulted on startup. Resolve it from the module main conf of the
  current cycle instead, which is NULL in exactly those cases.
- proxy_next_upstream swaps the peer connection, so pc->sent restarts from
  zero while the flushed mark still held the previous peer's total. The
  decrease was read as 'nothing to do' and every retried byte was lost. Treat
  a decrease as a counter restart.
- The interval clock was stamped even when nothing was written. The proxy
  module calls in once before any data has moved, so the first bytes of a
  session stayed invisible for a whole second. Only an actual write starts
  the interval.
- A UDP session has no FIN to observe, so one that simply ran to completion
  reported no reason at all. Fall back to a normal close when the session
  ended on NGX_STREAM_OK, and record the reason on the UDP shutdown path too.
- Unix sockets inside stream{} are internal plumbing, not proxy ports.
  Counting them reported gateway control traffic as proxied bytes and left a
  permanent floor of worker connections in the active gauge.
- A listening address too long for a slot was dropped silently; warn instead.

resty.apisix.stream.metrics no longer restricts itself to the stream
subsystem: the zone is process global and the reader touches no session, so
http can read it too.

Tests now cover what the requirements actually asked for: counters moving
while a session is still open (with exact byte counts), UDP accounting and
its reason, and the exclusion of internal unix sockets.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

80-90: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Clarify the closed mapping for UDP sessions.

A successful UDP session maps to closed, even though UDP has no FIN. State that closed means normal completion for TCP and UDP. Retain upstream_timeout for a UDP upstream that never answers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 80 - 90, Update the README status-table description
for closed to state that it represents normal completion for both TCP and UDP
sessions, without implying UDP uses FIN. Keep the upstream_timeout entry
unchanged for UDP upstreams that never answer.
🧹 Nitpick comments (1)
t/stream/metrics.t (1)

79-103: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use asymmetric payload sizes for directional-byte assertions.

Test 4 only verifies nonzero values. Test 8 uses four bytes in both directions. A request/response counter mapping error can pass both tests.

  • t/stream/metrics.t#L79-L103: Use a deterministic TCP upstream with different request and response payload sizes. Assert exact values for the proxied listener.
  • t/stream/metrics.t#L184-L231: Send a payload with a length different from "pong". Assert the four expected values, such as di=7 de=4 ue=7 ui=4.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@t/stream/metrics.t` around lines 79 - 103, The directional-byte tests need
asymmetric payloads and exact assertions to detect counter mapping errors. In
t/stream/metrics.t lines 79-103, update TEST 4 to use a deterministic TCP
upstream with different request and response payload sizes, then assert exact
byte values for the proxied listener instead of only checking nonzero flags; in
lines 184-231, change the sent payload to a length different from “pong” and
assert all four expected directional values, such as di=7, de=4, ue=7, and ui=4.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 38-40: Update the README text describing Unix sockets inside
stream{} to explicitly state that the AF_UNIX exclusion also omits
user-configured Unix proxy listeners, or revise the documented filter behavior
to exclude only APISIX’s internal worker event channel while retaining
user-configured Unix listeners.
- Around line 53-56: Update the proxy_next_upstream documentation to state that
the module’s counters aggregate bytes across all attempts, while
$upstream_bytes_sent and $upstream_bytes_received are per-attempt values;
instruct users to sum the corresponding per-attempt values before comparing, and
remove the claim that the aggregate is more accurate.

---

Outside diff comments:
In `@README.md`:
- Around line 80-90: Update the README status-table description for closed to
state that it represents normal completion for both TCP and UDP sessions,
without implying UDP uses FIN. Keep the upstream_timeout entry unchanged for UDP
upstreams that never answer.

---

Nitpick comments:
In `@t/stream/metrics.t`:
- Around line 79-103: The directional-byte tests need asymmetric payloads and
exact assertions to detect counter mapping errors. In t/stream/metrics.t lines
79-103, update TEST 4 to use a deterministic TCP upstream with different request
and response payload sizes, then assert exact byte values for the proxied
listener instead of only checking nonzero flags; in lines 184-231, change the
sent payload to a length different from “pong” and assert all four expected
directional values, such as di=7, de=4, ue=7, and ui=4.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1db6d97c-a34b-4443-88a0-09ff955ae8b0

📥 Commits

Reviewing files that changed from the base of the PR and between 1a6041c and 2b67d12.

📒 Files selected for processing (8)
  • README.md
  • lib/resty/apisix/stream/metrics.lua
  • patch/1.21.4/nginx-stream_metrics.patch
  • patch/1.25.3.1/nginx-stream_metrics.patch
  • patch/1.27.1.1/nginx-stream_metrics.patch
  • patch/1.29.2.4/nginx-stream_metrics.patch
  • src/stream/ngx_stream_apisix_metrics_module.c
  • t/stream/metrics.t
🚧 Files skipped from review as they are similar to previous changes (6)
  • patch/1.27.1.1/nginx-stream_metrics.patch
  • patch/1.25.3.1/nginx-stream_metrics.patch
  • lib/resty/apisix/stream/metrics.lua
  • patch/1.29.2.4/nginx-stream_metrics.patch
  • patch/1.21.4/nginx-stream_metrics.patch
  • src/stream/ngx_stream_apisix_metrics_module.c

Comment thread README.md Outdated
Comment thread README.md Outdated
…t the README

Second review round:

- proxy_next_upstream drops the peer and pc->sent with it. Treating a decrease
  as a restart only recovered the bytes if the new peer's counter had not yet
  overtaken the old mark; otherwise the failed attempt was still lost. Flush
  and reset the upstream mark before the peer goes away instead, which makes
  the total exact rather than nearly exact. The loss was small in practice --
  every next_upstream call site runs before u->connected, so only PROXY
  protocol and TLS handshake bytes were at stake -- but the requirement is
  exactness.
- Slot publication had no write barrier. Claiming is master-only, but a reload
  reuses the zone while the previous generation of workers is still reading,
  so on a weakly ordered architecture a reader could see the incremented count
  before the address it exposes. The old comment claimed fork made this safe,
  which is only true for a cold start.
- Guard the AF_UNIX check with NGX_HAVE_UNIX_DOMAIN, as nginx does everywhere
  else, and clamp the address length the FFI reader copies.
- resty.apisix.stream.metrics threw on a build without the stream addon rather
  than returning the documented nil, err.

The README promised totals that were 'exact' and upstream counters 'more
accurate' than nginx's own variables; neither held on the retry path. It also
omitted that removing the directive releases the zone, and that slots are
never reclaimed.

TEST 9 asserted only that no unix slot exists, which a long enough servroot
path would satisfy by tripping the address length check instead. It now
asserts the exact slot set.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds TCP/UDP stream proxy observability to the APISIX nginx-module runtime by introducing a per-listening-address shared metrics zone (active sessions + byte counters) and a new $stream_session_reason stream variable, with Lua FFI access for live scraping.

Changes:

  • Add ngx_stream_apisix_metrics_module implementing apisix_stream_metrics_zone, $stream_session_reason, and $stream_listen_addr.
  • Patch multiple supported nginx stream proxy module versions to record termination reasons and to update/flush byte counters during a live session.
  • Add Lua FFI reader (resty.apisix.stream.metrics) plus Test::Nginx coverage and README documentation for the new directive/variables.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
t/stream/metrics.t Adds stream-level tests for reason classification and per-listen live counters (TCP + UDP) via FFI.
src/stream/ngx_stream_apisix_module.h Makes the metrics API reachable from the proxy-module patch include entrypoint.
src/stream/ngx_stream_apisix_metrics_module.h Declares reason enum, FFI-visible entry layout, and proxy-module hooks.
src/stream/ngx_stream_apisix_metrics_module.c Implements the metrics zone, slot mapping, flush logic, stream variables, and FFI dump.
src/stream/config Registers the new stream module as a separate addon module in the build.
README.md Documents apisix_stream_metrics_zone, $stream_session_reason, and $stream_listen_addr, including behavior notes/limitations.
patch/1.21.4/nginx-stream_metrics.patch Hooks stream proxy module to set reasons and flush/update counters.
patch/1.25.3.1/nginx-stream_metrics.patch Same as above for nginx 1.25.3.1 patchset.
patch/1.27.1.1/nginx-stream_metrics.patch Same as above for nginx 1.27.1.1 patchset.
patch/1.29.2.4/nginx-stream_metrics.patch Same as above for nginx 1.29.2.4 patchset.
lib/resty/apisix/stream/metrics.lua Adds Lua FFI reader for dumping per-listen stream metrics from shared memory.
Suppressed comments (1)

src/stream/ngx_stream_apisix_metrics_module.c:833

  • ngx_stream_apisix_metrics_dump() reads sh->nused and then immediately reads sh->slots[]. Since the master may append slots during reload (with only a writer-side barrier), add a reader-side ngx_memory_barrier() after reading nused to ensure slot contents are visible before copying them.
    n = ngx_min(sh->nused, max);

    for (i = 0; i < n; i++) {
        slot = &sh->slots[i];

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/stream/ngx_stream_apisix_metrics_module.c
…e README

PR review comments:

- ngx_stream_apisix_set_session_reason indexes the reason table with a value
  that arrives from the ngx_stream_proxy_module patches, which are separate
  files free to drift from this enum. Refuse an out-of-range value instead of
  reading past the table.
- The unix socket note read as though only internal sockets are skipped. The
  filter is on the address family, so a unix listener configured for proxying
  is skipped too; say so.
- $upstream_bytes_sent and $upstream_bytes_received report one value per
  attempt rather than a total, so the retry note now says they have to be
  summed before they can be compared with these counters.
A store barrier alone only orders the writer. A reader that loads nused and
then the slot it exposes still lets a weakly ordered CPU hoist the slot load
above the count load, which is the very reordering the writer barrier exists
to prevent. Snapshot nused, barrier, then walk the slots, in both the lookup
and the FFI dump.
Final review round.

The once-a-second throttle did not bound staleness, it removed it. A skipped
delta has no later trigger: nothing calls back into the forwarding path until
more data arrives or the session ends. For request/response traffic on a long
lived connection -- the common L4 shape -- a whole response could stay
invisible for as long as the connection then stayed idle, which is exactly
what keeping the counters in nginx was supposed to avoid, and the README
claim of 'up to a second' was simply wrong.

The hook already sits where nginx has drained everything readable, so the
session-local accumulation still collapses the inner read/write loop into one
atomic per direction. The throttle was buying almost nothing for that.

Giving up before any peer answers -- connection refused, no live upstream, a
failed or timed out upstream handshake -- left no mark on the connection
flags and so reported no reason at all. It now reports connect_failed, and
the README no longer describes '-' as only meaning a pre-proxy rejection.

TEST 7 could not have caught the throttle bug: the flush mark starts at zero,
so the first burst always published. It now sends a second burst on the still
open session, which is precisely what a deferred flush would hide. TEST 3
gained the reason assertion for the path it was already walking.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants