Update logging configuration to support daily log files and implement log pruning; add guidance in CLAUDE.md and enhance README.md with command usage
This commit is contained in:
64
CLAUDE.md
Normal file
64
CLAUDE.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
A local Anthropic-compatible API proxy that lets Claude Code talk to AI Nexus Claude models. It receives Anthropic Messages API requests and translates them to AWS Bedrock Converse API calls targeting the corporate Nexus endpoint.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
uv sync --extra dev
|
||||||
|
|
||||||
|
# Start the proxy (prints Claude Code env helper)
|
||||||
|
uv run nexus-claude-api start --port 4141 --claude-code
|
||||||
|
|
||||||
|
# Start in dev mode (local config/logs instead of ~/.config)
|
||||||
|
uv run nexus-claude-api start --dev --port 4141 --claude-code
|
||||||
|
|
||||||
|
# Validate config without starting
|
||||||
|
uv run nexus-claude-api start --dry-run
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
uv run pytest
|
||||||
|
|
||||||
|
# Run a single test file or focused test
|
||||||
|
uv run pytest tests/test_cli.py
|
||||||
|
uv run pytest tests/test_routes.py -k messages_stream
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no lint, typecheck, or build command configured. Do not invent those steps.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Claude Code → FastAPI (routes/) → NexusClient (boto3) → AI Nexus Bedrock
|
||||||
|
```
|
||||||
|
|
||||||
|
- **cli.py** — CLI entrypoint (argparse). `__main__.py` just forwards to it.
|
||||||
|
- **server.py** — App factory (`create_app`), stores `settings` and `nexus_client` on `app.state`.
|
||||||
|
- **config.py** — `Settings` frozen dataclass, credential resolution, model ID mapping, defaults.
|
||||||
|
- **nexus_client.py** — Wraps boto3 Bedrock `converse` / `converse_stream`.
|
||||||
|
- **routes/messages.py** — `POST /v1/messages` and `/v1/messages/count_tokens`. Owns request validation, translation dispatch, SSE streaming, and error shaping.
|
||||||
|
- **translators/** — `anthropic_to_bedrock.py`, `bedrock_to_anthropic.py`, `stream.py`. Pure conversion logic.
|
||||||
|
- **errors.py** — `NexusClaudeError` exception mapped to Anthropic-shaped JSON errors with correlation IDs.
|
||||||
|
- **diagnostics.py** — Request summarization for logs; deliberately omits secrets, prompt text, base64.
|
||||||
|
- **shell.py** — Generates PowerShell env vars for Claude Code (sets `ANTHROPIC_AUTH_TOKEN='dummy'` as a placeholder).
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
- **Credential priority:** `--api-key` → `nexus-claude-api.local.json` → `NEXUS_API_KEY` env → `AWS_BEARER_TOKEN_BEDROCK` env (fixed in `Settings.from_values()`).
|
||||||
|
- **Model aliasing:** Sonnet/Haiku aliases resolve to Opus because users only have Opus access.
|
||||||
|
- **Localhost only:** Server binds to `127.0.0.1`.
|
||||||
|
- **No console logging:** App logs go to files only (`~/.config/nexus-claude-api/logs/` or `./logs/` in dev mode).
|
||||||
|
- **`NexusClient` side effect:** Writes `settings.api_key` into `AWS_BEARER_TOKEN_BEDROCK` env var before building the boto3 client.
|
||||||
|
|
||||||
|
## Testing Notes
|
||||||
|
|
||||||
|
- Tests use stub clients (`FakeNexusClient`, `AccessDeniedNexusClient`) — no network calls.
|
||||||
|
- `pythonpath = ["src"]` in pyproject.toml enables src-layout imports; keep this intact.
|
||||||
|
- `tests/test_cli.py` uses per-test temp workspaces under `.test-tmp/`.
|
||||||
|
- `nexus-claude-api.local.json` is gitignored and may exist locally; never overwrite it while testing.
|
||||||
|
- After changing request/error/logging behavior, run `uv run pytest tests/test_routes.py` — it covers SSE responses, correlation IDs, and verifies logs don't leak secrets.
|
||||||
12
README.md
12
README.md
@@ -13,6 +13,12 @@ uv run nexus-claude-api config set --api-key "your-nexus-api-key"
|
|||||||
uv run nexus-claude-api start --port 4141 --claude-code
|
uv run nexus-claude-api start --port 4141 --claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the `nexus-claude-api` command is not on your `PATH`, run the module entrypoint instead:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -m nexus_claude_api start --port 4141 --claude-code
|
||||||
|
```
|
||||||
|
|
||||||
Use the printed Claude Code command in the same shell.
|
Use the printed Claude Code command in the same shell.
|
||||||
|
|
||||||
`ANTHROPIC_AUTH_TOKEN='dummy'` in the printed command is only a local Claude Code compatibility placeholder. Claude Code expects an Anthropic auth token variable to exist, but this local proxy does not validate it by default. It is not your Nexus key.
|
`ANTHROPIC_AUTH_TOKEN='dummy'` in the printed command is only a local Claude Code compatibility placeholder. Claude Code expects an Anthropic auth token variable to exist, but this local proxy does not validate it by default. It is not your Nexus key.
|
||||||
@@ -40,7 +46,7 @@ Credential lookup order:
|
|||||||
To write user config:
|
To write user config:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
nexus-claude-api config set --api-key "your-nexus-api-key"
|
uv run nexus-claude-api config set --api-key "your-nexus-api-key"
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates `~/.config/nexus-claude-api/config.json`:
|
This creates `~/.config/nexus-claude-api/config.json`:
|
||||||
@@ -51,7 +57,7 @@ This creates `~/.config/nexus-claude-api/config.json`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Logs are written to `~/.config/nexus-claude-api/logs/nexus-claude-api.log` by default.
|
Logs are written to dated files under `~/.config/nexus-claude-api/logs/` by default, for example `nexus-claude-api-2026-06-27.log`. The service keeps the latest 7 daily log files and prunes older dated log files automatically.
|
||||||
|
|
||||||
For repository-local development only, pass `--dev` and create `nexus-claude-api.local.json` in the current directory:
|
For repository-local development only, pass `--dev` and create `nexus-claude-api.local.json` in the current directory:
|
||||||
|
|
||||||
@@ -59,7 +65,7 @@ For repository-local development only, pass `--dev` and create `nexus-claude-api
|
|||||||
uv run nexus-claude-api start --dev --port 4141 --claude-code
|
uv run nexus-claude-api start --dev --port 4141 --claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
In development mode, config is read from `nexus-claude-api.local.json` and logs are written to `logs/nexus-claude-api.log` in the current directory. The local config file is ignored by git.
|
In development mode, config is read from `nexus-claude-api.local.json` and daily logs are written to `logs/nexus-claude-api-YYYY-MM-DD.log` in the current directory. The local config file and `logs/` directory are ignored by git.
|
||||||
|
|
||||||
The service binds to `127.0.0.1` by default and does not persist API keys.
|
The service binds to `127.0.0.1` by default and does not persist API keys.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nexus-claude-api"
|
name = "nexus-claude-api"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
description = "Local Anthropic-compatible Claude Code proxy for AI Nexus Bedrock Converse."
|
description = "Local Anthropic-compatible Claude Code proxy for AI Nexus Bedrock Converse."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -1,23 +1,128 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import time
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import nexus_claude_api.config as config
|
import nexus_claude_api.config as config
|
||||||
|
|
||||||
LOCAL_LOG_DIR = Path("logs")
|
LOCAL_LOG_DIR = Path("logs")
|
||||||
LOCAL_LOG_FILE = LOCAL_LOG_DIR / "nexus-claude-api.log"
|
LOG_FILE_PREFIX = "nexus-claude-api"
|
||||||
|
LOG_FILE_SUFFIX = ".log"
|
||||||
|
LOG_RETENTION_DAYS = 7
|
||||||
|
LOCAL_LOG_FILE = LOCAL_LOG_DIR / f"{LOG_FILE_PREFIX}.log"
|
||||||
USER_LOG_DIR = config.USER_CONFIG_DIR / "logs"
|
USER_LOG_DIR = config.USER_CONFIG_DIR / "logs"
|
||||||
USER_LOG_FILE = USER_LOG_DIR / "nexus-claude-api.log"
|
USER_LOG_FILE = USER_LOG_DIR / f"{LOG_FILE_PREFIX}.log"
|
||||||
LOG_FILE = USER_LOG_FILE
|
LOG_FILE = USER_LOG_FILE
|
||||||
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s: %(message)s"
|
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s: %(message)s"
|
||||||
|
|
||||||
|
|
||||||
def configure_logging(*, verbose: bool = False, log_file: Path = LOG_FILE) -> Path:
|
DateProvider = Callable[[float | None], datetime]
|
||||||
|
|
||||||
|
|
||||||
|
def _local_datetime(timestamp: float | None = None) -> datetime:
|
||||||
|
if timestamp is None:
|
||||||
|
return datetime.fromtimestamp(time.time())
|
||||||
|
return datetime.fromtimestamp(timestamp)
|
||||||
|
|
||||||
|
|
||||||
|
def _dated_log_file(log_dir: Path, current_date: datetime) -> Path:
|
||||||
|
return log_dir / f"{LOG_FILE_PREFIX}-{current_date:%Y-%m-%d}{LOG_FILE_SUFFIX}"
|
||||||
|
|
||||||
|
|
||||||
|
class DailyLogFileHandler(logging.Handler):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
log_file: Path,
|
||||||
|
*,
|
||||||
|
date_provider: DateProvider = _local_datetime,
|
||||||
|
retention_days: int = LOG_RETENTION_DAYS,
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.log_dir = log_file.parent
|
||||||
|
self.date_provider = date_provider
|
||||||
|
self.retention_days = retention_days
|
||||||
|
self.encoding = encoding
|
||||||
|
self.current_date = ""
|
||||||
|
self.stream = None
|
||||||
|
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._open_for_date(self.date_provider(None))
|
||||||
|
self._prune_old_logs()
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
|
try:
|
||||||
|
record_datetime = self.date_provider(record.created)
|
||||||
|
self._open_for_date(record_datetime)
|
||||||
|
message = self.format(record)
|
||||||
|
if self.stream is None:
|
||||||
|
return
|
||||||
|
self.stream.write(message + self.terminator)
|
||||||
|
self.flush()
|
||||||
|
except Exception:
|
||||||
|
self.handleError(record)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def baseFilename(self) -> str:
|
||||||
|
return str(self.current_log_file)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def terminator(self) -> str:
|
||||||
|
return "\n"
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
if self.stream and not self.stream.closed:
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
try:
|
||||||
|
if self.stream and not self.stream.closed:
|
||||||
|
self.stream.close()
|
||||||
|
finally:
|
||||||
|
self.stream = None
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
def _open_for_date(self, current_datetime: datetime) -> None:
|
||||||
|
date_text = current_datetime.strftime("%Y-%m-%d")
|
||||||
|
if date_text == self.current_date and self.stream is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.stream is not None and not self.stream.closed:
|
||||||
|
self.stream.close()
|
||||||
|
|
||||||
|
self.current_date = date_text
|
||||||
|
self.current_log_file = _dated_log_file(self.log_dir, current_datetime)
|
||||||
|
self.stream = self.current_log_file.open("a", encoding=self.encoding)
|
||||||
|
self._prune_old_logs()
|
||||||
|
|
||||||
|
def _prune_old_logs(self) -> None:
|
||||||
|
dated_logs = []
|
||||||
|
for path in self.log_dir.glob(f"{LOG_FILE_PREFIX}-*{LOG_FILE_SUFFIX}"):
|
||||||
|
date_text = path.stem.removeprefix(f"{LOG_FILE_PREFIX}-")
|
||||||
|
try:
|
||||||
|
log_date = datetime.strptime(date_text, "%Y-%m-%d").date()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
dated_logs.append((log_date, path))
|
||||||
|
|
||||||
|
dated_logs.sort(reverse=True)
|
||||||
|
for _, path in dated_logs[self.retention_days :]:
|
||||||
|
path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_logging(
|
||||||
|
*,
|
||||||
|
verbose: bool = False,
|
||||||
|
log_file: Path = LOG_FILE,
|
||||||
|
date_provider: DateProvider = _local_datetime,
|
||||||
|
) -> Path:
|
||||||
|
log_file = _dated_log_file(log_file.parent, date_provider(None))
|
||||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
formatter = logging.Formatter(LOG_FORMAT)
|
formatter = logging.Formatter(LOG_FORMAT)
|
||||||
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
file_handler = DailyLogFileHandler(log_file, date_provider=date_provider)
|
||||||
file_handler.setLevel(logging.DEBUG)
|
file_handler.setLevel(logging.DEBUG)
|
||||||
file_handler.setFormatter(formatter)
|
file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
@@ -32,5 +137,5 @@ def configure_logging(*, verbose: bool = False, log_file: Path = LOG_FILE) -> Pa
|
|||||||
|
|
||||||
def resolve_log_file(*, dev: bool = False) -> Path:
|
def resolve_log_file(*, dev: bool = False) -> Path:
|
||||||
if dev:
|
if dev:
|
||||||
return LOCAL_LOG_FILE
|
return _dated_log_file(LOCAL_LOG_DIR, _local_datetime(None))
|
||||||
return config.USER_CONFIG_DIR / "logs" / "nexus-claude-api.log"
|
return _dated_log_file(config.USER_CONFIG_DIR / "logs", _local_datetime(None))
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from nexus_claude_api.logging_config import configure_logging, resolve_log_file
|
from nexus_claude_api.logging_config import configure_logging, resolve_log_file
|
||||||
|
|
||||||
|
|
||||||
|
FIXED_NOW = datetime(2026, 6, 27, 10, 30, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def fixed_date_provider(timestamp: float | None = None) -> datetime:
|
||||||
|
if timestamp is None:
|
||||||
|
return FIXED_NOW
|
||||||
|
return datetime.fromtimestamp(timestamp)
|
||||||
|
|
||||||
|
|
||||||
def test_configure_logging_writes_app_logs_to_file_without_console(
|
def test_configure_logging_writes_app_logs_to_file_without_console(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
capsys,
|
capsys,
|
||||||
) -> None:
|
) -> None:
|
||||||
log_file = tmp_path / "nexus-claude-api.log"
|
log_file = tmp_path / "nexus-claude-api.log"
|
||||||
|
dated_log_file = tmp_path / "nexus-claude-api-2026-06-27.log"
|
||||||
logger = logging.getLogger("nexus_claude_api.routes.messages")
|
logger = logging.getLogger("nexus_claude_api.routes.messages")
|
||||||
app_logger = logging.getLogger("nexus_claude_api")
|
app_logger = logging.getLogger("nexus_claude_api")
|
||||||
original_handlers = app_logger.handlers[:]
|
original_handlers = app_logger.handlers[:]
|
||||||
@@ -18,13 +29,18 @@ def test_configure_logging_writes_app_logs_to_file_without_console(
|
|||||||
original_propagate = app_logger.propagate
|
original_propagate = app_logger.propagate
|
||||||
|
|
||||||
try:
|
try:
|
||||||
configure_logging(verbose=False, log_file=log_file)
|
returned_log_file = configure_logging(
|
||||||
|
verbose=False,
|
||||||
|
log_file=log_file,
|
||||||
|
date_provider=fixed_date_provider,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("detailed app request")
|
logger.info("detailed app request")
|
||||||
logger.debug("debug details hidden without verbose")
|
logger.debug("debug details hidden without verbose")
|
||||||
|
|
||||||
output = capsys.readouterr()
|
output = capsys.readouterr()
|
||||||
log_text = log_file.read_text(encoding="utf-8")
|
log_text = dated_log_file.read_text(encoding="utf-8")
|
||||||
|
assert returned_log_file == dated_log_file
|
||||||
assert output.err == ""
|
assert output.err == ""
|
||||||
assert "detailed app request" in log_text
|
assert "detailed app request" in log_text
|
||||||
assert "debug details hidden without verbose" not in log_text
|
assert "debug details hidden without verbose" not in log_text
|
||||||
@@ -39,6 +55,7 @@ def test_configure_logging_verbose_writes_debug_app_logs_to_file(
|
|||||||
tmp_path,
|
tmp_path,
|
||||||
) -> None:
|
) -> None:
|
||||||
log_file = tmp_path / "nexus-claude-api.log"
|
log_file = tmp_path / "nexus-claude-api.log"
|
||||||
|
dated_log_file = tmp_path / "nexus-claude-api-2026-06-27.log"
|
||||||
logger = logging.getLogger("nexus_claude_api.routes.messages")
|
logger = logging.getLogger("nexus_claude_api.routes.messages")
|
||||||
app_logger = logging.getLogger("nexus_claude_api")
|
app_logger = logging.getLogger("nexus_claude_api")
|
||||||
original_handlers = app_logger.handlers[:]
|
original_handlers = app_logger.handlers[:]
|
||||||
@@ -46,11 +63,18 @@ def test_configure_logging_verbose_writes_debug_app_logs_to_file(
|
|||||||
original_propagate = app_logger.propagate
|
original_propagate = app_logger.propagate
|
||||||
|
|
||||||
try:
|
try:
|
||||||
configure_logging(verbose=True, log_file=log_file)
|
configure_logging(
|
||||||
|
verbose=True,
|
||||||
|
log_file=log_file,
|
||||||
|
date_provider=fixed_date_provider,
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug("debug details stay in file")
|
logger.debug("debug details stay in file")
|
||||||
|
|
||||||
assert "debug details stay in file" in log_file.read_text(encoding="utf-8")
|
assert (
|
||||||
|
"debug details stay in file"
|
||||||
|
in dated_log_file.read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
app_logger.handlers.clear()
|
app_logger.handlers.clear()
|
||||||
app_logger.handlers.extend(original_handlers)
|
app_logger.handlers.extend(original_handlers)
|
||||||
@@ -64,9 +88,118 @@ def test_resolve_log_file_uses_user_config_dir_by_default(
|
|||||||
) -> None:
|
) -> None:
|
||||||
user_config_dir = tmp_path / ".config" / "nexus-claude-api"
|
user_config_dir = tmp_path / ".config" / "nexus-claude-api"
|
||||||
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_DIR", user_config_dir)
|
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_DIR", user_config_dir)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nexus_claude_api.logging_config._local_datetime",
|
||||||
|
lambda timestamp=None: FIXED_NOW,
|
||||||
|
)
|
||||||
|
|
||||||
assert resolve_log_file() == user_config_dir / "logs" / "nexus-claude-api.log"
|
assert (
|
||||||
|
resolve_log_file()
|
||||||
|
== user_config_dir / "logs" / "nexus-claude-api-2026-06-27.log"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_log_file_uses_local_logs_in_dev_mode() -> None:
|
def test_resolve_log_file_uses_local_logs_in_dev_mode(monkeypatch) -> None:
|
||||||
assert resolve_log_file(dev=True) == Path("logs") / "nexus-claude-api.log"
|
monkeypatch.setattr(
|
||||||
|
"nexus_claude_api.logging_config._local_datetime",
|
||||||
|
lambda timestamp=None: FIXED_NOW,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resolve_log_file(dev=True) == Path("logs") / "nexus-claude-api-2026-06-27.log"
|
||||||
|
|
||||||
|
|
||||||
|
def test_configure_logging_prunes_logs_older_than_recent_seven_days(tmp_path) -> None:
|
||||||
|
for days_ago in range(10):
|
||||||
|
log_date = FIXED_NOW - timedelta(days=days_ago)
|
||||||
|
(tmp_path / f"nexus-claude-api-{log_date:%Y-%m-%d}.log").write_text(
|
||||||
|
f"log {days_ago}",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
ignored_log = tmp_path / "nexus-claude-api-debug.log"
|
||||||
|
ignored_log.write_text("keep me", encoding="utf-8")
|
||||||
|
|
||||||
|
app_logger = logging.getLogger("nexus_claude_api")
|
||||||
|
original_handlers = app_logger.handlers[:]
|
||||||
|
original_level = app_logger.level
|
||||||
|
original_propagate = app_logger.propagate
|
||||||
|
|
||||||
|
try:
|
||||||
|
configure_logging(
|
||||||
|
verbose=False,
|
||||||
|
log_file=tmp_path / "nexus-claude-api.log",
|
||||||
|
date_provider=fixed_date_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
remaining_logs = sorted(
|
||||||
|
path.name for path in tmp_path.glob("nexus-claude-api-*.log")
|
||||||
|
)
|
||||||
|
assert remaining_logs == [
|
||||||
|
"nexus-claude-api-2026-06-21.log",
|
||||||
|
"nexus-claude-api-2026-06-22.log",
|
||||||
|
"nexus-claude-api-2026-06-23.log",
|
||||||
|
"nexus-claude-api-2026-06-24.log",
|
||||||
|
"nexus-claude-api-2026-06-25.log",
|
||||||
|
"nexus-claude-api-2026-06-26.log",
|
||||||
|
"nexus-claude-api-2026-06-27.log",
|
||||||
|
"nexus-claude-api-debug.log",
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
app_logger.handlers.clear()
|
||||||
|
app_logger.handlers.extend(original_handlers)
|
||||||
|
app_logger.setLevel(original_level)
|
||||||
|
app_logger.propagate = original_propagate
|
||||||
|
|
||||||
|
|
||||||
|
def test_configure_logging_switches_files_when_record_date_changes(tmp_path) -> None:
|
||||||
|
log_file = tmp_path / "nexus-claude-api.log"
|
||||||
|
logger = logging.getLogger("nexus_claude_api.routes.messages")
|
||||||
|
app_logger = logging.getLogger("nexus_claude_api")
|
||||||
|
original_handlers = app_logger.handlers[:]
|
||||||
|
original_level = app_logger.level
|
||||||
|
original_propagate = app_logger.propagate
|
||||||
|
|
||||||
|
try:
|
||||||
|
configure_logging(
|
||||||
|
verbose=False,
|
||||||
|
log_file=log_file,
|
||||||
|
date_provider=lambda timestamp=None: datetime.fromtimestamp(timestamp)
|
||||||
|
if timestamp is not None
|
||||||
|
else datetime(2026, 6, 27, 10, 30, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
before_record = logger.makeRecord(
|
||||||
|
logger.name,
|
||||||
|
logging.INFO,
|
||||||
|
__file__,
|
||||||
|
1,
|
||||||
|
"before midnight",
|
||||||
|
(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
before_record.created = datetime(2026, 6, 27, 23, 59, 0).timestamp()
|
||||||
|
after_record = logger.makeRecord(
|
||||||
|
logger.name,
|
||||||
|
logging.INFO,
|
||||||
|
__file__,
|
||||||
|
1,
|
||||||
|
"after midnight",
|
||||||
|
(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
after_record.created = datetime(2026, 6, 28, 0, 1, 0).timestamp()
|
||||||
|
|
||||||
|
for handler in app_logger.handlers:
|
||||||
|
handler.handle(before_record)
|
||||||
|
handler.handle(after_record)
|
||||||
|
|
||||||
|
assert "before midnight" in (
|
||||||
|
tmp_path / "nexus-claude-api-2026-06-27.log"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
assert "after midnight" in (
|
||||||
|
tmp_path / "nexus-claude-api-2026-06-28.log"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
finally:
|
||||||
|
app_logger.handlers.clear()
|
||||||
|
app_logger.handlers.extend(original_handlers)
|
||||||
|
app_logger.setLevel(original_level)
|
||||||
|
app_logger.propagate = original_propagate
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -173,7 +173,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nexus-claude-api"
|
name = "nexus-claude-api"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "boto3" },
|
{ name = "boto3" },
|
||||||
|
|||||||
Reference in New Issue
Block a user