@@ -12,6 +12,7 @@ import (
1212 "time"
1313
1414 "github.com/prometheus/client_golang/prometheus"
15+ "github.com/satyrius/gonx"
1516 "github.com/stretchr/testify/assert"
1617 "github.com/stretchr/testify/require"
1718 "github.com/vmkteam/embedlog"
@@ -238,6 +239,121 @@ func TestParsedLine_RawURIUnnormalized(t *testing.T) {
238239 wantURI : "/api" ,
239240 wantRawURI : "/api?token=ab%3Dcd%26ef" ,
240241 },
242+ {
243+ // Key=value log_format with separate $uri and $args fields.
244+ // Without joining, RawURI would be path-only and gatesrv-side
245+ // HasQueryParams stays false (the original bug).
246+ name : "text key=value with separate $uri and $args" ,
247+ setup : func (c * LogCollector ) {
248+ c .defaultParser = gonx .NewParser (`time="$time_iso8601" host="$host" uriPath="$uri" uriQuery="$args" httpStatus=$status bodyBytesSent=$body_bytes_sent` )
249+ c .AddObserver (& recordingObserver {})
250+ },
251+ feed : func (c * LogCollector ) {
252+ c .parseLine (`time="2026-05-12T12:09:45+03:00" host="en.example.com" uriPath="/movies/catalog/" uriQuery="page=11051" httpStatus=404 bodyBytesSent=11496` )
253+ },
254+ wantURI : "/movies/catalog/" ,
255+ wantRawURI : "/movies/catalog/?page=11051" ,
256+ },
257+ {
258+ // Empty $args (the common case — most requests have no query).
259+ // RawURI must NOT carry a trailing '?'.
260+ name : "text key=value with empty $args" ,
261+ setup : func (c * LogCollector ) {
262+ c .defaultParser = gonx .NewParser (`host="$host" uriPath="$uri" uriQuery="$args" httpStatus=$status bodyBytesSent=$body_bytes_sent` )
263+ c .AddObserver (& recordingObserver {})
264+ },
265+ feed : func (c * LogCollector ) {
266+ c .parseLine (`host="x.example" uriPath="/about/" uriQuery="" httpStatus=200 bodyBytesSent=100` )
267+ },
268+ wantURI : "/about/" ,
269+ wantRawURI : "/about/" ,
270+ },
271+ {
272+ // $query_string is an alias of $args; some operators prefer it.
273+ name : "text key=value with $query_string instead of $args" ,
274+ setup : func (c * LogCollector ) {
275+ c .defaultParser = gonx .NewParser (`host="$host" uriPath="$uri" qs="$query_string" httpStatus=$status bodyBytesSent=$body_bytes_sent` )
276+ c .AddObserver (& recordingObserver {})
277+ },
278+ feed : func (c * LogCollector ) {
279+ c .parseLine (`host="x.example" uriPath="/search" qs="q=hello" httpStatus=200 bodyBytesSent=100` )
280+ },
281+ wantURI : "/search" ,
282+ wantRawURI : "/search?q=hello" ,
283+ },
284+ {
285+ // $request_uri carries the original path+query, URL-encoded as
286+ // received. Formats that log it directly need no rejoining.
287+ name : "text key=value with $request_uri directly" ,
288+ setup : func (c * LogCollector ) {
289+ c .defaultParser = gonx .NewParser (`host="$host" reqUri="$request_uri" httpStatus=$status bodyBytesSent=$body_bytes_sent` )
290+ c .AddObserver (& recordingObserver {})
291+ },
292+ feed : func (c * LogCollector ) {
293+ c .parseLine (`host="x.example" reqUri="/news/12345/title?utm=x" httpStatus=200 bodyBytesSent=100` )
294+ },
295+ wantURI : "/news/:id/:rest" ,
296+ wantRawURI : "/news/12345/title?utm=x" ,
297+ },
298+ {
299+ // Hybrid format: "$request" + ru="$request_uri" + u="$uri".
300+ // $request_uri wins so we ship what the client actually requested,
301+ // not the post-rewrite $uri pointing at the FastCGI dispatcher.
302+ name : "hybrid combined + key=value, internal rewrite" ,
303+ setup : func (c * LogCollector ) {
304+ c .defaultParser = gonx .NewParser (`$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" rt="$request_time" "$http_user_agent" "$http_x_forwarded_for" h="$host" sn="$server_name" ru="$request_uri" u="$uri"` )
305+ c .AddObserver (& recordingObserver {})
306+ },
307+ feed : func (c * LogCollector ) {
308+ c .parseLine (`192.0.2.9 - - [12/May/2026:00:00:30 +0000] "GET /events.ics HTTP/2.0" 200 900 "-" rt="0.098" "iOS/17.1 (21B74) dataaccessd/1.0" "-" h="example.com" sn="example.com" ru="/events.ics" u="/dispatch.php"` )
309+ },
310+ wantURI : "/events.ics" ,
311+ wantRawURI : "/events.ics" ,
312+ },
313+ {
314+ // Same hybrid format with a long query containing commas — gonx
315+ // must not get confused by literal commas inside the quoted value.
316+ name : "hybrid format with comma-rich query string" ,
317+ setup : func (c * LogCollector ) {
318+ c .defaultParser = gonx .NewParser (`$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" rt="$request_time" "$http_user_agent" "$http_x_forwarded_for" h="$host" sn="$server_name" ru="$request_uri" u="$uri"` )
319+ c .AddObserver (& recordingObserver {})
320+ },
321+ feed : func (c * LogCollector ) {
322+ c .parseLine (`192.0.2.5 - - [12/May/2026:00:00:23 +0000] "GET /shared/minify.php?hash&files=/a.js,/b.js,/c.js HTTP/2.0" 200 80612 "https://example.com/" rt="0.004" "Mozilla/5.0" "-" h="example.com" sn="example.com" ru="/shared/minify.php?hash&files=/a.js,/b.js,/c.js" u="/shared/minify.php"` )
323+ },
324+ wantURI : "/shared/:file.php" ,
325+ wantRawURI : "/shared/minify.php?hash&files=/a.js,/b.js,/c.js" ,
326+ },
327+ {
328+ // Operator misconfiguration: $request_uri logged under "uri" field
329+ // name. RawURI keeps the query (that's the point); metric URI must
330+ // be query-free — otherwise utm/session params blow up cardinality.
331+ name : "metric URI stays query-free even if $uri field carries query" ,
332+ setup : func (c * LogCollector ) {
333+ c .defaultParser = gonx .NewParser (`host="$host" uriPath="$uri" httpStatus=$status bodyBytesSent=$body_bytes_sent` )
334+ c .AddObserver (& recordingObserver {})
335+ },
336+ feed : func (c * LogCollector ) {
337+ c .parseLine (`host="x.example" uriPath="/news/12345?utm_source=tg&utm_campaign=spring" httpStatus=200 bodyBytesSent=100` )
338+ },
339+ wantURI : "/news/:id" ,
340+ wantRawURI : "/news/12345?utm_source=tg&utm_campaign=spring" ,
341+ },
342+ {
343+ // JSON map path with a base64-like signed URL (file CDN style):
344+ // "/get/<token>==,<numeric-id>/<asset-path>". RawURI ships verbatim;
345+ // metric URI collapses the token+id and truncates deep paths.
346+ name : "JSON map path with base64 token in request_uri" ,
347+ setup : func (c * LogCollector ) {
348+ c .extractFields = []string {"http_user_agent" }
349+ c .AddObserver (& recordingObserver {uaIdx : 0 })
350+ },
351+ feed : func (c * LogCollector ) {
352+ c .ParseJSONLine (`{"status":"200","body_bytes_sent":"123456789","request_time":"42.000","request_uri":"/get/AbCdEf01234567_HiJkLmNo-PqRs==,1234567890/category/asset/files/example.zip","http_user_agent":"Mozilla/5.0"}` )
353+ },
354+ wantURI : "/get/:token/:rest" ,
355+ wantRawURI : "/get/AbCdEf01234567_HiJkLmNo-PqRs==,1234567890/category/asset/files/example.zip" ,
356+ },
241357 }
242358 for _ , tc := range cases {
243359 t .Run (tc .name , func (t * testing.T ) {
0 commit comments