Skip to content

Commit 200387a

Browse files
committed
Rewrite logs.py for the RavenDB 7.x logging schema
RavenDB 7.0 replaced the LogMode (None/Operations/Information) model with an NLog-style log-level model, so GetLogsConfigurationResult.from_json raised KeyError('CurrentMode') on 7.x. Rewrite serverwide/operations/logs.py to the 7.x /admin/logs/configuration schema (verified against a live 7.2.5 server and the ravendb/ravendb v7.2 client source): - Replace LogMode with LogLevel (Trace/Debug/Info/Warn/Error/Fatal/Off); add LogFilter + LogFilterAction. - GetLogsConfigurationResult -> {logs, audit_logs, microsoft_logs, admin_logs}. - SetLogsConfigurationOperation takes one of Logs/MicrosoftLogs/AdminLogs (+ Persist), mirroring the .NET client's three-constructor shape. - Unskip + rewrite test_logs_configuration_test and test_ravenDB_11440 to the new model.
1 parent 66386e3 commit 200387a

3 files changed

Lines changed: 277 additions & 111 deletions

File tree

ravendb/serverwide/operations/logs.py

Lines changed: 241 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,196 @@
11
from __future__ import annotations
22
import json
3-
from datetime import timedelta
43
from enum import Enum
5-
from typing import Optional, Any, Dict, TYPE_CHECKING
4+
from typing import List, Optional, Any, Dict, TYPE_CHECKING
65

76
import requests
87

98
from ravendb import ServerNode
109
from ravendb.http.raven_command import RavenCommand, VoidRavenCommand
1110
from ravendb.serverwide.operations.common import ServerOperation, T, VoidServerOperation
12-
from ravendb.tools.utils import Utils
1311

1412
if TYPE_CHECKING:
1513
from ravendb.documents.conventions import DocumentConventions
1614

1715

18-
class LogMode(Enum):
19-
NONE = "None"
20-
OPERATIONS = "Operations"
21-
INFORMATION = "Information"
16+
class LogLevel(Enum):
17+
# RavenDB 7.x logging is NLog-based; the old LogMode (None/Operations/Information) is gone.
18+
TRACE = "Trace"
19+
DEBUG = "Debug"
20+
INFO = "Info"
21+
WARN = "Warn"
22+
ERROR = "Error"
23+
FATAL = "Fatal"
24+
OFF = "Off"
2225

26+
def __str__(self):
27+
return self.value
2328

