Skip to content

Commit 3d2d05a

Browse files
MaojiaShengopenviking
andauthored
feat: wrap log configs in LogConfig, add log rotation, fix empty file creation (#261)
- Create new LogConfig class to wrap all log-related configuration - Update OpenVikingConfig to use nested log config instead of separate fields - Add log rotation support with TimedRotatingFileHandler in logger.py - Add configure_uvicorn_logging to make uvicorn use OpenViking's logging config - Fix empty file being created in current directory by properly handling log.output=file in logging_init.py - Update examples/ov.conf.example to use new nested log config structure - Update __init__.py to export LogConfig and initialize_openviking_config Co-authored-by: openviking <openviking@example.com>
1 parent e7d3610 commit 3d2d05a

File tree

7 files changed

+166
-54
lines changed

7 files changed

+166
-54
lines changed

examples/ov.conf.example

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,14 @@
6767
"default_search_limit": 3,
6868
"enable_memory_decay": true,
6969
"memory_decay_check_interval": 3600,
70-
"log_level": "INFO",
71-
"log_format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
72-
"log_output": "stdout",
70+
"log": {
71+
"level": "INFO",
72+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
73+
"output": "stdout",
74+
"rotation": true,
75+
"rotation_days": 3,
76+
"rotation_interval": "midnight"
77+
},
7378
"parsers": {
7479
"pdf": {
7580
"strategy": "auto",

openviking/server/bootstrap.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from openviking.server.app import create_app
1111
from openviking.server.config import load_server_config
12+
from openviking_cli.utils.logger import configure_uvicorn_logging
1213

1314

1415
def main():
@@ -52,9 +53,12 @@ def main():
5253
if args.port is not None:
5354
config.port = args.port
5455

56+
# Configure logging for Uvicorn
57+
configure_uvicorn_logging()
58+
5559
# Create and run app
5660
app = create_app(config)
57-
uvicorn.run(app, host=config.host, port=config.port)
61+
uvicorn.run(app, host=config.host, port=config.port, log_config=None)
5862

5963

6064
if __name__ == "__main__":

openviking/storage/vectordb/utils/logging_init.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,21 @@ def init_cpp_logging():
5555

5656
config = get_openviking_config()
5757

58-
log_level = config.log_level.upper() if config.log_level else "INFO"
59-
log_output = config.log_output if config.log_output else "stdout"
58+
log_level = config.log.level.upper() if config.log.level else "INFO"
59+
log_output = config.log.output if config.log.output else "stdout"
60+
61+
# If log_output is "file", convert it to the actual file path
62+
if log_output == "file":
63+
from pathlib import Path
64+
65+
workspace_path = Path(config.storage.workspace).resolve()
66+
log_dir = workspace_path / "log"
67+
log_dir.mkdir(parents=True, exist_ok=True)
68+
log_output = str(log_dir / "openviking.log")
6069

6170
py_log_format = (
62-
config.log_format
63-
if config.log_format
71+
config.log.format
72+
if config.log.format
6473
else "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
6574
)
6675
spd_log_format = _convert_python_format_to_spdlog(py_log_format)

openviking_cli/utils/config/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
resolve_config_path,
1313
)
1414
from .embedding_config import EmbeddingConfig
15+
from .log_config import LogConfig
1516
from .open_viking_config import (
1617
OpenVikingConfig,
1718
OpenVikingConfigSingleton,
1819
get_openviking_config,
20+
initialize_openviking_config,
1921
is_valid_openviking_config,
2022
set_openviking_config,
2123
)
@@ -44,6 +46,7 @@
4446
"DEFAULT_OV_CONF",
4547
"DEFAULT_OVCLI_CONF",
4648
"EmbeddingConfig",
49+
"LogConfig",
4750
"OPENVIKING_CLI_CONFIG_ENV",
4851
"OPENVIKING_CONFIG_ENV",
4952
"OpenVikingConfig",
@@ -65,6 +68,7 @@
6568
"load_parser_configs_from_dict",
6669
"PARSER_CONFIG_REGISTRY",
6770
"get_openviking_config",
71+
"initialize_openviking_config",
6872
"load_json_config",
6973
"require_config",
7074
"resolve_config_path",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import Any, Dict
4+
5+
from pydantic import BaseModel, Field
6+
7+
8+
class LogConfig(BaseModel):
9+
"""Logging configuration for OpenViking."""
10+
11+
level: str = Field(
12+
default="WARNING", description="Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL"
13+
)
14+
15+
format: str = Field(
16+
default="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
17+
description="Log format string",
18+
)
19+
20+
output: str = Field(default="stdout", description="Log output: stdout, stderr, or file path")
21+
22+
rotation: bool = Field(default=True, description="Enable log file rotation")
23+
24+
rotation_days: int = Field(default=3, description="Number of days to retain rotated log files")
25+
26+
rotation_interval: str = Field(
27+
default="midnight",
28+
description="Log rotation interval: 'midnight', 'H' (hourly), 'D' (daily), 'W0'-'W6' (weekly)",
29+
)
30+
31+
@classmethod
32+
def from_dict(cls, config: Dict[str, Any]) -> "LogConfig":
33+
"""Create configuration from dictionary."""
34+
return cls(**config)
35+
36+
def to_dict(self) -> Dict[str, Any]:
37+
"""Convert configuration to dictionary."""
38+
return self.model_dump()

openviking_cli/utils/config/open_viking_config.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
resolve_config_path,
1616
)
1717
from .embedding_config import EmbeddingConfig
18+
from .log_config import LogConfig
1819
from .parser_config import (
1920
AudioConfig,
2021
CodeConfig,
@@ -115,18 +116,7 @@ class OpenVikingConfig(BaseModel):
115116
),
116117
)
117118

