feat(egress): add global log option for full request/response logging
lint / lint (push) Failing after 1m25s
test / unit (pull_request) Successful in 29s
test / integration (pull_request) Successful in 44s

Adds a top-level `log: true` option to the egress config that logs the
full request (method, path, headers, body) and response (status, headers,
body) for every forwarded connection as JSON lines on stderr.

Wire format: `log: true` at the root of routes.yaml, parsed into the new
`Config` dataclass alongside `routes`. The sidecar addon switches from
`self.routes` to `self.config` and writes `_log_request` / `_log_response`
JSON lines when `self.config.log` is set.

Manifest: `egress.log: true` in bottle YAML flows through `EgressConfig.Log`
→ `Egress.prepare()` → `egress_render_routes(..., log=)` → routes.yaml.
`EgressPlan` also carries the flag for introspection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 13:59:48 -04:00
parent e82bbb587f
commit e72247a1b5
7 changed files with 183 additions and 17 deletions
+38
View File
@@ -324,6 +324,44 @@ class TestRenderRoutes(unittest.TestCase):
self.assertEqual(("token_patterns",), addon_routes[0].outbound_detectors)
self.assertEqual((), addon_routes[0].inbound_detectors)
def test_log_false_omitted_from_render(self):
b = _bottle([{"host": "x.example"}])
routes = egress_routes_for_bottle(b)
rendered = egress_render_routes(routes, log=False)
self.assertNotIn("log:", rendered)
def test_log_true_emitted_at_top_level(self):
b = _bottle([{"host": "x.example"}])
routes = egress_routes_for_bottle(b)
rendered = egress_render_routes(routes, log=True)
self.assertTrue(rendered.startswith("log: true\n"))
def test_log_true_round_trips_to_addon_core(self):
from bot_bottle.egress_addon_core import load_config
b = _bottle([{"host": "x.example"}])
routes = egress_routes_for_bottle(b)
rendered = egress_render_routes(routes, log=True)
cfg = load_config(rendered)
self.assertTrue(cfg.log)
self.assertEqual("x.example", cfg.routes[0].host)
def test_log_via_manifest_flows_to_render(self):
from bot_bottle.manifest import Manifest
from bot_bottle.egress_addon_core import load_config
m = Manifest.from_json_obj({
"bottles": {"dev": {"egress": {
"log": True,
"routes": [{"host": "x.example"}],
}}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
})
bottle = m.bottles["dev"]
self.assertTrue(bottle.egress.Log)
routes = egress_routes_for_bottle(bottle)
rendered = egress_render_routes(routes, log=bottle.egress.Log)
cfg = load_config(rendered)
self.assertTrue(cfg.log)
class TestResolveTokenValues(unittest.TestCase):
def test_reads_host_env(self):
+31
View File
@@ -13,6 +13,7 @@ from pathlib import Path
from urllib.parse import urlsplit
from bot_bottle.egress_addon_core import (
Config,
Decision,
HeaderMatch,
MatchEntry,
@@ -21,8 +22,10 @@ from bot_bottle.egress_addon_core import (
decide,
evaluate_matches,
is_git_push_request,
load_config,
load_routes,
match_route,
parse_config,
parse_routes,
)
@@ -271,6 +274,34 @@ class TestLoadRoutes(unittest.TestCase):
load_routes("routes:\n\t- host: x\n")
# --- load_config / parse_config ------------------------------------------
class TestLoadConfig(unittest.TestCase):
def test_log_defaults_to_false(self):
cfg = load_config('routes:\n - host: "api.example"\n')
self.assertFalse(cfg.log)
self.assertEqual(1, len(cfg.routes))
def test_log_true_parsed(self):
cfg = load_config('log: true\nroutes:\n - host: "api.example"\n')
self.assertTrue(cfg.log)
self.assertEqual(1, len(cfg.routes))
def test_log_false_explicit(self):
cfg = load_config('log: false\nroutes:\n - host: "api.example"\n')
self.assertFalse(cfg.log)
def test_log_non_bool_rejected(self):
with self.assertRaises(ValueError):
load_config('log: "yes"\nroutes: []\n')
def test_routes_accessible_via_config(self):
cfg = load_config('routes:\n - host: "x.example"\n')
self.assertIsInstance(cfg, Config)
self.assertEqual("x.example", cfg.routes[0].host)
# --- evaluate_matches ---------------------------------------------------
+19
View File
@@ -346,6 +346,25 @@ class TestConfigShape(unittest.TestCase):
"bottle": "dev"}},
})
def test_log_defaults_false(self):
b = _bottle([])
self.assertFalse(b.egress.Log)
def test_log_true_accepted(self):
b = Manifest.from_json_obj({
"bottles": {"dev": {"egress": {"log": True, "routes": []}}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).bottles["dev"]
self.assertTrue(b.egress.Log)
def test_log_non_bool_rejected(self):
with self.assertRaises(ManifestError):
Manifest.from_json_obj({
"bottles": {"dev": {"egress": {"log": "yes"}}},
"agents": {"demo": {"skills": [], "prompt": "",
"bottle": "dev"}},
})
if __name__ == "__main__":
unittest.main()