From d3ee845e2633d61fae525b40dea0c5b9699f189c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:41:32 +0000 Subject: [PATCH 1/2] Initial plan From f95f01de0ef771dda7b73158cbbdd404511b62ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:45:15 +0000 Subject: [PATCH 2/2] feat: filter benign operational logs from Squid access.log - Add ACL and log_access directive to filter localhost healthcheck probes - Update log aggregator to skip transaction-end-before-headers entries - Add comprehensive tests for both changes Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- src/logs/log-aggregator.test.ts | 104 ++++++++++++++++++++++++++++++++ src/logs/log-aggregator.ts | 13 +++- src/squid-config.test.ts | 26 ++++++++ src/squid-config.ts | 4 ++ 4 files changed, 145 insertions(+), 2 deletions(-) diff --git a/src/logs/log-aggregator.test.ts b/src/logs/log-aggregator.test.ts index 13c63397..887e4833 100644 --- a/src/logs/log-aggregator.test.ts +++ b/src/logs/log-aggregator.test.ts @@ -105,6 +105,110 @@ describe('log-aggregator', () => { expect(stats.byDomain.has('-')).toBe(true); expect(stats.byDomain.has('github.com')).toBe(true); }); + + it('should filter out transaction-end-before-headers entries', () => { + const entries: ParsedLogEntry[] = [ + createLogEntry({ + domain: 'github.com', + url: 'github.com:443', + isAllowed: true + }), + createLogEntry({ + domain: '-', + url: 'error:transaction-end-before-headers', + decision: 'NONE_NONE:HIER_NONE', + statusCode: 0, + isAllowed: false + }), + createLogEntry({ + domain: 'npmjs.org', + url: 'npmjs.org:443', + isAllowed: true + }), + ]; + + const stats = aggregateLogs(entries); + + // Should only count the two valid entries + expect(stats.totalRequests).toBe(2); // Only actual requests, not benign operational entries + expect(stats.allowedRequests).toBe(2); + expect(stats.deniedRequests).toBe(0); + expect(stats.uniqueDomains).toBe(2); + expect(stats.byDomain.has('github.com')).toBe(true); + expect(stats.byDomain.has('npmjs.org')).toBe(true); + expect(stats.byDomain.has('-')).toBe(false); // Filtered entry not in domain stats + }); + + it('should handle multiple transaction-end-before-headers entries', () => { + const entries: ParsedLogEntry[] = [ + createLogEntry({ + domain: 'github.com', + url: 'github.com:443', + isAllowed: true + }), + createLogEntry({ + domain: '-', + url: 'error:transaction-end-before-headers', + clientIp: '::1', // healthcheck from localhost + decision: 'NONE_NONE:HIER_NONE', + statusCode: 0, + isAllowed: false + }), + createLogEntry({ + domain: '-', + url: 'error:transaction-end-before-headers', + clientIp: '172.30.0.20', // shutdown-time connection closure + decision: 'NONE_NONE:HIER_NONE', + statusCode: 0, + isAllowed: false + }), + createLogEntry({ + domain: 'npmjs.org', + url: 'npmjs.org:443', + isAllowed: true + }), + ]; + + const stats = aggregateLogs(entries); + + expect(stats.totalRequests).toBe(2); // Only actual requests + expect(stats.allowedRequests).toBe(2); + expect(stats.deniedRequests).toBe(0); + expect(stats.uniqueDomains).toBe(2); + }); + + it('should still count time range from all entries including filtered ones', () => { + const entries: ParsedLogEntry[] = [ + createLogEntry({ + timestamp: 1000.0, + domain: 'github.com', + url: 'github.com:443', + isAllowed: true + }), + createLogEntry({ + timestamp: 1500.0, + domain: '-', + url: 'error:transaction-end-before-headers', + decision: 'NONE_NONE:HIER_NONE', + statusCode: 0, + isAllowed: false + }), + createLogEntry({ + timestamp: 2000.0, + domain: 'npmjs.org', + url: 'npmjs.org:443', + isAllowed: true + }), + ]; + + const stats = aggregateLogs(entries); + + // Time range should span all entries, even filtered ones + expect(stats.timeRange).toEqual({ + start: 1000.0, + end: 2000.0, + }); + }); }); describe('loadAllLogs', () => { diff --git a/src/logs/log-aggregator.ts b/src/logs/log-aggregator.ts index ad578d31..69b721d9 100644 --- a/src/logs/log-aggregator.ts +++ b/src/logs/log-aggregator.ts @@ -53,9 +53,10 @@ export function aggregateLogs(entries: ParsedLogEntry[]): AggregatedStats { let deniedRequests = 0; let minTimestamp = Infinity; let maxTimestamp = -Infinity; + let totalRequests = 0; for (const entry of entries) { - // Track time range + // Track time range for all entries if (entry.timestamp < minTimestamp) { minTimestamp = entry.timestamp; } @@ -63,6 +64,15 @@ export function aggregateLogs(entries: ParsedLogEntry[]): AggregatedStats { maxTimestamp = entry.timestamp; } + // Skip benign operational entries (connection closures without HTTP headers) + // These appear during healthchecks and shutdown-time keep-alive connection closures + if (entry.url === 'error:transaction-end-before-headers') { + continue; + } + + // Count this as a real request + totalRequests++; + // Count allowed/denied if (entry.isAllowed) { allowedRequests++; @@ -91,7 +101,6 @@ export function aggregateLogs(entries: ParsedLogEntry[]): AggregatedStats { } } - const totalRequests = entries.length; const uniqueDomains = byDomain.size; const timeRange = entries.length > 0 ? { start: minTimestamp, end: maxTimestamp } : null; diff --git a/src/squid-config.test.ts b/src/squid-config.test.ts index 0b329ebb..1d604c6e 100644 --- a/src/squid-config.test.ts +++ b/src/squid-config.test.ts @@ -476,6 +476,32 @@ describe('generateSquidConfig', () => { const result = generateSquidConfig(config); expect(result).toContain('access_log /var/log/squid/access.log firewall_detailed'); }); + + it('should filter localhost healthcheck probes from logs', () => { + const config: SquidConfig = { + domains: ['example.com'], + port: defaultPort, + }; + const result = generateSquidConfig(config); + expect(result).toContain('acl healthcheck_localhost src 127.0.0.1 ::1'); + expect(result).toContain('log_access deny healthcheck_localhost'); + }); + + it('should place healthcheck filter before access_log directive', () => { + const config: SquidConfig = { + domains: ['example.com'], + port: defaultPort, + }; + const result = generateSquidConfig(config); + // Verify the order: ACL definition, then log_access deny, then access_log + const aclIndex = result.indexOf('acl healthcheck_localhost'); + const logAccessIndex = result.indexOf('log_access deny healthcheck_localhost'); + const accessLogIndex = result.indexOf('access_log /var/log/squid/access.log'); + + expect(aclIndex).toBeGreaterThan(-1); + expect(logAccessIndex).toBeGreaterThan(aclIndex); + expect(accessLogIndex).toBeGreaterThan(logAccessIndex); + }); }); describe('Streaming/Long-lived Connection Support', () => { diff --git a/src/squid-config.ts b/src/squid-config.ts index a4c44ac8..8029bbad 100644 --- a/src/squid-config.ts +++ b/src/squid-config.ts @@ -511,6 +511,10 @@ pinger_enable off # Note: For CONNECT requests (HTTPS), the domain is in the URL field logformat firewall_detailed %ts.%03tu %>a:%>p %{Host}>h %Hs %Ss:%Sh %ru "%{User-Agent}>h" +# Don't log healthcheck probes from localhost +acl healthcheck_localhost src 127.0.0.1 ::1 +log_access deny healthcheck_localhost + # Access log and cache configuration access_log /var/log/squid/access.log firewall_detailed cache_log /var/log/squid/cache.log