118-
log_level: str = Field(
119-
default="WARNING", description="Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL"
120-
)
121-
122-
log_format: str = Field(
123-
default="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
124-
description="Log format string",
125-
)
126-
127-
log_output: str = Field(
128-
default="stdout", description="Log output: stdout, stderr, or file path"
129-
)
119+
log: LogConfig = Field(default_factory=lambda: LogConfig(), description="Logging configuration")
130120

131121
model_config = {"arbitrary_types_allowed": True, "extra": "forbid"}
132122

@@ -143,14 +133,19 @@ def from_dict(cls, config: Dict[str, Any]) -> "OpenVikingConfig":
143133
parser_configs = {}
144134
if "parsers" in config_copy:
145135
parser_configs = config_copy.pop("parsers")
146-
147-
# Also check for individual parser configs at root level
148136
parser_types = ["pdf", "code", "image", "audio", "video", "markdown", "html", "text"]
149137
for parser_type in parser_types:
150138
if parser_type in config_copy:
151139
parser_configs[parser_type] = config_copy.pop(parser_type)
140+
# Handle log configuration from nested "log" section
141+
log_config_data = None
142+
if "log" in config_copy:
143+
log_config_data = config_copy.pop("log")
152144

153145
instance = cls(**config_copy)
146+
# Apply log configuration
147+
if log_config_data is not None:
148+
instance.log = LogConfig.from_dict(log_config_data)
154149

155150
# Apply parser configurations
156151
for parser_type, parser_data in parser_configs.items():

openviking_cli/utils/logger.py

Lines changed: 89 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,59 +6,116 @@
66

