Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Commit afabff3

Browse files
committed
feat: add custom logger that includes the origin
When running servers, use a custom structlog logger that binds an extra parameter called origin. Can be: generic_server, copilot_proxy Closes: #301
1 parent 99f7489 commit afabff3

File tree

5 files changed

+52
-36
lines changed

5 files changed

+52
-36
lines changed

src/codegate/cli.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from typing import Dict, Optional
88

99
import click
10-
import structlog
1110
from uvicorn.config import Config as UvicornConfig
1211
from uvicorn.server import Server
1312

@@ -20,6 +19,7 @@
2019
from codegate.providers.copilot.provider import CopilotProvider
2120
from codegate.server import init_app
2221
from codegate.storage.utils import restore_storage_backup
22+
from codegate.logger.logger import OriginLogger
2323

2424

2525
class UvicornServer:
@@ -33,7 +33,9 @@ def __init__(self, config: UvicornConfig, server: Server):
3333
self._startup_complete = asyncio.Event()
3434
self._shutdown_event = asyncio.Event()
3535
self._should_exit = False
36-
self.logger = structlog.get_logger("codegate")
36+
37+
logger_obj = OriginLogger("generic_server")
38+
self.logger = logger_obj.logger
3739

3840
async def serve(self) -> None:
3941
"""Start the uvicorn server and handle shutdown gracefully."""
@@ -84,8 +86,10 @@ async def cleanup(self) -> None:
8486