24-
class GetLogsConfigurationResult:
29+
30+
class LogFilterAction(Enum):
31+
NEUTRAL = "Neutral"
32+
LOG = "Log"
33+
IGNORE = "Ignore"
34+
LOG_FINAL = "LogFinal"
35+
IGNORE_FINAL = "IgnoreFinal"
36+
37+
def __str__(self):
38+
return self.value
39+
40+
41+
class LogFilter:
42+
def __init__(
43+
self,
44+
min_level: LogLevel = None,
45+
max_level: LogLevel = None,
46+
condition: str = None,
47+
action: LogFilterAction = None,
48+
):
49+
self.min_level = min_level
50+
self.max_level = max_level
51+
self.condition = condition
52+
self.action = action
53+
54+
@classmethod
55+
def from_json(cls, json_dict: Dict[str, Any]) -> LogFilter:
56+
return cls(
57+
LogLevel(json_dict["MinLevel"]),
58+
LogLevel(json_dict["MaxLevel"]),
59+
json_dict.get("Condition"),
60+
LogFilterAction(json_dict["Action"]),
61+
)
62+
63+
def to_json(self) -> Dict[str, Any]:
64+
return {
65+
"MinLevel": self.min_level.value,
66+
"MaxLevel": self.max_level.value,
67+
"Condition": self.condition,
68+
"Action": self.action.value,
69+
}
70+
71+
72+
# ---- GET /admin/logs/configuration result sub-objects ----
73+
class LogsConfiguration:
74+
def __init__(
75+
self,
76+
path: str = None,
77+
current_min_level: LogLevel = None,
78+
current_filters: List[LogFilter] = None,
79+
current_log_filter_default_action: LogFilterAction = None,
80+
min_level: LogLevel = None,
81+
archive_above_size_in_mb: int = None,
82+
max_archive_days: int = None,
83+
max_archive_files: int = None,
84+
enable_archive_file_compression: bool = None,
85+
):
86+
self.path = path
87+
self.current_min_level = current_min_level
88+
self.current_filters = current_filters if current_filters is not None else []
89+
self.current_log_filter_default_action = current_log_filter_default_action
90+
self.min_level = min_level
91+
self.archive_above_size_in_mb = archive_above_size_in_mb
92+
self.max_archive_days = max_archive_days
93+
self.max_archive_files = max_archive_files
94+
self.enable_archive_file_compression = enable_archive_file_compression
95+
96+
@classmethod
97+
def from_json(cls, json_dict: Dict[str, Any]) -> LogsConfiguration:
98+
return cls(
99+
path=json_dict.get("Path"),
100+
current_min_level=LogLevel(json_dict["CurrentMinLevel"]),
101+
current_filters=[LogFilter.from_json(f) for f in (json_dict.get("CurrentFilters") or [])],
102+
current_log_filter_default_action=LogFilterAction(json_dict["CurrentLogFilterDefaultAction"]),
103+
min_level=LogLevel(json_dict["MinLevel"]),
104+
archive_above_size_in_mb=json_dict.get("ArchiveAboveSizeInMb"),
105+
max_archive_days=json_dict.get("MaxArchiveDays"),
106+
max_archive_files=json_dict.get("MaxArchiveFiles"),
107+
enable_archive_file_compression=json_dict.get("EnableArchiveFileCompression"),
108+
)
109+
110+
111+
class AuditLogsConfiguration:
25112
def __init__(
26113
self,
27-
current_mode: LogMode = None,
28-
mode: LogMode = None,
29114
path: str = None,
30-
use_utc_time: bool = None,
31-
retention_time: timedelta = None,
32-
retention_size: int = None,
33-
compress: bool = None,
115+
level: LogLevel = None,
116+
archive_above_size_in_mb: int = None,
117+
max_archive_days: int = None,
118+
max_archive_files: int = None,
119+
enable_archive_file_compression: bool = None,
34120
):
35-
self.current_mode = current_mode
36-
self.mode = mode
37121
self.path = path
38-
self.use_utc_time = use_utc_time
39-
self.retention_time = retention_time
40-
self.retention_size = retention_size
41-
self.compress = compress
122+
self.level = level
123+
self.archive_above_size_in_mb = archive_above_size_in_mb
124+
self.max_archive_days = max_archive_days
125+
self.max_archive_files = max_archive_files
126+
self.enable_archive_file_compression = enable_archive_file_compression
127+
128+
@classmethod
129+
def from_json(cls, json_dict: Dict[str, Any]) -> AuditLogsConfiguration:
130+
return cls(
131+
path=json_dict.get("Path"),
132+
level=LogLevel(json_dict["Level"]),
133+
archive_above_size_in_mb=json_dict.get("ArchiveAboveSizeInMb"),
134+
max_archive_days=json_dict.get("MaxArchiveDays"),
135+
max_archive_files=json_dict.get("MaxArchiveFiles"),
136+
enable_archive_file_compression=json_dict.get("EnableArchiveFileCompression"),
137+
)
138+
139+
140+
class MicrosoftLogsConfiguration:
141+
def __init__(self, current_min_level: LogLevel = None, min_level: LogLevel = None):
142+
self.current_min_level = current_min_level
143+
self.min_level = min_level
144+
145+
@classmethod
146+
def from_json(cls, json_dict: Dict[str, Any]) -> MicrosoftLogsConfiguration:
147+
return cls(LogLevel(json_dict["CurrentMinLevel"]), LogLevel(json_dict["MinLevel"]))
148+
149+
150+
class AdminLogsConfiguration:
151+
def __init__(
152+
self,
153+
current_min_level: LogLevel = None,
154+
current_filters: List[LogFilter] = None,
155+
current_log_filter_default_action: LogFilterAction = None,
156+
):
157+
self.current_min_level = current_min_level
158+
self.current_filters = current_filters if current_filters is not None else []
159+
self.current_log_filter_default_action = current_log_filter_default_action
160+
161+
@classmethod
162+
def from_json(cls, json_dict: Dict[str, Any]) -> AdminLogsConfiguration:
163+
return cls(
164+
LogLevel(json_dict["CurrentMinLevel"]),
165+
[LogFilter.from_json(f) for f in (json_dict.get("CurrentFilters") or [])],
166+
LogFilterAction(json_dict["CurrentLogFilterDefaultAction"]),
167+
)
168+
169+
170+
class GetLogsConfigurationResult:
171+
def __init__(
172+
self,
173+
logs: LogsConfiguration = None,
174+
audit_logs: AuditLogsConfiguration = None,
175+
microsoft_logs: MicrosoftLogsConfiguration = None,
176+
admin_logs: AdminLogsConfiguration = None,
177+
):
178+
self.logs = logs
179+
self.audit_logs = audit_logs
180+
self.microsoft_logs = microsoft_logs
181+
self.admin_logs = admin_logs
42182

