feat(egress): replace log bool with integer log levels (0/1/2)
lint / lint (push) Failing after 1m25s
test / unit (pull_request) Successful in 31s
test / integration (pull_request) Successful in 42s

Level 0 (off, default): no stderr output beyond boot line.
Level 1 (blocks): each block/warn emitted as JSON with reason and
request context (host, method, path, response_status for inbound).
Level 2 (full): level-1 events + egress_request and egress_response
JSON lines for every forwarded connection.

Block logging at level 1+ replaces the previous plain-text stderr write.
DLP warn logging is also gated on level 1+. All block call sites now pass
_req_ctx(flow) so the blocked request is visible in the log entry.
Boot message shows log level label (off/blocks/full).

Adds PRD 0053 documenting wire format, manifest format, and all log event
shapes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 14:16:12 -04:00
parent e72247a1b5
commit a165752edb
8 changed files with 287 additions and 53 deletions
+15 -13
View File
@@ -324,43 +324,45 @@ 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):
def test_log_zero_omitted_from_render(self):
b = _bottle([{"host": "x.example"}])
routes = egress_routes_for_bottle(b)
rendered = egress_render_routes(routes, log=False)
rendered = egress_render_routes(routes, log=0)
self.assertNotIn("log:", rendered)
def test_log_true_emitted_at_top_level(self):
def test_log_level_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"))
for level in (1, 2):
with self.subTest(level=level):
rendered = egress_render_routes(routes, log=level)
self.assertTrue(rendered.startswith(f"log: {level}\n"))
def test_log_true_round_trips_to_addon_core(self):
from bot_bottle.egress_addon_core import load_config
def test_log_level_round_trips_to_addon_core(self):
from bot_bottle.egress_addon_core import load_config, LOG_FULL
b = _bottle([{"host": "x.example"}])
routes = egress_routes_for_bottle(b)
rendered = egress_render_routes(routes, log=True)
rendered = egress_render_routes(routes, log=LOG_FULL)
cfg = load_config(rendered)
self.assertTrue(cfg.log)
self.assertEqual(LOG_FULL, 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
from bot_bottle.egress_addon_core import load_config, LOG_BLOCKS
m = Manifest.from_json_obj({
"bottles": {"dev": {"egress": {
"log": True,
"log": 1,
"routes": [{"host": "x.example"}],
}}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
})
bottle = m.bottles["dev"]
self.assertTrue(bottle.egress.Log)
self.assertEqual(LOG_BLOCKS, 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)
self.assertEqual(LOG_BLOCKS, cfg.log)
class TestResolveTokenValues(unittest.TestCase):
+25 -11
View File
@@ -13,6 +13,9 @@ from pathlib import Path
from urllib.parse import urlsplit
from bot_bottle.egress_addon_core import (
LOG_BLOCKS,
LOG_FULL,
LOG_OFF,
Config,
Decision,
HeaderMatch,
@@ -278,23 +281,34 @@ class TestLoadRoutes(unittest.TestCase):
class TestLoadConfig(unittest.TestCase):
def test_log_defaults_to_false(self):
def test_log_defaults_to_off(self):
cfg = load_config('routes:\n - host: "api.example"\n')
self.assertFalse(cfg.log)
self.assertEqual(LOG_OFF, 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_level_1_parsed(self):
cfg = load_config('log: 1\nroutes:\n - host: "api.example"\n')
self.assertEqual(LOG_BLOCKS, cfg.log)
def test_log_false_explicit(self):
cfg = load_config('log: false\nroutes:\n - host: "api.example"\n')
self.assertFalse(cfg.log)
def test_log_level_2_parsed(self):
cfg = load_config('log: 2\nroutes:\n - host: "api.example"\n')
self.assertEqual(LOG_FULL, cfg.log)
def test_log_non_bool_rejected(self):
def test_log_level_0_explicit(self):
cfg = load_config('log: 0\nroutes:\n - host: "api.example"\n')
self.assertEqual(LOG_OFF, cfg.log)
def test_log_invalid_level_rejected(self):
with self.assertRaises(ValueError):
load_config('log: "yes"\nroutes: []\n')
load_config('log: 3\nroutes: []\n')
def test_log_bool_rejected(self):
with self.assertRaises(ValueError):
load_config('log: true\nroutes: []\n')
def test_log_string_rejected(self):
with self.assertRaises(ValueError):
load_config('log: "full"\nroutes: []\n')
def test_routes_accessible_via_config(self):
cfg = load_config('routes:\n - host: "x.example"\n')
+30 -7
View File
@@ -346,21 +346,44 @@ class TestConfigShape(unittest.TestCase):
"bottle": "dev"}},
})
def test_log_defaults_false(self):
def test_log_defaults_zero(self):
b = _bottle([])
self.assertFalse(b.egress.Log)
self.assertEqual(0, b.egress.Log)
def test_log_true_accepted(self):
def test_log_level_1_accepted(self):
b = Manifest.from_json_obj({
"bottles": {"dev": {"egress": {"log": True, "routes": []}}},
"bottles": {"dev": {"egress": {"log": 1, "routes": []}}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).bottles["dev"]
self.assertTrue(b.egress.Log)
self.assertEqual(1, b.egress.Log)
def test_log_non_bool_rejected(self):
def test_log_level_2_accepted(self):
b = Manifest.from_json_obj({
"bottles": {"dev": {"egress": {"log": 2, "routes": []}}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).bottles["dev"]
self.assertEqual(2, b.egress.Log)
def test_log_invalid_level_rejected(self):
with self.assertRaises(ManifestError):
Manifest.from_json_obj({
"bottles": {"dev": {"egress": {"log": "yes"}}},
"bottles": {"dev": {"egress": {"log": 3}}},
"agents": {"demo": {"skills": [], "prompt": "",
"bottle": "dev"}},
})
def test_log_bool_rejected(self):
with self.assertRaises(ManifestError):
Manifest.from_json_obj({
"bottles": {"dev": {"egress": {"log": True}}},
"agents": {"demo": {"skills": [], "prompt": "",
"bottle": "dev"}},
})
def test_log_string_rejected(self):
with self.assertRaises(ManifestError):
Manifest.from_json_obj({
"bottles": {"dev": {"egress": {"log": "full"}}},
"agents": {"demo": {"skills": [], "prompt": "",
"bottle": "dev"}},
})