mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-08 22:20:54 +08:00
feat: unify runtime logging (rotating files, log root, scripts, desktop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
86d4bad107
commit
1490bd5f24
25 changed files with 541 additions and 53 deletions
|
|
@ -27,6 +27,8 @@ This repository is fully consolidated under `oclaw/`.
|
|||
|
||||
3. **助手主库(PostgreSQL,可选)**:默认使用本地 SQLite;若要将助手持久化迁到 PostgreSQL 或从零搭 PG,步骤与脚本说明见 `docs/ASSISTANT_PG_MIGRATION.md`(环境变量仍以 `docs/ENVIRONMENT_VARIABLES.md` 为准)。
|
||||
|
||||
4. **日志**:运行与轮转日志目录、排障时查看哪些文件,见 `docs/LOGGING.md`。
|
||||
|
||||
### 命令(仓库根目录执行)
|
||||
|
||||
```powershell
|
||||
|
|
@ -129,6 +131,7 @@ WhatsApp install/start guide: see `docs/RUNBOOK.md` → “4.2 WhatsApp(实验
|
|||
Minimal onboarding guide: `docs/OPEN_SOURCE_QUICKSTART.md`.
|
||||
Branding guide (external-first assets under `_local/branding`): `docs/BRANDING.md`.
|
||||
Assistant main store: SQLite by default; optional PostgreSQL setup and SQLite→PG cutover: `docs/ASSISTANT_PG_MIGRATION.md`.
|
||||
Runtime logging layout and troubleshooting: `docs/LOGGING.md`.
|
||||
|
||||
Chinese zero-to-running checklist: see **开箱即用(从零跑起来)** at the top of this README.
|
||||
|
||||
|
|
@ -172,6 +175,7 @@ Chinese zero-to-running checklist: see **开箱即用(从零跑起来)** at
|
|||
|
||||
See `docs/ENVIRONMENT_VARIABLES.md` for full runtime variable reference.
|
||||
PostgreSQL assistant-store migration (schema, data import, cutover): `docs/ASSISTANT_PG_MIGRATION.md`.
|
||||
Logging directories and rotating files: `docs/LOGGING.md`.
|
||||
|
||||
## License and security
|
||||
|
||||
|
|
|
|||
|
|
@ -279,8 +279,21 @@ AIA_MAX_ATTACHMENT_BYTES=
|
|||
# -----------------------------------------------------------------------------
|
||||
# 十二、路由与运行时日志
|
||||
# -----------------------------------------------------------------------------
|
||||
# 详见 docs/LOGGING.md。根目录默认:<db_path 父目录>/logs(常为 data/logs/)。
|
||||
AIA_OCLAW_ROUTER_MODE=
|
||||
AIA_RUNTIME_LOG_DIR=
|
||||
# OCLAW_DESKTOP_LOG_ROOT 可选;Electron 写 desktop.log/backend.log 的根;start_desktop.ps1 会设为与 AIA_RUNTIME_LOG_DIR 相同。
|
||||
OCLAW_DESKTOP_LOG_ROOT=
|
||||
# OCLAW_LOG_LEVEL / AIA_LOG_LEVEL 应用与 uvicorn 文件日志级别,默认 INFO。
|
||||
# OCLAW_LOG_MAX_BYTES 单文件上限(字节),默认 20971520(20 MiB)。
|
||||
# OCLAW_LOG_BACKUP_COUNT 轮转保留份数,默认 5。
|
||||
# AIA_LOG_TO_FILE 设为 0/false/off 时不写 app/*.log(pytest 也会跳过文件 handler)。
|
||||
# OCLAW_HOOK_LOG_USE_STATE_DIR 设为 1/true 时,command-logger / boot-md 改回 ~/.oclaw/logs(或 OCLAW_STATE_DIR/logs);默认写入运行日志根下 hooks/。
|
||||
OCLAW_LOG_LEVEL=
|
||||
OCLAW_LOG_MAX_BYTES=
|
||||
OCLAW_LOG_BACKUP_COUNT=
|
||||
AIA_LOG_TO_FILE=
|
||||
OCLAW_HOOK_LOG_USE_STATE_DIR=
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 十三、RAG / 记忆
|
||||
|
|
|
|||
|
|
@ -27,9 +27,12 @@ The app will:
|
|||
2. Start backend using `python -m runtime.operations gateway start --host 127.0.0.1 --port <port>`.
|
||||
3. Open `http://127.0.0.1:<port>/chat` in the desktop window.
|
||||
|
||||
Logs are written under:
|
||||
Logs (embedded backend, desktop shell, optional WeCom channel child) are written under the **runtime log root** (same as `AIA_RUNTIME_LOG_DIR` / `data/logs/` — see repo root [`docs/LOGGING.md`](../docs/LOGGING.md)). When launched via `runtime/operations/scripts/start_desktop.ps1`, `OCLAW_DESKTOP_LOG_ROOT` is set for you.
|
||||
|
||||
- `%APPDATA%/oclaw/logs/backend.log` (Windows)
|
||||
Typical files:
|
||||
|
||||
- `desktop.log`
|
||||
- `backend.log`
|
||||
|
||||
## Environment knobs
|
||||
|
||||
|
|
|
|||
|
|
@ -23,8 +23,16 @@ const APP_ICON_PATH = (() => {
|
|||
}
|
||||
return candidates[candidates.length - 1];
|
||||
})();
|
||||
const REPO_ROOT_FOR_LOGS = path.resolve(__dirname, "..");
|
||||
function resolveFileLogRoot() {
|
||||
const fromEnv = String(
|
||||
process.env.OCLAW_DESKTOP_LOG_ROOT || process.env.AIA_RUNTIME_LOG_DIR || ""
|
||||
).trim();
|
||||
if (fromEnv) return path.resolve(fromEnv);
|
||||
return path.join(REPO_ROOT_FOR_LOGS, "data", "logs");
|
||||
}
|
||||
const DATA_ROOT = path.join(app.getPath("userData"), "runtime-data");
|
||||
const LOG_ROOT = path.join(app.getPath("userData"), "logs");
|
||||
const LOG_ROOT = resolveFileLogRoot();
|
||||
const BACKEND_LOG_FILE = path.join(LOG_ROOT, "backend.log");
|
||||
const STARTUP_TIMEOUT_MS = 30000;
|
||||
const POLL_INTERVAL_MS = 600;
|
||||
|
|
|
|||
|
|
@ -519,9 +519,40 @@
|
|||
- 生效:`oclaw/interfaces/http/fastapi_app.py`
|
||||
|
||||
- `AIA_RUNTIME_LOG_DIR`
|
||||
- 默认:空(使用内部默认目录)
|
||||
- 作用:运行日志目录
|
||||
- 生效:`oclaw/runtime/operations/runtime.py`
|
||||
- 默认:空(使用 `<assistant DB 文件父目录>/logs`,常见为 `data/logs/`)
|
||||
- 作用:运行日志根目录(网关、渠道、wiki worker、微信/WhatsApp sidecar 重定向、Desktop 文件日志、Hook `hooks/` 等)
|
||||
- 说明:布局与排障见 [`LOGGING.md`](./LOGGING.md)
|
||||
- 生效:`oclaw/svc/config/log_paths.py`, `oclaw/runtime/operations/runtime.py`, `oclaw/svc/observability/logging_setup.py`, `oclaw/interfaces/http/fastapi_app.py`, `oclaw/desktop/main.js`, `oclaw/runtime/operations/scripts/lib/ResolveRuntimeLogDir.ps1` 及依赖该库的启动脚本
|
||||
|
||||
- `OCLAW_DESKTOP_LOG_ROOT`
|
||||
- 默认:空(Desktop 使用 `AIA_RUNTIME_LOG_DIR`;再空则 `<仓库根>/data/logs/`,见 `desktop/main.js`)
|
||||
- 作用:Electron 内嵌网关等写入 `desktop.log` / `backend.log` 等的目录;`start_desktop.ps1` 会设为与 `AIA_RUNTIME_LOG_DIR` 相同
|
||||
- 生效:`oclaw/desktop/main.js`, `oclaw/runtime/operations/scripts/start_desktop.ps1`
|
||||
|
||||
- `OCLAW_LOG_LEVEL` / `AIA_LOG_LEVEL`
|
||||
- 默认:`INFO`
|
||||
- 作用:写入 `app/oclaw.log`(及 uvicorn 相关 logger)的级别;取值 `DEBUG` / `INFO` / `WARNING` / `ERROR` / `CRITICAL`(`WARN`→`WARNING`,`FATAL`→`CRITICAL`)
|
||||
- 生效:`oclaw/svc/observability/logging_setup.py`
|
||||
|
||||
- `OCLAW_LOG_MAX_BYTES`
|
||||
- 默认:`20971520`(20 MiB)
|
||||
- 作用:单一日志文件达到该大小后轮转
|
||||
- 生效:`oclaw/svc/observability/logging_setup.py`
|
||||
|
||||
- `OCLAW_LOG_BACKUP_COUNT`
|
||||
- 默认:`5`
|
||||
- 作用:轮转文件保留数量(含当前文件在内的上限由 handler 实现决定)
|
||||
- 生效:`oclaw/svc/observability/logging_setup.py`
|
||||
|
||||
- `AIA_LOG_TO_FILE`
|
||||
- 默认:未设置(启用文件日志;pytest 进程内仍会因 `PYTEST_CURRENT_TEST` 跳过写文件)
|
||||
- 作用:设为 `0` / `false` / `no` / `off` 时不配置轮转文件 handler(便于前台调试或特殊环境)
|
||||
- 生效:`oclaw/svc/observability/logging_setup.py`, `oclaw/interfaces/http/fastapi_app.py`(网关不传 `log_config`)
|
||||
|
||||
- `OCLAW_HOOK_LOG_USE_STATE_DIR`
|
||||
- 默认:未设置(`command-logger` / `boot-md` 写入运行日志根下的 `hooks/`,与 `data/logs` 同树)
|
||||
- 作用:设为 `1` / `true` / `yes` / `on` 时,改回旧布局:写入 `OCLAW_STATE_DIR`(或 `~/.oclaw`)下的 `logs/`
|
||||
- 生效:`oclaw/runtime/hooks/bundled/command-logger/handler.py`, `oclaw/runtime/hooks/bundled/boot-md/handler.py`, `oclaw/svc/config/log_paths.py`
|
||||
|
||||
- `AIA_SSE_QUEUE_MAXSIZE`
|
||||
- 默认:`2000`
|
||||
|
|
|
|||
43
docs/LOGGING.md
Normal file
43
docs/LOGGING.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# 日志与排障
|
||||
|
||||
本文说明 Oclaw **日志根目录**、进程级日志与 **Python 轮转日志** 的落点,便于线上/本机排障。环境变量完整列表仍以 [`ENVIRONMENT_VARIABLES.md`](./ENVIRONMENT_VARIABLES.md) 为准。
|
||||
|
||||
## 日志根目录
|
||||
|
||||
由 **`AIA_RUNTIME_LOG_DIR`** 指定;未设置时与 `runtime.operations.runtime.assistant_runtime_log_dir()` 一致:**助手库文件(`db_path()`)所在目录下的 `logs/`**(常见为仓库下 `data/logs/`)。
|
||||
|
||||
网关、`stack up` 启用的渠道进程、**轮转应用日志**、**内置 Hook 文件日志**(`hooks/`)、**微信/WhatsApp sidecar 的 stdout/stderr**(`weixin_sidecar.*` / `whatsapp_sidecar.*`),以及 **Desktop(Electron)** 写入的 `desktop.log` / `backend.log` / `channel-wecom.log`,均默认使用该根目录(Desktop 由 `start_desktop.ps1` 设置 `AIA_RUNTIME_LOG_DIR` / `OCLAW_DESKTOP_LOG_ROOT`,或直接运行 Electron 时回落到 **`<仓库根>/data/logs/`**)。Wiki worker 若用 `start_wiki_worker.ps1` 后台启动,其 stdout/stderr 为 `wiki_worker.out.log` / `wiki_worker.err.log`。
|
||||
|
||||
## 目录布局(根目录下)
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `gateway.out.log` / `gateway.err.log` | `start_service` 重定向子进程的 stdout/stderr;裸 `print`、未走 `logging` 的输出等。**追加写,无应用内轮转**;生产可配合系统 logrotate。 |
|
||||
| `channel_*.out.log` / `.err.log` | 同上,渠道名中的 `:` 会替换为 `_`。 |
|
||||
| `app/oclaw.log` | **轮转**:应用与 uvicorn 主日志(`RotatingFileHandler`)。 |
|
||||
| `app/uvicorn-access.log` | **轮转**:HTTP access(仅网关 uvicorn 模式)。 |
|
||||
| `weixin_sidecar.log` / `weixin_sidecar.err.log` | `weixin_start.ps1` 重定向 sidecar 进程输出(原在 `data/channel_sidecar/.../logs`,已统一到运行日志根)。 |
|
||||
| `whatsapp_sidecar.log` / `whatsapp_sidecar.err.log` | `whatsapp_start.ps1` 同上。 |
|
||||
| `desktop.log` / `backend.log` / `channel-wecom.log` | Desktop Electron 内嵌网关与渠道子进程日志(`desktop/main.js`)。 |
|
||||
| `wiki_worker.out.log` / `wiki_worker.err.log` | `start_wiki_worker.ps1` 后台模式重定向的 stdout/stderr(与运行日志根一致)。 |
|
||||
| `hooks/commands.log`、`boot-md.log` | 默认在 **`{运行日志根}/hooks/`**(与网关同根)。若需恢复旧行为(`OCLAW_STATE_DIR` 或 `~/.oclaw/logs`),设置 **`OCLAW_HOOK_LOG_USE_STATE_DIR=1`**。 |
|
||||
|
||||
## 环境变量(摘要)
|
||||
|
||||
| 变量 | 作用 |
|
||||
|------|------|
|
||||
| `AIA_RUNTIME_LOG_DIR` | 上述根目录。 |
|
||||
| `OCLAW_LOG_LEVEL` / `AIA_LOG_LEVEL` | 文件日志级别,默认 `INFO`(支持 `DEBUG`、`WARNING`、`ERROR`、`CRITICAL` 等)。 |
|
||||
| `OCLAW_LOG_MAX_BYTES` | 单文件最大字节,默认 `20971520`(20 MiB)。 |
|
||||
| `OCLAW_LOG_BACKUP_COUNT` | 轮转保留份数,默认 `5`。 |
|
||||
| `AIA_LOG_TO_FILE` | 设为 `0` / `false` / `off` 等时**不写**轮转文件(pytest 也会自动跳过文件 handler)。 |
|
||||
| `OCLAW_DESKTOP_LOG_ROOT` | 可选;Desktop 写入 `desktop.log` / `backend.log` 等的目录。未设置时读 `AIA_RUNTIME_LOG_DIR`;再未设置则用 **`<仓库根>/data/logs/`**(与 `desktop/main.js` 一致)。`start_desktop.ps1` 会将其设为与 `AIA_RUNTIME_LOG_DIR` 相同。 |
|
||||
| `OCLAW_HOOK_LOG_USE_STATE_DIR` | 设为 `1` / `true` 时,内置钩子日志改写到 **`OCLAW_STATE_DIR`(或 `~/.oclaw`)下的 `logs/`**;未设置时默认 **`{运行日志根}/hooks/`**。 |
|
||||
|
||||
## 实现位置
|
||||
|
||||
- 根路径:[`svc/config/log_paths.py`](../svc/config/log_paths.py)(`oclaw_log_root`、`oclaw_hooks_log_dir`)
|
||||
- 配置:[`svc/observability/logging_setup.py`](../svc/observability/logging_setup.py)
|
||||
- 网关:[`interfaces/http/fastapi_app.py`](../interfaces/http/fastapi_app.py) 的 `main()` 在 `uvicorn.run(..., log_config=...)`
|
||||
- PowerShell 解析与网关相同的根目录:[`runtime/operations/scripts/lib/ResolveRuntimeLogDir.ps1`](../runtime/operations/scripts/lib/ResolveRuntimeLogDir.ps1)(`Get-OclawRuntimeLogDir`)
|
||||
- 企微 longconn / wiki worker:进程入口 `configure_oclaw_logging(...)`
|
||||
|
|
@ -650,6 +650,11 @@ def _run_ws_forever(*, sender: WeComClient, deliver_outbound: bool, use_response
|
|||
|
||||
|
||||
def run_forever() -> int:
|
||||
from svc.config.bootstrap_env import load_system_env
|
||||
from svc.observability.logging_setup import configure_oclaw_logging
|
||||
|
||||
load_system_env()
|
||||
configure_oclaw_logging(service_name="wecom-longconn", include_uvicorn_formatters=False)
|
||||
store = get_assistant_store()
|
||||
sender = WeComClient(store)
|
||||
lock = _SingleInstanceLock(Path(db_path()).resolve().parent / "locks" / "wecom_longconn.lock")
|
||||
|
|
|
|||
|
|
@ -300,6 +300,13 @@ def create_app() -> FastAPI:
|
|||
|
||||
def main() -> int:
|
||||
load_system_env()
|
||||
from svc.observability.logging_setup import (
|
||||
build_uvicorn_logging_dict_config,
|
||||
configure_oclaw_logging,
|
||||
skip_file_logging,
|
||||
)
|
||||
|
||||
configure_oclaw_logging(service_name="gateway", include_uvicorn_formatters=True)
|
||||
host = (os.getenv("AIA_ASSISTANT_GATEWAY_HOST") or "0.0.0.0").strip()
|
||||
port = int(os.getenv("AIA_ASSISTANT_GATEWAY_PORT") or "8787")
|
||||
try:
|
||||
|
|
@ -313,14 +320,17 @@ def main() -> int:
|
|||
except Exception:
|
||||
ws_max_size = int(MAX_PAYLOAD_BYTES)
|
||||
ws_max_size = max(1024, min(int(ws_max_size), 200_000_000))
|
||||
uvicorn.run(
|
||||
"interfaces.http.fastapi_app:create_app",
|
||||
host=host,
|
||||
port=port,
|
||||
reload=False,
|
||||
factory=True,
|
||||
ws_max_size=ws_max_size,
|
||||
)
|
||||
run_kw: dict[str, Any] = {
|
||||
"app": "interfaces.http.fastapi_app:create_app",
|
||||
"host": host,
|
||||
"port": port,
|
||||
"reload": False,
|
||||
"factory": True,
|
||||
"ws_max_size": ws_max_size,
|
||||
}
|
||||
if not skip_file_logging():
|
||||
run_kw["log_config"] = build_uvicorn_logging_dict_config()
|
||||
uvicorn.run(**run_kw)
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import os
|
|||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from svc.config.log_paths import oclaw_hooks_log_dir
|
||||
|
||||
|
||||
def _resolve_state_dir() -> Path:
|
||||
override = os.environ.get("OCLAW_STATE_DIR") or os.environ.get("OCLAW_HOME")
|
||||
|
|
@ -49,9 +51,7 @@ def handle(event: Any) -> None:
|
|||
if getattr(event, "type", None) != "gateway" or getattr(event, "action", None) != "startup":
|
||||
return
|
||||
|
||||
state = _resolve_state_dir()
|
||||
log_dir = state / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_dir = oclaw_hooks_log_dir(state_dir_if_legacy=_resolve_state_dir())
|
||||
out_log = log_dir / "boot-md.log"
|
||||
|
||||
now = getattr(event, "timestamp", None)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import os
|
|||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from svc.config.log_paths import oclaw_hooks_log_dir
|
||||
|
||||
|
||||
def _resolve_state_dir() -> Path:
|
||||
# Keep this compatible with typical Oclaw layouts.
|
||||
|
|
@ -18,9 +20,7 @@ def handle(event) -> None:
|
|||
if getattr(event, "type", None) != "command":
|
||||
return
|
||||
|
||||
state_dir = _resolve_state_dir()
|
||||
log_dir = state_dir / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_dir = oclaw_hooks_log_dir(state_dir_if_legacy=_resolve_state_dir())
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"timestamp": getattr(getattr(event, "timestamp", None), "isoformat", lambda: None)(),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from dataclasses import dataclass
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from svc.config.log_paths import oclaw_log_root
|
||||
from svc.config.paths import db_path
|
||||
|
||||
|
||||
|
|
@ -17,10 +18,7 @@ def _runtime_file() -> Path:
|
|||
|
||||
|
||||
def _runtime_log_dir() -> Path:
|
||||
p = str(os.getenv("AIA_RUNTIME_LOG_DIR") or "").strip()
|
||||
if p:
|
||||
return Path(p).expanduser().resolve()
|
||||
return Path(db_path()).resolve().parent / "logs"
|
||||
return oclaw_log_root()
|
||||
|
||||
|
||||
def assistant_runtime_log_dir() -> Path:
|
||||
|
|
@ -29,7 +27,7 @@ def assistant_runtime_log_dir() -> Path:
|
|||
Same rule as ``AIA_RUNTIME_LOG_DIR`` or ``<parent of sqlite db_path>)/logs``.
|
||||
Use this from shell scripts so paths match Python ``stack up`` / ``start_service``.
|
||||
"""
|
||||
return _runtime_log_dir()
|
||||
return oclaw_log_root()
|
||||
|
||||
|
||||
def _read_state() -> dict[str, Any]:
|
||||
|
|
|
|||
23
runtime/operations/scripts/lib/ResolveRuntimeLogDir.ps1
Normal file
23
runtime/operations/scripts/lib/ResolveRuntimeLogDir.ps1
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Shared: resolve the same directory as Python ``assistant_runtime_log_dir()`` / ``AIA_RUNTIME_LOG_DIR``.
|
||||
function Get-OclawRuntimeLogDir {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$RepoRoot
|
||||
)
|
||||
$venvPython = Join-Path $RepoRoot ".venv\Scripts\python.exe"
|
||||
$pythonExe = if (Test-Path $venvPython) { $venvPython } else { "python" }
|
||||
$savedPyPath = $env:PYTHONPATH
|
||||
$env:PYTHONPATH = $RepoRoot
|
||||
$dir = $null
|
||||
try {
|
||||
$dir = (& $pythonExe -c "from runtime.operations.runtime import assistant_runtime_log_dir; print(str(assistant_runtime_log_dir()))" 2>$null | Select-Object -Last 1).Trim()
|
||||
} catch { }
|
||||
if ($null -ne $savedPyPath) {
|
||||
$env:PYTHONPATH = $savedPyPath
|
||||
} else {
|
||||
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
|
||||
}
|
||||
if (-not $dir) {
|
||||
$dir = Join-Path $RepoRoot "data\logs"
|
||||
}
|
||||
return $dir
|
||||
}
|
||||
|
|
@ -12,8 +12,6 @@ $desktopDir = Join-Path $repoRoot "desktop"
|
|||
$runDir = Join-Path $PSScriptRoot ".run"
|
||||
New-Item -ItemType Directory -Force -Path $runDir | Out-Null
|
||||
$pidFile = Join-Path $runDir "desktop.pid"
|
||||
$outLog = Join-Path $runDir "desktop.out.log"
|
||||
$errLog = Join-Path $runDir "desktop.err.log"
|
||||
|
||||
if (-not (Test-Path $desktopDir)) {
|
||||
Write-Host "[ERROR] desktop directory not found: $desktopDir" -ForegroundColor Red
|
||||
|
|
@ -27,6 +25,12 @@ $env:AIA_WORKSPACE_ROOT = $repoRoot
|
|||
$env:OPS_WORKSPACE_ROOT = $repoRoot
|
||||
$env:OCLAW_WORKSPACE = $repoRoot
|
||||
|
||||
. (Join-Path $PSScriptRoot "lib\ResolveRuntimeLogDir.ps1")
|
||||
$runtimeLogDir = Get-OclawRuntimeLogDir -RepoRoot $repoRoot
|
||||
$env:AIA_RUNTIME_LOG_DIR = $runtimeLogDir
|
||||
$env:OCLAW_DESKTOP_LOG_ROOT = $runtimeLogDir
|
||||
New-Item -ItemType Directory -Force -Path $runtimeLogDir | Out-Null
|
||||
|
||||
# Do not inherit attach-mode env from a parent shell when launching standalone desktop.
|
||||
Remove-Item Env:OCLAW_DESKTOP_EMBED_BACKEND -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:OCLAW_DESKTOP_GATEWAY_BASE_URL -ErrorAction SilentlyContinue
|
||||
|
|
@ -73,7 +77,7 @@ if ($Background) {
|
|||
}
|
||||
Set-Content -Path $pidFile -Value "$($p.Id)" -Encoding ascii
|
||||
Write-Host "desktop.pid = $pidFile" -ForegroundColor DarkGray
|
||||
Write-Host "Embedded backend/chat logs (Electron): see %APPDATA%\oclaw\logs\desktop.log / backend.log" -ForegroundColor DarkGray
|
||||
Write-Host "Desktop / embedded gateway file logs: $runtimeLogDir\desktop.log , backend.log , channel-wecom.log" -ForegroundColor DarkGray
|
||||
Write-Host "PID = $($p.Id)" -ForegroundColor Green
|
||||
Write-Host "==> Desktop launched in background (this script exits now)." -ForegroundColor Cyan
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -77,14 +77,8 @@ if (-not $SkipInstall) {
|
|||
Write-Step "Skip dependency install"
|
||||
}
|
||||
|
||||
# Same directory as runtime.operations.runtime.start_service (db_path parent / logs, or AIA_RUNTIME_LOG_DIR).
|
||||
$logDir = $null
|
||||
try {
|
||||
$logDir = (& $pythonExe -c "from runtime.operations.runtime import assistant_runtime_log_dir; print(str(assistant_runtime_log_dir()))" 2>$null | Select-Object -Last 1).Trim()
|
||||
} catch { }
|
||||
if (-not $logDir) {
|
||||
$logDir = Join-Path $repoRoot "data\logs"
|
||||
}
|
||||
. (Join-Path $PSScriptRoot "lib\ResolveRuntimeLogDir.ps1")
|
||||
$logDir = Get-OclawRuntimeLogDir -RepoRoot $repoRoot
|
||||
Write-Step "Runtime log dir: $logDir"
|
||||
Write-Host " (stack up / start_service: gateway.err.log + gateway.out.log here)" -ForegroundColor DarkGray
|
||||
Write-Host " (this script foreground: gateway.foreground.log when log mirror on)" -ForegroundColor DarkGray
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ $repoRoot = Resolve-RepoRoot
|
|||
$runDir = Join-Path $PSScriptRoot ".run"
|
||||
$null = New-Item -ItemType Directory -Force -Path $runDir -ErrorAction SilentlyContinue
|
||||
$pidFile = Join-Path $runDir "wiki_worker.pid"
|
||||
$outLog = Join-Path $runDir "wiki_worker.out.log"
|
||||
$errLog = Join-Path $runDir "wiki_worker.err.log"
|
||||
|
||||
function Test-AlivePid([int]$procId) {
|
||||
try {
|
||||
|
|
@ -51,6 +49,12 @@ $env:AIA_WORKSPACE_ROOT = $repoRoot
|
|||
$env:OPS_WORKSPACE_ROOT = $repoRoot
|
||||
$env:OCLAW_WORKSPACE = $repoRoot
|
||||
|
||||
. (Join-Path $PSScriptRoot "lib\ResolveRuntimeLogDir.ps1")
|
||||
$logDir = Get-OclawRuntimeLogDir -RepoRoot $repoRoot
|
||||
$null = New-Item -ItemType Directory -Force -Path $logDir -ErrorAction SilentlyContinue
|
||||
$outLog = Join-Path $logDir "wiki_worker.out.log"
|
||||
$errLog = Join-Path $logDir "wiki_worker.err.log"
|
||||
|
||||
if (-not $Background) {
|
||||
& $pythonExe -m runtime.workers.wiki.main
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -6,10 +6,16 @@ function Warn([string]$msg) {
|
|||
Write-Host "[WARN] $msg" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
|
||||
$env:PYTHONPATH = $repoRoot
|
||||
. (Join-Path $PSScriptRoot "lib\ResolveRuntimeLogDir.ps1")
|
||||
$logDir = Get-OclawRuntimeLogDir -RepoRoot $repoRoot
|
||||
|
||||
$runDir = Join-Path $PSScriptRoot ".run"
|
||||
$pidFile = Join-Path $runDir "desktop.pid"
|
||||
$outLog = Join-Path $runDir "desktop.out.log"
|
||||
$errLog = Join-Path $runDir "desktop.err.log"
|
||||
$desktopLog = Join-Path $logDir "desktop.log"
|
||||
$backendLog = Join-Path $logDir "backend.log"
|
||||
$channelWecomLog = Join-Path $logDir "channel-wecom.log"
|
||||
|
||||
if (-not (Test-Path $pidFile)) {
|
||||
Warn "desktop.pid not found: $pidFile"
|
||||
|
|
@ -29,8 +35,9 @@ try {
|
|||
Write-Host "desktop_running=1" -ForegroundColor Green
|
||||
Write-Host "pid=$procId"
|
||||
Write-Host "name=$($p.ProcessName)"
|
||||
if (Test-Path $outLog) { Write-Host "out_log=$outLog" }
|
||||
if (Test-Path $errLog) { Write-Host "err_log=$errLog" }
|
||||
if (Test-Path $desktopLog) { Write-Host "desktop_log=$desktopLog" }
|
||||
if (Test-Path $backendLog) { Write-Host "backend_log=$backendLog" }
|
||||
if (Test-Path $channelWecomLog) { Write-Host "channel_wecom_log=$channelWecomLog" }
|
||||
exit 0
|
||||
} catch {
|
||||
Warn "desktop process not found: PID=$procId"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ function Sync-WeixinBridgeRunners {
|
|||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $sidecarRoot | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $sidecarRoot "logs") | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $stateDir | Out-Null
|
||||
|
||||
function Ensure-OfficialPluginRuntimeDeps {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ function Resolve-RepoRoot {
|
|||
$oclawRoot = Resolve-RepoRoot
|
||||
$sidecarRoot = Join-Path $oclawRoot "data\\channel_sidecar\\$ChannelId"
|
||||
$stateDir = Join-Path $sidecarRoot "state"
|
||||
$logDir = Join-Path $sidecarRoot "logs"
|
||||
$pidFile = Join-Path $sidecarRoot "pid.txt"
|
||||
$bridgeSrc = Join-Path $oclawRoot "runtime\\operations\\weixin_bridge"
|
||||
$pluginRoot = Join-Path $env:USERPROFILE ".openclaw\\extensions\\openclaw-weixin"
|
||||
|
||||
$env:PYTHONPATH = $oclawRoot
|
||||
. (Join-Path $PSScriptRoot "lib\ResolveRuntimeLogDir.ps1")
|
||||
$runtimeLogDir = Get-OclawRuntimeLogDir -RepoRoot $oclawRoot
|
||||
|
||||
function Get-SidecarProcesses {
|
||||
$escapedSidecarRoot = $sidecarRoot.Replace("\", "\\")
|
||||
$patterns = @(
|
||||
|
|
@ -74,8 +77,8 @@ function Ensure-OfficialPluginRuntimeDeps {
|
|||
if (-not (Test-Path $sidecarRoot)) {
|
||||
New-Item -ItemType Directory -Force -Path $sidecarRoot | Out-Null
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $stateDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $runtimeLogDir | Out-Null
|
||||
|
||||
$cleaned = Stop-SidecarProcesses
|
||||
Remove-Item -Force $pidFile -ErrorAction SilentlyContinue
|
||||
|
|
@ -89,8 +92,8 @@ if (Test-Path $bridgeSrc) {
|
|||
}
|
||||
}
|
||||
|
||||
$logPath = Join-Path $logDir "weixin_sidecar.log"
|
||||
$errPath = Join-Path $logDir "weixin_sidecar.err.log"
|
||||
$logPath = Join-Path $runtimeLogDir "weixin_sidecar.log"
|
||||
$errPath = Join-Path $runtimeLogDir "weixin_sidecar.err.log"
|
||||
if (Test-Path (Join-Path $sidecarRoot "official_runner.ts")) {
|
||||
Ensure-OfficialPluginRuntimeDeps
|
||||
$cmd = "cmd.exe"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ function Resolve-RepoRoot {
|
|||
$oclawRoot = Resolve-RepoRoot
|
||||
$sidecarRoot = Join-Path $oclawRoot "data\\channel_sidecar\\$ChannelId"
|
||||
$stateDir = Join-Path $sidecarRoot "state"
|
||||
$logDir = Join-Path $sidecarRoot "logs"
|
||||
$bridgeSrc = Join-Path $oclawRoot "runtime\\operations\\whatsapp_bridge"
|
||||
|
||||
# Pin known-good versions to avoid breakage from upstream latest releases.
|
||||
|
|
@ -29,7 +28,6 @@ $typescriptVersion = "5.8.2"
|
|||
|
||||
New-Item -ItemType Directory -Force -Path $sidecarRoot | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $stateDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
|
||||
|
||||
Push-Location $sidecarRoot
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -14,15 +14,18 @@ function Resolve-RepoRoot {
|
|||
$oclawRoot = Resolve-RepoRoot
|
||||
$sidecarRoot = Join-Path $oclawRoot "data\\channel_sidecar\\$ChannelId"
|
||||
$stateDir = Join-Path $sidecarRoot "state"
|
||||
$logDir = Join-Path $sidecarRoot "logs"
|
||||
$sidecarPidFile = Join-Path $sidecarRoot "pid.txt"
|
||||
|
||||
$env:PYTHONPATH = $oclawRoot
|
||||
. (Join-Path $PSScriptRoot "lib\ResolveRuntimeLogDir.ps1")
|
||||
$runtimeLogDir = Get-OclawRuntimeLogDir -RepoRoot $oclawRoot
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $sidecarRoot | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $stateDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $runtimeLogDir | Out-Null
|
||||
|
||||
$logPath = Join-Path $logDir "whatsapp_sidecar.log"
|
||||
$errPath = Join-Path $logDir "whatsapp_sidecar.err.log"
|
||||
$logPath = Join-Path $runtimeLogDir "whatsapp_sidecar.log"
|
||||
$errPath = Join-Path $runtimeLogDir "whatsapp_sidecar.err.log"
|
||||
$systemNodeDir = "C:\\Program Files\\nodejs"
|
||||
if (Test-Path (Join-Path $systemNodeDir "node.exe")) {
|
||||
$env:PATH = "$systemNodeDir;$env:PATH"
|
||||
|
|
|
|||
|
|
@ -337,4 +337,9 @@ def run_worker() -> int:
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from svc.config.bootstrap_env import load_system_env
|
||||
from svc.observability.logging_setup import configure_oclaw_logging
|
||||
|
||||
load_system_env()
|
||||
configure_oclaw_logging(service_name="wiki-worker", include_uvicorn_formatters=False)
|
||||
raise SystemExit(run_worker())
|
||||
|
|
|
|||
40
svc/config/log_paths.py
Normal file
40
svc/config/log_paths.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""Runtime log root (gateway / channel workers / stack scripts).
|
||||
|
||||
Matches ``AIA_RUNTIME_LOG_DIR`` or ``<parent of assistant DB file>/logs`` — same rule as
|
||||
``runtime.operations.runtime.assistant_runtime_log_dir`` but lives under ``svc.config`` to avoid
|
||||
import cycles when HTTP or workers configure logging early.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from svc.config.paths import db_path
|
||||
|
||||
|
||||
def oclaw_log_root() -> Path:
|
||||
p = str(os.getenv("AIA_RUNTIME_LOG_DIR") or "").strip()
|
||||
if p:
|
||||
return Path(p).expanduser().resolve()
|
||||
return Path(db_path()).resolve().parent / "logs"
|
||||
|
||||
|
||||
def oclaw_hooks_log_dir(*, state_dir_if_legacy: Path) -> Path:
|
||||
"""Directory for bundled hook file logs (``command-logger``, ``boot-md``).
|
||||
|
||||
Default: ``oclaw_log_root() / "hooks"`` (same tree as gateway/channel ``start_service`` logs).
|
||||
|
||||
Set ``OCLAW_HOOK_LOG_USE_STATE_DIR=1`` to restore the legacy layout
|
||||
``state_dir_if_legacy / "logs"`` (typically ``~/.oclaw/logs``).
|
||||
"""
|
||||
raw = str(os.getenv("OCLAW_HOOK_LOG_USE_STATE_DIR") or "").strip().lower()
|
||||
if raw in ("1", "true", "yes", "on"):
|
||||
out = state_dir_if_legacy / "logs"
|
||||
else:
|
||||
out = oclaw_log_root() / "hooks"
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
return out
|
||||
|
||||
|
||||
__all__ = ["oclaw_hooks_log_dir", "oclaw_log_root"]
|
||||
9
svc/observability/__init__.py
Normal file
9
svc/observability/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"""Observability helpers (logging, metrics hooks)."""
|
||||
|
||||
from svc.observability.logging_setup import (
|
||||
build_uvicorn_logging_dict_config,
|
||||
configure_oclaw_logging,
|
||||
skip_file_logging,
|
||||
)
|
||||
|
||||
__all__ = ["build_uvicorn_logging_dict_config", "configure_oclaw_logging", "skip_file_logging"]
|
||||
193
svc/observability/logging_setup.py
Normal file
193
svc/observability/logging_setup.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
"""Central logging setup: rotating files under :func:`svc.config.log_paths.oclaw_log_root`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging.config
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from svc.config.log_paths import oclaw_log_root
|
||||
|
||||
_CONFIGURED = False
|
||||
|
||||
|
||||
def _skip_file_handlers() -> bool:
|
||||
if str(os.getenv("AIA_LOG_TO_FILE") or "").strip().lower() in ("0", "false", "no", "off"):
|
||||
return True
|
||||
if os.environ.get("PYTEST_CURRENT_TEST"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def skip_file_logging() -> bool:
|
||||
"""Whether rotating file handlers are disabled (pytest or ``AIA_LOG_TO_FILE=0``)."""
|
||||
return _skip_file_handlers()
|
||||
|
||||
|
||||
def _resolve_level_name() -> str:
|
||||
raw = (os.environ.get("OCLAW_LOG_LEVEL") or os.environ.get("AIA_LOG_LEVEL") or "INFO").strip().upper()
|
||||
if raw not in ("DEBUG", "INFO", "WARNING", "WARN", "ERROR", "CRITICAL", "FATAL"):
|
||||
return "INFO"
|
||||
if raw == "WARN":
|
||||
return "WARNING"
|
||||
if raw == "FATAL":
|
||||
return "CRITICAL"
|
||||
return raw
|
||||
|
||||
|
||||
def _int_env(name: str, default: int) -> int:
|
||||
raw = str(os.getenv(name) or "").strip()
|
||||
if not raw:
|
||||
return default
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def build_worker_logging_dict_config(
|
||||
*,
|
||||
log_root: Path | None = None,
|
||||
level: str | None = None,
|
||||
max_bytes: int | None = None,
|
||||
backup_count: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""dictConfig for non-uvicorn workers (plain :mod:`logging` formatters)."""
|
||||
root = log_root or oclaw_log_root()
|
||||
lvl = level or _resolve_level_name()
|
||||
mb = int(max_bytes if max_bytes is not None else _int_env("OCLAW_LOG_MAX_BYTES", 20_971_520))
|
||||
bc = int(backup_count if backup_count is not None else _int_env("OCLAW_LOG_BACKUP_COUNT", 5))
|
||||
app_dir = root / "app"
|
||||
app_dir.mkdir(parents=True, exist_ok=True)
|
||||
oclaw_path = app_dir / "oclaw.log"
|
||||
fmt = "%(asctime)s %(levelname)s [%(name)s] %(message)s"
|
||||
datefmt = "%Y-%m-%d %H:%M:%S"
|
||||
return {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"file": {
|
||||
"format": fmt,
|
||||
"datefmt": datefmt,
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"oclaw_file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"formatter": "file",
|
||||
"filename": str(oclaw_path),
|
||||
"maxBytes": max(256_000, mb),
|
||||
"backupCount": max(0, min(bc, 100)),
|
||||
"encoding": "utf-8",
|
||||
},
|
||||
},
|
||||
"root": {"handlers": ["oclaw_file"], "level": lvl},
|
||||
}
|
||||
|
||||
|
||||
def build_uvicorn_logging_dict_config(
|
||||
*,
|
||||
log_root: Path | None = None,
|
||||
level: str | None = None,
|
||||
max_bytes: int | None = None,
|
||||
backup_count: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""dictConfig compatible with ``uvicorn.run(log_config=...)`` (uses uvicorn formatters)."""
|
||||
root = log_root or oclaw_log_root()
|
||||
lvl = level or _resolve_level_name()
|
||||
mb = int(max_bytes if max_bytes is not None else _int_env("OCLAW_LOG_MAX_BYTES", 20_971_520))
|
||||
bc = int(backup_count if backup_count is not None else _int_env("OCLAW_LOG_BACKUP_COUNT", 5))
|
||||
app_dir = root / "app"
|
||||
app_dir.mkdir(parents=True, exist_ok=True)
|
||||
oclaw_path = app_dir / "oclaw.log"
|
||||
access_path = app_dir / "uvicorn-access.log"
|
||||
return {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": "%(levelprefix)s %(message)s",
|
||||
"use_colors": False,
|
||||
},
|
||||
"access": {
|
||||
"()": "uvicorn.logging.AccessFormatter",
|
||||
"fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"default_file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"formatter": "default",
|
||||
"filename": str(oclaw_path),
|
||||
"maxBytes": max(256_000, mb),
|
||||
"backupCount": max(0, min(bc, 100)),
|
||||
"encoding": "utf-8",
|
||||
},
|
||||
"access_file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"formatter": "access",
|
||||
"filename": str(access_path),
|
||||
"maxBytes": max(256_000, mb),
|
||||
"backupCount": max(0, min(bc, 100)),
|
||||
"encoding": "utf-8",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"uvicorn": {"handlers": ["default_file"], "level": lvl, "propagate": False},
|
||||
"uvicorn.error": {"handlers": ["default_file"], "level": lvl, "propagate": False},
|
||||
"uvicorn.access": {"handlers": ["access_file"], "level": lvl, "propagate": False},
|
||||
},
|
||||
"root": {"handlers": ["default_file"], "level": lvl},
|
||||
}
|
||||
|
||||
|
||||
def configure_oclaw_logging(
|
||||
*,
|
||||
service_name: str | None = None,
|
||||
include_uvicorn_formatters: bool = False,
|
||||
_force_file_handlers: bool = False,
|
||||
) -> None:
|
||||
"""Prepare or apply rotating file logging under the runtime log root (idempotent).
|
||||
|
||||
Skips file handlers during pytest (``PYTEST_CURRENT_TEST``) or when
|
||||
``AIA_LOG_TO_FILE=0`` (false/no/off), unless ``_force_file_handlers`` is true (tests only).
|
||||
|
||||
When ``include_uvicorn_formatters`` is true, only creates ``log_root`` / ``app``; the caller
|
||||
must pass :func:`build_uvicorn_logging_dict_config` to ``uvicorn.run(log_config=...)`` so
|
||||
``dictConfig`` runs once (avoids duplicate handlers).
|
||||
|
||||
``service_name`` is reserved for future structured fields; process/service is still visible
|
||||
in logger names and file layout (``app/oclaw.log`` per process).
|
||||
"""
|
||||
global _CONFIGURED
|
||||
_ = service_name
|
||||
if _CONFIGURED:
|
||||
return
|
||||
if _skip_file_handlers() and not _force_file_handlers:
|
||||
_CONFIGURED = True
|
||||
return
|
||||
log_root = oclaw_log_root()
|
||||
log_root.mkdir(parents=True, exist_ok=True)
|
||||
(log_root / "app").mkdir(parents=True, exist_ok=True)
|
||||
if include_uvicorn_formatters:
|
||||
_CONFIGURED = True
|
||||
return
|
||||
logging.config.dictConfig(build_worker_logging_dict_config(log_root=log_root))
|
||||
_CONFIGURED = True
|
||||
|
||||
|
||||
def reset_oclaw_logging_for_tests() -> None:
|
||||
"""Clear idempotency flag (tests only)."""
|
||||
global _CONFIGURED
|
||||
_CONFIGURED = False
|
||||
|
||||
|
||||
__all__ = [
|
||||
"build_uvicorn_logging_dict_config",
|
||||
"build_worker_logging_dict_config",
|
||||
"configure_oclaw_logging",
|
||||
"reset_oclaw_logging_for_tests",
|
||||
"skip_file_logging",
|
||||
]
|
||||
91
tests/test_logging_setup.py
Normal file
91
tests/test_logging_setup.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"""Tests for :mod:`svc.observability.logging_setup` and :mod:`svc.config.log_paths`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from svc.config import log_paths
|
||||
from svc.observability import logging_setup
|
||||
|
||||
|
||||
def test_oclaw_log_root_respects_aia_runtime_log_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("AIA_RUNTIME_LOG_DIR", str(tmp_path / "xlogs"))
|
||||
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
||||
assert log_paths.oclaw_log_root() == (tmp_path / "xlogs").resolve()
|
||||
|
||||
|
||||
def test_configure_worker_writes_rotating_log(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
log_root = tmp_path / "logs"
|
||||
monkeypatch.setenv("AIA_RUNTIME_LOG_DIR", str(log_root))
|
||||
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
||||
logging_setup.reset_oclaw_logging_for_tests()
|
||||
|
||||
logging_setup.configure_oclaw_logging(
|
||||
service_name="test",
|
||||
include_uvicorn_formatters=False,
|
||||
_force_file_handlers=True,
|
||||
)
|
||||
log = logging.getLogger("test_logging_module")
|
||||
log.warning("hello from logging setup test")
|
||||
|
||||
app_log = log_root / "app" / "oclaw.log"
|
||||
assert app_log.is_file()
|
||||
text = app_log.read_text(encoding="utf-8", errors="replace")
|
||||
assert "hello from logging setup test" in text
|
||||
|
||||
logging_setup.configure_oclaw_logging(
|
||||
service_name="test",
|
||||
include_uvicorn_formatters=False,
|
||||
_force_file_handlers=True,
|
||||
)
|
||||
handlers = [h for h in logging.root.handlers if hasattr(h, "baseFilename")]
|
||||
assert len(handlers) == 1
|
||||
|
||||
|
||||
def test_configure_uvicorn_deferred_marks_configured_without_dictconfig(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
log_root = tmp_path / "logs2"
|
||||
monkeypatch.setenv("AIA_RUNTIME_LOG_DIR", str(log_root))
|
||||
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
||||
logging_setup.reset_oclaw_logging_for_tests()
|
||||
|
||||
logging_setup.configure_oclaw_logging(
|
||||
service_name="gw",
|
||||
include_uvicorn_formatters=True,
|
||||
_force_file_handlers=True,
|
||||
)
|
||||
assert (log_root / "app").is_dir()
|
||||
assert not (log_root / "app" / "oclaw.log").exists()
|
||||
|
||||
|
||||
def test_oclaw_hooks_log_dir_defaults_under_runtime_root(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("AIA_RUNTIME_LOG_DIR", str(tmp_path / "logs"))
|
||||
monkeypatch.delenv("OCLAW_HOOK_LOG_USE_STATE_DIR", raising=False)
|
||||
state = tmp_path / "fake_state"
|
||||
state.mkdir()
|
||||
d = log_paths.oclaw_hooks_log_dir(state_dir_if_legacy=state)
|
||||
assert d == (tmp_path / "logs" / "hooks").resolve()
|
||||
assert d.is_dir()
|
||||
|
||||
|
||||
def test_oclaw_hooks_log_dir_legacy_when_env_set(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("AIA_RUNTIME_LOG_DIR", str(tmp_path / "logs"))
|
||||
monkeypatch.setenv("OCLAW_HOOK_LOG_USE_STATE_DIR", "1")
|
||||
state = tmp_path / "fake_state"
|
||||
state.mkdir()
|
||||
d = log_paths.oclaw_hooks_log_dir(state_dir_if_legacy=state)
|
||||
assert d == (state / "logs").resolve()
|
||||
Loading…
Add table
Add a link
Reference in a new issue