8587
def validate_port(ctx: click.Context, param: click.Parameter, value: int) -> int:
8688
"""Validate the port number is in valid range."""
87-
logger = structlog.get_logger("codegate")
88-
logger.debug(f"Validating port number: {value}")
89+
cli_logger_obj = OriginLogger("cli")
90+
cli_logger = cli_logger_obj.logger
91+
92+
cli_logger.debug(f"Validating port number: {value}")
8993
if value is not None and not (1 <= value <= 65535):
9094
raise click.BadParameter("Port must be between 1 and 65535")
9195
return value
@@ -296,7 +300,8 @@ def serve(
296300

297301
# Set up logging first
298302
setup_logging(cfg.log_level, cfg.log_format)
299-
logger = structlog.get_logger("codegate")
303+
cli_logger_obj = OriginLogger("cli")
304+
logger = cli_logger_obj.logger
300305

301306
init_db_sync(cfg.db_path)
302307

@@ -327,7 +332,9 @@ def serve(
327332
click.echo(f"Configuration error: {e}", err=True)
328333
sys.exit(1)
329334
except Exception as e:
330-
logger = structlog.get_logger("codegate")
335+
cli_logger_obj = OriginLogger("cli")
336+
logger = cli_logger_obj.logger
337+
331338
logger.exception("Unexpected error occurred")
332339
click.echo(f"Error: {e}", err=True)
333340
sys.exit(1)
@@ -336,7 +343,9 @@ def serve(
336343
async def run_servers(cfg: Config, app) -> None:
337344
"""Run the codegate server."""
338345
try:
339-
logger = structlog.get_logger("codegate")
346+
cli_logger_obj = OriginLogger("cli")
347+
logger = cli_logger_obj.logger
348+
340349
logger.info(
341350
"Starting server",
342351
extra={

src/codegate/codegate_logging.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ def _missing_(cls, value: str) -> Optional["LogFormat"]:
4848
)
4949

5050

51+
def add_origin(logger, log_method, event_dict):
52+
# Add 'origin' if it's bound to the logger but not explicitly in the event dict
53+
if 'origin' not in event_dict and hasattr(logger, '_context'):
54+
origin = logger._context.get('origin')
55+
if origin:
56+
event_dict['origin'] = origin
57+
return event_dict
58+
59+
5160
def setup_logging(
5261
log_level: Optional[LogLevel] = None, log_format: Optional[LogFormat] = None
5362
) -> logging.Logger:
@@ -74,6 +83,7 @@ def setup_logging(
7483
shared_processors = [
7584
structlog.processors.add_log_level,
7685
structlog.processors.TimeStamper(fmt="%Y-%m-%dT%H:%M:%S.%03dZ", utc=True),
86+
add_origin,
7787
structlog.processors.CallsiteParameterAdder(
7888
[
7989
structlog.processors.CallsiteParameter.MODULE,

src/codegate/logger/logger.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import structlog
2+
3+
4+
class OriginLogger:
5+
def __init__(self, origin: str):
6+
self.logger = structlog.get_logger().bind(origin=origin)

src/codegate/providers/copilot/provider.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Dict, List, Optional, Tuple, Union
66
from urllib.parse import unquote, urljoin, urlparse
77

8-
import structlog
8+
from src.codegate.logger.logger import OriginLogger
99
from litellm.types.utils import Delta, ModelResponse, StreamingChoices
1010

1111
from codegate.ca.codegate_ca import CertificateAuthority
@@ -22,7 +22,8 @@
2222
)
2323
from codegate.providers.copilot.streaming import SSEProcessor
2424

25-
logger = structlog.get_logger("codegate")
25+
logger_obj = OriginLogger("copilot_proxy")
26+
logger = logger_obj.logger
2627

2728
# Constants
2829
MAX_BUFFER_SIZE = 10 * 1024 * 1024 # 10MB
@@ -637,7 +638,7 @@ async def get_target_url(path: str) -> Optional[str]:
637638
# Check for prefix match
638639
for route in VALIDATED_ROUTES:
639640
# For prefix matches, keep the rest of the path
640-
remaining_path = path[len(route.path) :]
641+
remaining_path = path[len(route.path):]
641642
logger.debug(f"Remaining path: {remaining_path}")
642643
# Make sure we don't end up with double slashes
643644
if remaining_path and remaining_path.startswith("/"):
@@ -791,7 +792,7 @@ def data_received(self, data: bytes) -> None:
791792
self._proxy_transport_write(headers)
792793
logger.debug(f"Headers sent: {headers}")
793794

794-
data = data[header_end + 4 :]
795+
data = data[header_end + 4:]
795796

796797
self._process_chunk(data)
797798

tests/test_cli.py

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -190,27 +190,17 @@ def cli_runner():
190190
return CliRunner()
191191

192192

193-
@pytest.fixture
194-
def mock_logging(mocker):
195-
return mocker.patch("your_cli_module.structlog.get_logger")
196-
197-
198-
@pytest.fixture
199-
def mock_setup_logging(mocker):
200-
return mocker.patch("your_cli_module.setup_logging")
201-
202-
203193
def test_serve_default_options(cli_runner):
204194
"""Test serve command with default options."""
205195
# Use patches for run_servers and logging setup
206196
with (
207197
patch("src.codegate.cli.run_servers") as mock_run,
208-
patch("src.codegate.cli.structlog.get_logger") as mock_logging,
198+
patch("src.codegate.cli.OriginLogger") as mock_origin_logger,
209199
patch("src.codegate.cli.setup_logging") as mock_setup_logging,
210200
):
211201

212202
logger_instance = MagicMock()
213-
mock_logging.return_value = logger_instance
203+
mock_origin_logger.return_value = logger_instance
214204

215205
# Invoke the CLI command
216206
result = cli_runner.invoke(cli, ["serve"])
@@ -222,7 +212,7 @@ def test_serve_default_options(cli_runner):
222212
mock_setup_logging.assert_called_once_with(LogLevel.INFO, LogFormat.JSON)
223213

224214
# Check if logging was done correctly
225-
mock_logging.assert_called_with("codegate")
215+
mock_origin_logger.assert_called_with("cli")
226216

227217
# Validate run_servers was called once
228218
mock_run.assert_called_once()
@@ -232,12 +222,12 @@ def test_serve_custom_options(cli_runner):
232222
"""Test serve command with custom options."""
233223
with (
234224
patch("src.codegate.cli.run_servers") as mock_run,
235-
patch("src.codegate.cli.structlog.get_logger") as mock_logging,
225+
patch("src.codegate.cli.OriginLogger") as mock_origin_logger,
236226
patch("src.codegate.cli.setup_logging") as mock_setup_logging,
237227
):
238228

239229
logger_instance = MagicMock()
240-
mock_logging.return_value = logger_instance
230+
mock_origin_logger.return_value = logger_instance
241231

242232
# Invoke the CLI command with custom options
243233
result = cli_runner.invoke(
@@ -272,7 +262,7 @@ def test_serve_custom_options(cli_runner):
272262
mock_setup_logging.assert_called_once_with(LogLevel.DEBUG, LogFormat.TEXT)
273263

274264
# Assert logger got called with the expected module name
275-
mock_logging.assert_called_with("codegate")
265+
mock_origin_logger.assert_called_with("cli")
276266

277267
# Validate run_servers was called once
278268
mock_run.assert_called_once()
@@ -332,20 +322,20 @@ def test_serve_with_config_file(cli_runner, temp_config_file):
332322
"""Test serve command with config file."""
333323
with (
334324
patch("src.codegate.cli.run_servers") as mock_run,
335-
patch("src.codegate.cli.structlog.get_logger") as mock_logging,
325+
patch("src.codegate.cli.OriginLogger") as mock_origin_logger,
336326
patch("src.codegate.cli.setup_logging") as mock_setup_logging,
337327
):
338328

339329
logger_instance = MagicMock()
340-
mock_logging.return_value = logger_instance
330+
mock_origin_logger.return_value = logger_instance
341331

342332
# Invoke the CLI command with the configuration file
343333
result = cli_runner.invoke(cli, ["serve", "--config", str(temp_config_file)])
344334

345335
# Assertions to ensure the CLI ran successfully
346336
assert result.exit_code == 0
347337
mock_setup_logging.assert_called_once_with(LogLevel.DEBUG, LogFormat.JSON)
348-
mock_logging.assert_called_with("codegate")
338+
mock_origin_logger.assert_called_with("cli")
349339

350340
# Validate that run_servers was called with the expected configuration
351341
mock_run.assert_called_once()
@@ -380,12 +370,12 @@ def test_serve_priority_resolution(cli_runner: CliRunner, temp_config_file: Path
380370
with (
381371
patch.dict(os.environ, {"LOG_LEVEL": "INFO", "PORT": "9999"}, clear=True),
382372
patch("src.codegate.cli.run_servers") as mock_run,
383-
patch("src.codegate.cli.structlog.get_logger") as mock_logging,
373+
patch("src.codegate.cli.OriginLogger") as mock_origin_logger,
384374
patch("src.codegate.cli.setup_logging") as mock_setup_logging,
385375
):
386376
# Set up mock logger
387377
logger_instance = MagicMock()
388-
mock_logging.return_value = logger_instance
378+
mock_origin_logger.return_value = logger_instance
389379

390380
# Execute CLI command with specific options overriding environment and config file settings
391381
result = cli_runner.invoke(
@@ -420,7 +410,7 @@ def test_serve_priority_resolution(cli_runner: CliRunner, temp_config_file: Path
420410

421411
# Ensure logging setup was called with the highest priority settings (CLI arguments)
422412
mock_setup_logging.assert_called_once_with("ERROR", "TEXT")
423-
mock_logging.assert_called_with("codegate")
413+
mock_origin_logger.assert_called_with("cli")
424414

425415
# Verify that the run_servers was called with the overridden settings
426416
config_arg = mock_run.call_args[0][0] # Assuming Config is the first positional arg
@@ -448,12 +438,12 @@ def test_serve_certificate_options(cli_runner: CliRunner) -> None:
448438
"""Test serve command with certificate options."""
449439
with (
450440
patch("src.codegate.cli.run_servers") as mock_run,
451-
patch("src.codegate.cli.structlog.get_logger") as mock_logging,
441+
patch("src.codegate.cli.OriginLogger") as mock_origin_logger,
452442
patch("src.codegate.cli.setup_logging") as mock_setup_logging,
453443
):
454444
# Set up mock logger
455445
logger_instance = MagicMock()
456-
mock_logging.return_value = logger_instance
446+
mock_origin_logger.return_value = logger_instance
457447

458448
# Execute CLI command with certificate options
459449
result = cli_runner.invoke(
@@ -478,7 +468,7 @@ def test_serve_certificate_options(cli_runner: CliRunner) -> None:
478468

479469
# Ensure logging setup was called with expected arguments
480470
mock_setup_logging.assert_called_once_with("INFO", "JSON")
481-
mock_logging.assert_called_with("codegate")
471+
mock_origin_logger.assert_called_with("cli")
482472

483473
# Verify that run_servers was called with the provided certificate options
484474
config_arg = mock_run.call_args[0][0] # Assuming Config is the first positional arg

0 commit comments

Comments
 (0)