77
import logging
88
import sys
9-
from typing import Optional
9+
from logging.handlers import TimedRotatingFileHandler
10+
from pathlib import Path
11+
from typing import Any, Optional, Tuple
12+
13+
14+
def _load_log_config() -> Tuple[str, str, str, Optional[Any]]:
15+
config = None
16+
try:
17+
from openviking_cli.utils.config import get_openviking_config
18+
19+
config = get_openviking_config()
20+
log_level_str = config.log.level.upper()
21+
log_format = config.log.format
22+
log_output = config.log.output
23+
24+
if log_output == "file":
25+
workspace_path = Path(config.storage.workspace).resolve()
26+
log_dir = workspace_path / "log"
27+
log_dir.mkdir(parents=True, exist_ok=True)
28+
log_output = str(log_dir / "openviking.log")
29+
except Exception:
30+
log_level_str = "INFO"
31+
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
32+
log_output = "stdout"
33+
34+
return log_level_str, log_format, log_output, config
35+
36+
37+
def _create_log_handler(log_output: str, config: Optional[Any]) -> logging.Handler:
38+
# Prevent creating a file literally named "file"
39+
if log_output == "file":
40+
log_output = "stdout"
41+
42+
if log_output == "stdout":
43+
return logging.StreamHandler(sys.stdout)
44+
elif log_output == "stderr":
45+
return logging.StreamHandler(sys.stderr)
46+
else:
47+
if config is not None:
48+
try:
49+
log_rotation = config.log.rotation
50+
if log_rotation:
51+
log_rotation_days = config.log.rotation_days
52+
log_rotation_interval = config.log.rotation_interval
53+
54+
if log_rotation_interval == "midnight":
55+
when = "midnight"
56+
interval = 1
57+
else:
58+
when = log_rotation_interval
59+
interval = 1
60+
61+
return TimedRotatingFileHandler(
62+
log_output,
63+
when=when,
64+
interval=interval,
65+
backupCount=log_rotation_days,
66+
encoding="utf-8",
67+
)
68+
else:
69+
return logging.FileHandler(log_output, encoding="utf-8")
70+
except Exception:
71+
return logging.FileHandler(log_output, encoding="utf-8")
72+
else:
73+
return logging.FileHandler(log_output, encoding="utf-8")
1074

1175

1276
def get_logger(
1377
name: str = "openviking",
1478
format_string: Optional[str] = None,
1579
) -> logging.Logger:
16-
"""
17-
Get a configured logger.
18-
19-
Args:
20-
name: Logger name
21-
format_string: Custom format string (overrides config)
22-
23-
Returns:
24-
Configured logger
25-
"""
2680
logger = logging.getLogger(name)
2781

2882
if not logger.handlers:
29-
try:
30-
from openviking_cli.utils.config import get_openviking_config
31-
32-
config = get_openviking_config()
33-
log_level_str = config.log_level.upper()
34-
log_format = config.log_format
35-
log_output = config.log_output
36-
except Exception:
37-
log_level_str = "INFO"
38-
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
39-
log_output = "stdout"
40-
83+
log_level_str, log_format, log_output, config = _load_log_config()
4184
level = getattr(logging, log_level_str, logging.INFO)
42-
43-
if log_output == "stdout":
44-
handler = logging.StreamHandler(sys.stdout)
45-
elif log_output == "stderr":
46-
handler = logging.StreamHandler(sys.stderr)
47-
else:
48-
handler = logging.FileHandler(log_output)
85+
handler = _create_log_handler(log_output, config)
4986

5087
if format_string is None:
5188
format_string = log_format
52-
5389
formatter = logging.Formatter(format_string)
5490
handler.setFormatter(formatter)
5591
logger.addHandler(handler)
5692
logger.propagate = False
57-
5893
logger.setLevel(level)
5994

6095
return logger
6196

6297

6398
# Default logger instance
6499
default_logger = get_logger()
100+
101+
102+
def configure_uvicorn_logging() -> None:
103+
"""Configure Uvicorn loggers to use OpenViking's logging configuration.
104+
105+
This function configures the 'uvicorn', 'uvicorn.error', and 'uvicorn.access'
106+
loggers to use the same handlers and format as our openviking loggers.
107+
"""
108+
log_level_str, log_format, log_output, config = _load_log_config()
109+
level = getattr(logging, log_level_str, logging.INFO)
110+
handler = _create_log_handler(log_output, config)
111+
formatter = logging.Formatter(log_format)
112+
handler.setFormatter(formatter)
113+
114+
# Configure all Uvicorn loggers
115+
uvicorn_logger_names = ["uvicorn", "uvicorn.error", "uvicorn.access"]
116+
for logger_name in uvicorn_logger_names:
117+
logger = logging.getLogger(logger_name)
118+
logger.handlers.clear()
119+
logger.addHandler(handler)
120+
logger.setLevel(level)
121+
logger.propagate = False

0 commit comments

Comments
 (0)