Repository navigation
feat(relay): log reads, logins and connections on access target - #8376
Merged
Merged
Conversation
wpfleger96
force-pushed
the
duncan/relay-access-logging
branch
2 times, most recently
from
October 15, 2026 23:02
2287045 to
1fa935a
Compare
wpfleger96
marked this pull request as ready for review
October 17, 2026 17:21
wpfleger96
temporarily deployed
to
codex-review
October 17, 2026 17:21 — with
GitHub Actions
Inactive
🔐 Codex Security Review
Review SummaryOverall Risk: NONE
FindingsNo concrete security, correctness, or reliability findings were identified. Notes
Generated by Codex Security Review | |
wpfleger96
temporarily deployed
to
codex-review
October 17, 2026 19:24 — with
GitHub Actions
Inactive
wpfleger96
temporarily deployed
to
codex-review
October 17, 2026 20:04 — with
GitHub Actions
Inactive
wpfleger96
had a problem deploying
to
codex-review
October 17, 2026 20:28 — with
GitHub Actions
Error
wpfleger96
had a problem deploying
to
codex-review
October 17, 2026 20:31 — with
GitHub Actions
Error
wpfleger96
had a problem deploying
to
codex-review
October 17, 2026 20:32 — with
GitHub Actions
Error
wpfleger96
had a problem deploying
to
codex-review
October 17, 2026 20:33 — with
GitHub Actions
Error
wpfleger96
temporarily deployed
to
codex-review
October 18, 2026 16:06 — with
GitHub Actions
Inactive
wpfleger96
had a problem deploying
to
codex-review
October 18, 2026 17:01 — with
GitHub Actions
Error
wpfleger96
temporarily deployed
to
codex-review
October 18, 2026 17:35 — with
GitHub Actions
Inactive
wpfleger96
temporarily deployed
to
codex-review
October 18, 2026 18:08 — with
GitHub Actions
Inactive
wpfleger96
temporarily deployed
to
codex-review
October 18, 2026 18:18 — with
GitHub Actions
Inactive
wpfleger96
force-pushed
the
duncan/relay-access-logging
branch
from
October 18, 2026 19:43
1a3e31d to
1ecd8ae
Compare
wpfleger96
temporarily deployed
to
codex-review
October 27, 2026 00:39 — with
GitHub Actions
Inactive
wpfleger96
temporarily deployed
to
codex-review
October 27, 2026 01:35 — with
GitHub Actions
Inactive
wpfleger96
force-pushed
the
duncan/relay-access-logging
branch
from
October 28, 2026 15:15
f5643c2 to
ec8eaa6
Compare
wpfleger96
temporarily deployed
to
codex-review
October 28, 2026 15:15 — with
GitHub Actions
Inactive
wpfleger96
force-pushed
the
duncan/relay-access-logging
branch
from
November 2, 2026 17:44
ec8eaa6 to
9c14e51
Compare
wpfleger96
temporarily deployed
to
codex-review
November 2, 2026 17:44 — with
GitHub Actions
Inactive
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Successful bridge reads that return nothing are mostly polling, so they drop to DEBUG; WebSocket REQ lines stay at INFO because an empty subscription can still deliver live events. buzz_ws_reads_total counts REQ and COUNT frames at any log level to measure the production read rate. Filter summaries omit *_count when the list already shows every value, and HTTP lines take the method from the http.request span, now at ERROR level so it is present wherever an access line is. The HTTP detail becomes one flat struct, and overlapping router tests are merged. Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The http.request span is absent under an access-only filter and when a pending request is dropped, so lines relying on it lost the verb. Keep the span at INFO and the verb on the line; also test the WS read counter through the dispatcher. Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
The capture helpers built a generic flattened JSON layer, which could drift from the TraceContextJson formatter main.rs installs. Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
send_if_owner declines EOSE when a same-ID request replaced the read or the outbound queue rejects it. The four REQ completion sites now mark served only when EOSE was queued, so those reads stay aborted, and a recorded search error is kept. Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
…al changes An artifact read now records its summary as soon as it routes as an artifact read, before its database work awaits, so a dropped read keeps its filter summary. /query records an incremental response's change count, removals included, so an empty poll logs at DEBUG like other empty reads. Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
Add the new EOSE regression and the fourteen infra-free tests this change added or edited to the just test-unit selector, and mirror every access logging selector from just test-unit into the Cargo fallback. Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
List the sync event and buzz_ws_reads_total type label, say when a sync line is written and what its summary holds, and describe the causes of an aborted read beyond client disconnect. Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
Wait for a backend blocked by the held transaction's PID instead of any cluster-wide artifact lock waiter, matching the sync policy test. Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
Admin role checks, invite mint owner-link and role gates, banned invite claims, the audio early ban check and moderation lookup failures now record their real outcome on the access line instead of success, aborted or not_moderator. Duplicate admin Authorization headers log duplicate_authorization. No status, body or auth decision changes. Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
A claim refused for an active ban or timeout now logs denied/restricted on both the v2 and legacy v1 paths; v2 no longer labels a timeout as banned. The duplicate Authorization admin test now asserts the captured access line. Responses are unchanged. Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]>
kalvinnchau
approved these changes
Nov 3, 2026
kalvinnchau
left a comment
Contributor
There was a problem hiding this comment.
🤖 Approved. Nonblocking P3: invite claim owner mismatches still log success in api/invites/claim_auth.rs:71–73; record Denied(OwnerMismatch) in that error arm.
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Adds an access log for relay reads, logins, connections and HTTP requests on the dedicated
buzz_relay::accesstarget. It writes to stdout, notaudit_log, so reads never take the audit advisory lock. The log is shaped for volume: one line per HTTP request, one line per WebSocket read, and one line per WebSocket connection.Lines per operation
http), except the skip list belowhttp,route="<unmatched>") at status 400 and above, otherwise noneclose, carrying the login)REQ/COUNT/ NIP-AR syncreq/count/sync), plus thebuzz_ws_reads_totalcounterauth) per decided or cut-off attempt, capped after the decision as belowauth, at its final admission check)EVENTEvent ingested, not on the access target)HTTP requests
One middleware,
http_access_log, writes every HTTP line. It sits betweentrack_metricsandhttp_trace_layer, so rebuilt errors, body-limit refusals and localhost refusals all reach it. It writes the line when the response is ready, which is not proof the body was delivered. A request dropped before then logsrequest=abortedat WARN with nostatus.route(the matched route template, never the raw path),peer_addrwhen the listener has one,status,time_to_response_msandhttp_method(the HTTP verb). The verb stays on the line itself because thehttp.requestspan is absent under an access-only filter and when a pending request is dropped.AccessSlot: the login (method,pubkey,pubkey_verified,community), the decision, the filter summary, and the route's own fields. The decision isoutcome(success/denied/failed/challenged) with a fixedreason, orundecidedwhen the route recorded none. A refusal is final: a later admission doesn't replace it, so a replayed NIP-98 proof that verifies and is then refused logsfailed/nip98_replay.outcomedescribes the login. What the route did is instatusand the route's fields: a rejected/eventswrite logsoutcome=successwithstatus=400,accepted=falseand itsreject_reason./queryand/count: the filter summary andresult_count. An incremental/queryresponse ({changes:[...]}) counts its changes, removals included. Artifact reads record their summary before the query runs, so a read dropped mid-query keeps it./events:accepted,kind,event_id, and on a refused write the parsecategory/line/columnor a fixedreject_reason(such asinvalidorrestricted), never the ingest message.invite_id,expires_atandmax_uses. Invite claim:joined.feedback_idandattachment_sha256. WithBUZZ_ADMIN_AUTH=disabledthey logmethod=network_trustedwith no key, and Host or Origin refusals log their fixed reason./media/{sha256_ext}template./,/info,/.well-known/nostr.json,/health,/_liveness,/_readiness, and the SPA and static files the relay serves when enabled (web assets, invite landing pages, git web pages, the admin bundle) at any status. Successful WebSocket upgrades (101) log nohttpline. Refusals before the upgrade on/and the audio path log one, with the status actually sent.outcome=undecidedwith no key.X-Pubkeyrequests logmethod=x_pubkey,pubkey_verified=false./queryor/countthat returned nothing (result_count=0) logs at DEBUG: on staging, 93% of/querylines were empty polls. Denied, failed, aborted and error reads, reads with noresult_count, writes and logins keep their levels.challenged, with its 401) are INFO. Refusals, statuses of 400 and above, and aborted requests are WARN. In Enforce mode the line records the remapped status (for example 403). Denial bodies and headers are unchanged.WebSocket reads
REQ/COUNT/synclines carrypubkey,conn_id,sub_id, the filter summary, the outcome (served/denied/aborted/errorplus a fixed reason), andevents_queued(REQ and sync) orcount(a COUNT that counted). AREQisservedonly once its EOSE is queued to the client. A read logs WARN only when it ends inerror.served,deniedandabortedare INFO, sinceabortedmeans the read ended before completing: the client disconnected or sent CLOSE, a same-IDREQreplaced it, or the session was cancelled.syncline when its initial set completes, not when live delivery ends. Its summary is the registered live filter (kinds and channels, never artifact predicates), andevents_queuedcounts the initial set.REQlines stay at INFO even when empty at EOSE, because the subscription can still deliver live events later./queryand/count:kinds, channel ids, authors andpvalues each list the first 10 distinct values. The exactkind_count/channel_count/author_count/p_count(summed per filter) appears only when the list is*or shows fewer values than the filters held. A dimension that any filter leaves open logs*.since/until/limittake the widest bound and are absent when any filter leaves them open, so no field understates what the read covered.idsis a count only. NIP-50 search logssearch: true, never the text. Only well-formed channel ids and pubkeys are listed. An artifact read's summary uses only the standard dimensions of each raw filter, so artifact predicates and client-defined tags (such as#1) are never logged. The summary is skipped when the access target is off. Under=warn, every WARN line, HTTP or WebSocket, still carries it.auth_required, quota, concurrency, overload) or denied are logged. The rest are counted asreads_suppressedon the close line. A refusal that arrives after the close line has been written, from a read still running when the client left, logs its own line, so every refusal is either logged or counted, never both. Served reads are never capped.WebSocket read counter
buzz_ws_reads_total{type="req"|"count"|"sync"}counts every parsedREQ,COUNTand NIP-AR sync frame before admission, exported at zero from startup. It is a metric, so it counts at anyRUST_LOG, including production's currenterror.WebSocket logins
A NIP-42 login writes one
authline when it is decided. A successful login logs INFOoutcome=successat its commit point, decided under the connection's close-reason lock, so a disconnect racing the login (session expiry, admin disconnect, revocation, ban or roster removal) either wins, and the login logs no success, or loses and closes the connection right after the success line. A refused or failed login writes one WARNauthline withoutcome, a fixedreasonfrom the sharedAuthReasoncode set,pubkeyandpubkey_verified. Root WebSocketauthlines carrypeer_addr. AUTH frames after the login is decided are capped at 16 per connection, and the rest are counted asauth_suppressed. An attempt cut off by close or session expiry logsaborted. Audio logins log oneauthline at their final admission check.Connections
The close line is the connection's only line. It is owned by the upgraded socket, so it is written exactly once however the connection ends: a normal close, a refusal while checking the community is active (
refused=not_admitted), the connection limit (refused=connection_limit), or the handler being dropped. It carriesconn_id,community,peer_addr,duration_ms, the login,events_queued/bytes_queued(frames the writer accepted and flushed, not client delivery), and the two suppression counters.Error text
DbError::kind()(SQLSTATE or variant), including the command-executor event insert. Search errors, on both read paths and in the search-cancel task, log asSearchError::kind()/ the same SQLx classifier. Workflow database and webhook errors log as their codes, since webhook URLs can be secrets.Operator notes
.env.exampledocuments the target, itseventvalues (http,auth,req,count,sync,close), the skip list, the levels, and how to turn it down:buzz_relay::access=warnkeeps refusals, errors and aborted HTTP requests, and of WebSocket reads only those that end inerror, and=offdrops everything. It also lists the removed lines:NIP-42 auth successful/failed,WebSocket connection established/closed,event=connect,HTTP bridge request,Event ingested via pipeline,relay invite minted,relay member added via invite,admin feedback attachment read, andaddr(nowpeer_addr). JSON lines no longer repeat the current span asspan, sincespansalready ends with it. A line logged under an explicitparent:span loses that parent's fields. The default OTel export filter setsbuzz_relay::access=off, so access lines aren't exported a second time as span events.peer_addris the TCP peer.BUZZ_CLIENT_IP_HEADERoptionally names a request header (for examplecf-connecting-ipbehind Cloudflare orx-real-ipbehind nginx, matched case-insensitively). When set,http,auth(root and audio),closeand connection-limit lines also logclient_ip: the rightmost comma-separated entry of the header's last field, only if it parses as an IP address. It never replacespeer_addr, and it is only trustworthy when every request arrives through a proxy that overwrites the header.