43183
@classmethod
44184
def from_json(cls, json_dict: Dict[str, Any]) -> GetLogsConfigurationResult:
45185
return cls(
46-
LogMode(json_dict["CurrentMode"]),
47-
LogMode(json_dict["Mode"]),
48-
json_dict["Path"],
49-
json_dict["UseUtcTime"],
50-
Utils.string_to_timedelta(json_dict["RetentionTime"]),
51-
json_dict["RetentionSize"],
52-
json_dict["Compress"],
186+
LogsConfiguration.from_json(json_dict["Logs"]) if json_dict.get("Logs") else None,
187+
AuditLogsConfiguration.from_json(json_dict["AuditLogs"]) if json_dict.get("AuditLogs") else None,
188+
(
189+
MicrosoftLogsConfiguration.from_json(json_dict["MicrosoftLogs"])
190+
if json_dict.get("MicrosoftLogs")
191+
else None
192+
),
193+
AdminLogsConfiguration.from_json(json_dict["AdminLogs"]) if json_dict.get("AdminLogs") else None,
53194
)
54195

55196

@@ -62,7 +203,7 @@ def __init__(self):
62203
super().__init__(GetLogsConfigurationResult)
63204

64205
def is_read_request(self) -> bool:
65-
return False
206+
return True
66207

67208
def create_request(self, node: ServerNode) -> requests.Request:
68209
return requests.Request("GET", f"{node.url}/admin/logs/configuration")
@@ -75,34 +216,84 @@ def set_response(self, response: Optional[str], from_cache: bool) -> None:
75216

76217

77218
class SetLogsConfigurationOperation(VoidServerOperation):
219+
# Mirrors the .NET client: one call sets exactly one of Logs / MicrosoftLogs / AdminLogs (+ Persist).
220+
class LogsConfiguration:
221+
def __init__(
222+
self,
223+
min_level: LogLevel,
224+
filters: List[LogFilter] = None,
225+
log_filter_default_action: LogFilterAction = None,
226+
):
227+
self.min_level = min_level
228+
self.filters = filters if filters is not None else []
229+
self.log_filter_default_action = log_filter_default_action
230+
231+
def to_json(self) -> Dict[str, Any]:
232+
result = {"MinLevel": self.min_level.value, "Filters": [f.to_json() for f in self.filters]}
233+
if self.log_filter_default_action is not None:
234+
result["LogFilterDefaultAction"] = self.log_filter_default_action.value
235+
return result
236+
237+
class MicrosoftLogsConfiguration:
238+
def __init__(self, min_level: LogLevel):
239+
self.min_level = min_level
240+
241+
def to_json(self) -> Dict[str, Any]:
242+
return {"MinLevel": self.min_level.value}
243+
244+
class AdminLogsConfiguration:
245+
def __init__(
246+
self,
247+
min_level: LogLevel,
248+
filters: List[LogFilter] = None,
249+
log_filter_default_action: LogFilterAction = None,
250+
):
251+
self.min_level = min_level
252+
self.filters = filters if filters is not None else []
253+
self.log_filter_default_action = log_filter_default_action
254+
255+
def to_json(self) -> Dict[str, Any]:
256+
result = {"MinLevel": self.min_level.value, "Filters": [f.to_json() for f in self.filters]}
257+
if self.log_filter_default_action is not None:
258+
result["LogFilterDefaultAction"] = self.log_filter_default_action.value
259+
return result
260+
78261
class Parameters:
79262
def __init__(
80263
self,
81-
mode: LogMode = None,
82-
retention_time: timedelta = None,
83-
retention_size: int = None,
84-
compress: bool = None,
264+
logs: "SetLogsConfigurationOperation.LogsConfiguration" = None,
265+
microsoft_logs: "SetLogsConfigurationOperation.MicrosoftLogsConfiguration" = None,
266+
admin_logs: "SetLogsConfigurationOperation.AdminLogsConfiguration" = None,
267+
persist: bool = False,
85268
) -> None:
86-
self.mode = mode
87-
self.retention_time = retention_time
88-
self.retention_size = retention_size
89-
self.compress = compress
90-
91-
@classmethod
92-
def from_get_logs(cls, get_logs: GetLogsConfigurationResult) -> "SetLogsConfigurationOperation.Parameters":
93-
return cls(get_logs.mode, get_logs.retention_time, get_logs.retention_size, get_logs.compress)
269+
self.logs = logs
270+
self.microsoft_logs = microsoft_logs
271+
self.admin_logs = admin_logs
272+
self.persist = persist
94273

95274
def to_json(self) -> Dict[str, Any]:
96-
return {
97-
"Mode": self.mode.value,
98-
"RetentionTime": Utils.timedelta_to_str(self.retention_time),
99-
"RetentionSize": self.retention_size,
100-
"Compress": self.compress,
101-
}
275+
result: Dict[str, Any] = {"Persist": self.persist}
276+
if self.logs is not None:
277+
result["Logs"] = self.logs.to_json()
278+
if self.microsoft_logs is not None:
279+
result["MicrosoftLogs"] = self.microsoft_logs.to_json()
280+
if self.admin_logs is not None:
281+
result["AdminLogs"] = self.admin_logs.to_json()
282+
return result
283+
284+
def __init__(self, configuration, persist: bool = False) -> None:
285+
if configuration is None:
286+
raise ValueError("Configuration cannot be None")
102287

103-
def __init__(self, parameters: Parameters) -> None:
104-
if parameters is None:
105-
raise ValueError("Parameters cannot be None")
288+
parameters = SetLogsConfigurationOperation.Parameters(persist=persist)
289+
if isinstance(configuration, SetLogsConfigurationOperation.LogsConfiguration):
290+
parameters.logs = configuration
291+
elif isinstance(configuration, SetLogsConfigurationOperation.MicrosoftLogsConfiguration):
292+
parameters.microsoft_logs = configuration
293+
elif isinstance(configuration, SetLogsConfigurationOperation.AdminLogsConfiguration):
294+
parameters.admin_logs = configuration
295+
else:
296+
raise TypeError("Unsupported configuration type: " + type(configuration).__name__)
106297

107298
self._parameters = parameters
108299

@@ -112,7 +303,7 @@ def get_command(self, conventions: "DocumentConventions") -> "VoidRavenCommand":
112303
class SetLogsConfigurationCommand(VoidRavenCommand):
113304
def __init__(self, parameters: "SetLogsConfigurationOperation.Parameters"):
114305
if parameters is None:
115-
raise ValueError("Parameters must be None")
306+
raise ValueError("Parameters cannot be None")
116307

117308
super().__init__()
118309
self._parameters = parameters

0 commit comments

Comments
 (0)