kstlib 0.0.1a0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kstlib/__init__.py +266 -1
- kstlib/__main__.py +16 -0
- kstlib/alerts/__init__.py +110 -0
- kstlib/alerts/channels/__init__.py +36 -0
- kstlib/alerts/channels/base.py +197 -0
- kstlib/alerts/channels/email.py +227 -0
- kstlib/alerts/channels/slack.py +389 -0
- kstlib/alerts/exceptions.py +72 -0
- kstlib/alerts/manager.py +651 -0
- kstlib/alerts/models.py +142 -0
- kstlib/alerts/throttle.py +263 -0
- kstlib/auth/__init__.py +139 -0
- kstlib/auth/callback.py +399 -0
- kstlib/auth/config.py +502 -0
- kstlib/auth/errors.py +127 -0
- kstlib/auth/models.py +316 -0
- kstlib/auth/providers/__init__.py +14 -0
- kstlib/auth/providers/base.py +393 -0
- kstlib/auth/providers/oauth2.py +645 -0
- kstlib/auth/providers/oidc.py +821 -0
- kstlib/auth/session.py +338 -0
- kstlib/auth/token.py +482 -0
- kstlib/cache/__init__.py +50 -0
- kstlib/cache/decorator.py +261 -0
- kstlib/cache/strategies.py +516 -0
- kstlib/cli/__init__.py +8 -0
- kstlib/cli/app.py +195 -0
- kstlib/cli/commands/__init__.py +5 -0
- kstlib/cli/commands/auth/__init__.py +39 -0
- kstlib/cli/commands/auth/common.py +122 -0
- kstlib/cli/commands/auth/login.py +325 -0
- kstlib/cli/commands/auth/logout.py +74 -0
- kstlib/cli/commands/auth/providers.py +57 -0
- kstlib/cli/commands/auth/status.py +291 -0
- kstlib/cli/commands/auth/token.py +199 -0
- kstlib/cli/commands/auth/whoami.py +106 -0
- kstlib/cli/commands/config.py +89 -0
- kstlib/cli/commands/ops/__init__.py +39 -0
- kstlib/cli/commands/ops/attach.py +49 -0
- kstlib/cli/commands/ops/common.py +269 -0
- kstlib/cli/commands/ops/list_sessions.py +252 -0
- kstlib/cli/commands/ops/logs.py +49 -0
- kstlib/cli/commands/ops/start.py +98 -0
- kstlib/cli/commands/ops/status.py +138 -0
- kstlib/cli/commands/ops/stop.py +60 -0
- kstlib/cli/commands/rapi/__init__.py +60 -0
- kstlib/cli/commands/rapi/call.py +341 -0
- kstlib/cli/commands/rapi/list.py +99 -0
- kstlib/cli/commands/rapi/show.py +206 -0
- kstlib/cli/commands/secrets/__init__.py +35 -0
- kstlib/cli/commands/secrets/common.py +425 -0
- kstlib/cli/commands/secrets/decrypt.py +88 -0
- kstlib/cli/commands/secrets/doctor.py +743 -0
- kstlib/cli/commands/secrets/encrypt.py +242 -0
- kstlib/cli/commands/secrets/shred.py +96 -0
- kstlib/cli/common.py +86 -0
- kstlib/config/__init__.py +76 -0
- kstlib/config/exceptions.py +110 -0
- kstlib/config/export.py +225 -0
- kstlib/config/loader.py +963 -0
- kstlib/config/sops.py +287 -0
- kstlib/db/__init__.py +54 -0
- kstlib/db/aiosqlcipher.py +137 -0
- kstlib/db/cipher.py +112 -0
- kstlib/db/database.py +367 -0
- kstlib/db/exceptions.py +25 -0
- kstlib/db/pool.py +302 -0
- kstlib/helpers/__init__.py +35 -0
- kstlib/helpers/exceptions.py +11 -0
- kstlib/helpers/time_trigger.py +396 -0
- kstlib/kstlib.conf.yml +890 -0
- kstlib/limits.py +963 -0
- kstlib/logging/__init__.py +108 -0
- kstlib/logging/manager.py +633 -0
- kstlib/mail/__init__.py +42 -0
- kstlib/mail/builder.py +626 -0
- kstlib/mail/exceptions.py +27 -0
- kstlib/mail/filesystem.py +248 -0
- kstlib/mail/transport.py +224 -0
- kstlib/mail/transports/__init__.py +19 -0
- kstlib/mail/transports/gmail.py +268 -0
- kstlib/mail/transports/resend.py +324 -0
- kstlib/mail/transports/smtp.py +326 -0
- kstlib/meta.py +72 -0
- kstlib/metrics/__init__.py +88 -0
- kstlib/metrics/decorators.py +1090 -0
- kstlib/metrics/exceptions.py +14 -0
- kstlib/monitoring/__init__.py +116 -0
- kstlib/monitoring/_styles.py +163 -0
- kstlib/monitoring/cell.py +57 -0
- kstlib/monitoring/config.py +424 -0
- kstlib/monitoring/delivery.py +579 -0
- kstlib/monitoring/exceptions.py +63 -0
- kstlib/monitoring/image.py +220 -0
- kstlib/monitoring/kv.py +79 -0
- kstlib/monitoring/list.py +69 -0
- kstlib/monitoring/metric.py +88 -0
- kstlib/monitoring/monitoring.py +341 -0
- kstlib/monitoring/renderer.py +139 -0
- kstlib/monitoring/service.py +392 -0
- kstlib/monitoring/table.py +129 -0
- kstlib/monitoring/types.py +56 -0
- kstlib/ops/__init__.py +86 -0
- kstlib/ops/base.py +148 -0
- kstlib/ops/container.py +577 -0
- kstlib/ops/exceptions.py +209 -0
- kstlib/ops/manager.py +407 -0
- kstlib/ops/models.py +176 -0
- kstlib/ops/tmux.py +372 -0
- kstlib/ops/validators.py +287 -0
- kstlib/py.typed +0 -0
- kstlib/rapi/__init__.py +118 -0
- kstlib/rapi/client.py +875 -0
- kstlib/rapi/config.py +861 -0
- kstlib/rapi/credentials.py +887 -0
- kstlib/rapi/exceptions.py +213 -0
- kstlib/resilience/__init__.py +101 -0
- kstlib/resilience/circuit_breaker.py +440 -0
- kstlib/resilience/exceptions.py +95 -0
- kstlib/resilience/heartbeat.py +491 -0
- kstlib/resilience/rate_limiter.py +506 -0
- kstlib/resilience/shutdown.py +417 -0
- kstlib/resilience/watchdog.py +637 -0
- kstlib/secrets/__init__.py +29 -0
- kstlib/secrets/exceptions.py +19 -0
- kstlib/secrets/models.py +62 -0
- kstlib/secrets/providers/__init__.py +79 -0
- kstlib/secrets/providers/base.py +58 -0
- kstlib/secrets/providers/environment.py +66 -0
- kstlib/secrets/providers/keyring.py +107 -0
- kstlib/secrets/providers/kms.py +223 -0
- kstlib/secrets/providers/kwargs.py +101 -0
- kstlib/secrets/providers/sops.py +209 -0
- kstlib/secrets/resolver.py +221 -0
- kstlib/secrets/sensitive.py +130 -0
- kstlib/secure/__init__.py +23 -0
- kstlib/secure/fs.py +194 -0
- kstlib/secure/permissions.py +70 -0
- kstlib/ssl.py +347 -0
- kstlib/ui/__init__.py +23 -0
- kstlib/ui/exceptions.py +26 -0
- kstlib/ui/panels.py +484 -0
- kstlib/ui/spinner.py +864 -0
- kstlib/ui/tables.py +382 -0
- kstlib/utils/__init__.py +48 -0
- kstlib/utils/dict.py +36 -0
- kstlib/utils/formatting.py +338 -0
- kstlib/utils/http_trace.py +237 -0
- kstlib/utils/lazy.py +49 -0
- kstlib/utils/secure_delete.py +205 -0
- kstlib/utils/serialization.py +247 -0
- kstlib/utils/text.py +56 -0
- kstlib/utils/validators.py +124 -0
- kstlib/websocket/__init__.py +97 -0
- kstlib/websocket/exceptions.py +214 -0
- kstlib/websocket/manager.py +1102 -0
- kstlib/websocket/models.py +361 -0
- kstlib-1.0.0.dist-info/METADATA +201 -0
- kstlib-1.0.0.dist-info/RECORD +163 -0
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.0.dist-info}/WHEEL +1 -1
- kstlib-1.0.0.dist-info/entry_points.txt +2 -0
- kstlib-1.0.0.dist-info/licenses/LICENSE.md +9 -0
- kstlib-0.0.1a0.dist-info/METADATA +0 -29
- kstlib-0.0.1a0.dist-info/RECORD +0 -6
- kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""Configuration loader for monitoring dashboards.
|
|
2
|
+
|
|
3
|
+
This module provides tools to load monitoring configurations from YAML files,
|
|
4
|
+
with auto-discovery of ``*.monitor.yml`` files in specified directories.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Load a single monitoring config:
|
|
8
|
+
|
|
9
|
+
>>> from kstlib.monitoring.config import load_monitoring_config
|
|
10
|
+
>>> config = load_monitoring_config("dashboard.monitor.yml") # doctest: +SKIP
|
|
11
|
+
>>> service = config.to_service() # doctest: +SKIP
|
|
12
|
+
|
|
13
|
+
Discover all monitoring configs in a directory:
|
|
14
|
+
|
|
15
|
+
>>> from kstlib.monitoring.config import discover_monitoring_configs
|
|
16
|
+
>>> configs = discover_monitoring_configs("./configs") # doctest: +SKIP
|
|
17
|
+
>>> for name, config in configs.items(): # doctest: +SKIP
|
|
18
|
+
... print(f"Found: {name}") # doctest: +SKIP
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import importlib
|
|
24
|
+
import pathlib
|
|
25
|
+
import re
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import yaml
|
|
30
|
+
|
|
31
|
+
from kstlib.monitoring.exceptions import MonitoringConfigError
|
|
32
|
+
from kstlib.monitoring.service import Collector, MonitoringService
|
|
33
|
+
|
|
34
|
+
# File pattern for monitoring configs
|
|
35
|
+
MONITORING_CONFIG_PATTERN = "*.monitor.yml"
|
|
36
|
+
MONITORING_CONFIG_SUFFIX = ".monitor.yml"
|
|
37
|
+
|
|
38
|
+
# Deep defense: Security limits
|
|
39
|
+
MAX_CONFIG_FILE_SIZE = 1024 * 1024 # 1 MB max config file size
|
|
40
|
+
MAX_COLLECTORS = 100 # Maximum number of collectors per config
|
|
41
|
+
MAX_NAME_LENGTH = 128 # Maximum length for names (config name, collector names)
|
|
42
|
+
MAX_TEMPLATE_SIZE = 512 * 1024 # 512 KB max template size
|
|
43
|
+
|
|
44
|
+
# Module import restrictions for callable collectors
|
|
45
|
+
BLOCKED_MODULE_PREFIXES = (
|
|
46
|
+
"os.",
|
|
47
|
+
"sys.",
|
|
48
|
+
"subprocess",
|
|
49
|
+
"shutil",
|
|
50
|
+
"socket",
|
|
51
|
+
"pickle",
|
|
52
|
+
"marshal",
|
|
53
|
+
"__",
|
|
54
|
+
)
|
|
55
|
+
ALLOWED_MODULE_PATTERN = r"^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MonitoringConfigFileNotFoundError(MonitoringConfigError, FileNotFoundError):
|
|
59
|
+
"""Monitoring configuration file not found."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MonitoringConfigFormatError(MonitoringConfigError, ValueError):
|
|
63
|
+
"""Invalid monitoring configuration format."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MonitoringConfigCollectorError(MonitoringConfigError):
|
|
67
|
+
"""Error loading a collector from configuration."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class CollectorConfig:
|
|
72
|
+
"""Configuration for a single collector.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
name: Name of the collector (used as template variable).
|
|
76
|
+
collector_type: Type of collector ("static", "callable", "env").
|
|
77
|
+
value: Static value (for type="static").
|
|
78
|
+
module: Module path (for type="callable").
|
|
79
|
+
function: Function name (for type="callable").
|
|
80
|
+
env_var: Environment variable name (for type="env").
|
|
81
|
+
default: Default value if env var not set (for type="env").
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
name: str
|
|
85
|
+
collector_type: str = "static"
|
|
86
|
+
value: Any = None
|
|
87
|
+
module: str | None = None
|
|
88
|
+
function: str | None = None
|
|
89
|
+
env_var: str | None = None
|
|
90
|
+
default: Any = None
|
|
91
|
+
|
|
92
|
+
def to_collector(self) -> Collector:
|
|
93
|
+
"""Convert config to a collector callable.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Callable that can be used as a MonitoringService collector.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
MonitoringConfigCollectorError: If collector cannot be created.
|
|
100
|
+
"""
|
|
101
|
+
if self.collector_type == "static":
|
|
102
|
+
return self._create_static_collector()
|
|
103
|
+
if self.collector_type == "callable":
|
|
104
|
+
return self._create_callable_collector()
|
|
105
|
+
if self.collector_type == "env":
|
|
106
|
+
return self._create_env_collector()
|
|
107
|
+
raise MonitoringConfigCollectorError(f"Unknown collector type: {self.collector_type}")
|
|
108
|
+
|
|
109
|
+
def _create_static_collector(self) -> Collector:
|
|
110
|
+
"""Create a static value collector."""
|
|
111
|
+
value = self.value
|
|
112
|
+
|
|
113
|
+
def collector() -> Any:
|
|
114
|
+
return value
|
|
115
|
+
|
|
116
|
+
return collector
|
|
117
|
+
|
|
118
|
+
def _create_callable_collector(self) -> Collector:
|
|
119
|
+
"""Create a collector from a module.function reference."""
|
|
120
|
+
if not self.module or not self.function:
|
|
121
|
+
raise MonitoringConfigCollectorError(
|
|
122
|
+
f"Collector '{self.name}' type='callable' requires 'module' and 'function'"
|
|
123
|
+
)
|
|
124
|
+
# Deep defense: Validate module name format
|
|
125
|
+
if not re.match(ALLOWED_MODULE_PATTERN, self.module):
|
|
126
|
+
raise MonitoringConfigCollectorError(f"Invalid module name format: '{self.module}'")
|
|
127
|
+
# Deep defense: Block dangerous modules
|
|
128
|
+
for prefix in BLOCKED_MODULE_PREFIXES:
|
|
129
|
+
if self.module.startswith(prefix) or self.module == prefix.rstrip("."):
|
|
130
|
+
raise MonitoringConfigCollectorError(f"Module '{self.module}' is blocked for security reasons")
|
|
131
|
+
try:
|
|
132
|
+
mod = importlib.import_module(self.module)
|
|
133
|
+
func: Collector = getattr(mod, self.function)
|
|
134
|
+
if not callable(func):
|
|
135
|
+
raise MonitoringConfigCollectorError(f"'{self.module}.{self.function}' is not callable")
|
|
136
|
+
return func
|
|
137
|
+
except ImportError as e:
|
|
138
|
+
raise MonitoringConfigCollectorError(f"Cannot import module '{self.module}': {e}") from e
|
|
139
|
+
except AttributeError as e:
|
|
140
|
+
raise MonitoringConfigCollectorError(f"Function '{self.function}' not found in '{self.module}': {e}") from e
|
|
141
|
+
|
|
142
|
+
def _create_env_collector(self) -> Collector:
|
|
143
|
+
"""Create a collector that reads from environment variable."""
|
|
144
|
+
import os
|
|
145
|
+
|
|
146
|
+
env_var = self.env_var
|
|
147
|
+
default = self.default
|
|
148
|
+
name = self.name
|
|
149
|
+
|
|
150
|
+
if not env_var:
|
|
151
|
+
raise MonitoringConfigCollectorError(f"Collector '{name}' type='env' requires 'env_var'")
|
|
152
|
+
|
|
153
|
+
def collector() -> Any:
|
|
154
|
+
return os.environ.get(env_var, default)
|
|
155
|
+
|
|
156
|
+
return collector
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class MonitoringConfig:
|
|
161
|
+
"""Parsed monitoring configuration.
|
|
162
|
+
|
|
163
|
+
Attributes:
|
|
164
|
+
name: Dashboard name (defaults to filename without extension).
|
|
165
|
+
template: Jinja2 template string for rendering.
|
|
166
|
+
collectors: List of collector configurations.
|
|
167
|
+
inline_css: Whether to use inline CSS (default True).
|
|
168
|
+
fail_fast: Whether to fail on first collector error (default True).
|
|
169
|
+
source_path: Path to the source config file (if loaded from file).
|
|
170
|
+
metadata: Additional metadata from the config file.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
name: str
|
|
174
|
+
template: str
|
|
175
|
+
collectors: list[CollectorConfig] = field(default_factory=list)
|
|
176
|
+
inline_css: bool = True
|
|
177
|
+
fail_fast: bool = True
|
|
178
|
+
source_path: pathlib.Path | None = None
|
|
179
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
180
|
+
|
|
181
|
+
def to_service(self) -> MonitoringService:
|
|
182
|
+
"""Create a MonitoringService from this configuration.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Configured MonitoringService instance.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
MonitoringConfigCollectorError: If any collector cannot be created.
|
|
189
|
+
"""
|
|
190
|
+
collectors: dict[str, Collector] = {}
|
|
191
|
+
for collector_config in self.collectors:
|
|
192
|
+
collectors[collector_config.name] = collector_config.to_collector()
|
|
193
|
+
|
|
194
|
+
return MonitoringService(
|
|
195
|
+
template=self.template,
|
|
196
|
+
collectors=collectors,
|
|
197
|
+
inline_css=self.inline_css,
|
|
198
|
+
fail_fast=self.fail_fast,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def _parse_collectors(cls, collectors_data: Any) -> list[CollectorConfig]:
|
|
203
|
+
"""Parse collectors from config data with validation."""
|
|
204
|
+
if not isinstance(collectors_data, dict):
|
|
205
|
+
raise MonitoringConfigFormatError("'collectors' must be a dictionary mapping names to collector configs")
|
|
206
|
+
|
|
207
|
+
# Deep defense: Limit number of collectors
|
|
208
|
+
if len(collectors_data) > MAX_COLLECTORS:
|
|
209
|
+
raise MonitoringConfigFormatError(f"Too many collectors ({len(collectors_data)} > {MAX_COLLECTORS})")
|
|
210
|
+
|
|
211
|
+
collectors: list[CollectorConfig] = []
|
|
212
|
+
for name, collector_data in collectors_data.items():
|
|
213
|
+
# Deep defense: Validate collector name length
|
|
214
|
+
if len(str(name)) > MAX_NAME_LENGTH:
|
|
215
|
+
raise MonitoringConfigFormatError(
|
|
216
|
+
f"Collector name '{name[:20]}...' exceeds maximum length ({MAX_NAME_LENGTH})"
|
|
217
|
+
)
|
|
218
|
+
if not isinstance(collector_data, dict):
|
|
219
|
+
# Simple static value
|
|
220
|
+
collectors.append(CollectorConfig(name=name, collector_type="static", value=collector_data))
|
|
221
|
+
else:
|
|
222
|
+
collectors.append(
|
|
223
|
+
CollectorConfig(
|
|
224
|
+
name=name,
|
|
225
|
+
collector_type=collector_data.get("type", "static"),
|
|
226
|
+
value=collector_data.get("value"),
|
|
227
|
+
module=collector_data.get("module"),
|
|
228
|
+
function=collector_data.get("function"),
|
|
229
|
+
env_var=collector_data.get("env_var"),
|
|
230
|
+
default=collector_data.get("default"),
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
return collectors
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def from_dict(
|
|
237
|
+
cls,
|
|
238
|
+
data: dict[str, Any],
|
|
239
|
+
*,
|
|
240
|
+
source_path: pathlib.Path | None = None,
|
|
241
|
+
) -> MonitoringConfig:
|
|
242
|
+
"""Create a MonitoringConfig from a dictionary.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
data: Configuration dictionary.
|
|
246
|
+
source_path: Optional path to source file.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Parsed MonitoringConfig.
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
MonitoringConfigFormatError: If required fields are missing.
|
|
253
|
+
"""
|
|
254
|
+
# Validate required fields
|
|
255
|
+
if "template" not in data:
|
|
256
|
+
raise MonitoringConfigFormatError("Monitoring config must have a 'template' field")
|
|
257
|
+
|
|
258
|
+
# Deep defense: Validate template size
|
|
259
|
+
template = data["template"]
|
|
260
|
+
if not isinstance(template, str):
|
|
261
|
+
raise MonitoringConfigFormatError("'template' must be a string")
|
|
262
|
+
if len(template) > MAX_TEMPLATE_SIZE:
|
|
263
|
+
raise MonitoringConfigFormatError(f"Template exceeds maximum size ({MAX_TEMPLATE_SIZE} bytes)")
|
|
264
|
+
|
|
265
|
+
# Deep defense: Validate config name length
|
|
266
|
+
config_name = data.get("name")
|
|
267
|
+
if config_name and len(str(config_name)) > MAX_NAME_LENGTH:
|
|
268
|
+
raise MonitoringConfigFormatError(f"Config name exceeds maximum length ({MAX_NAME_LENGTH})")
|
|
269
|
+
|
|
270
|
+
# Parse collectors
|
|
271
|
+
collectors = cls._parse_collectors(data.get("collectors", {}))
|
|
272
|
+
|
|
273
|
+
# Determine name
|
|
274
|
+
name = data.get("name")
|
|
275
|
+
if not name and source_path:
|
|
276
|
+
# Use filename without .monitor.yml suffix
|
|
277
|
+
name = source_path.name
|
|
278
|
+
name = name.removesuffix(MONITORING_CONFIG_SUFFIX)
|
|
279
|
+
|
|
280
|
+
# Extract known fields for metadata
|
|
281
|
+
known_fields = {
|
|
282
|
+
"name",
|
|
283
|
+
"template",
|
|
284
|
+
"collectors",
|
|
285
|
+
"inline_css",
|
|
286
|
+
"fail_fast",
|
|
287
|
+
}
|
|
288
|
+
metadata = {k: v for k, v in data.items() if k not in known_fields}
|
|
289
|
+
|
|
290
|
+
return cls(
|
|
291
|
+
name=name or "unnamed",
|
|
292
|
+
template=data["template"],
|
|
293
|
+
collectors=collectors,
|
|
294
|
+
inline_css=data.get("inline_css", True),
|
|
295
|
+
fail_fast=data.get("fail_fast", True),
|
|
296
|
+
source_path=source_path,
|
|
297
|
+
metadata=metadata,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def load_monitoring_config(
|
|
302
|
+
path: str | pathlib.Path,
|
|
303
|
+
*,
|
|
304
|
+
encoding: str = "utf-8",
|
|
305
|
+
) -> MonitoringConfig:
|
|
306
|
+
"""Load a monitoring configuration from a YAML file.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
path: Path to the monitoring config file.
|
|
310
|
+
encoding: File encoding (default UTF-8).
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Parsed MonitoringConfig.
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
MonitoringConfigFileNotFoundError: If file does not exist.
|
|
317
|
+
MonitoringConfigFormatError: If file format is invalid.
|
|
318
|
+
|
|
319
|
+
Examples:
|
|
320
|
+
>>> config = load_monitoring_config("dashboard.monitor.yml") # doctest: +SKIP
|
|
321
|
+
>>> service = config.to_service() # doctest: +SKIP
|
|
322
|
+
>>> result = service.run_sync() # doctest: +SKIP
|
|
323
|
+
"""
|
|
324
|
+
path = pathlib.Path(path)
|
|
325
|
+
if not path.is_file():
|
|
326
|
+
raise MonitoringConfigFileNotFoundError(f"Config file not found: {path}")
|
|
327
|
+
|
|
328
|
+
# Deep defense: Check file size before reading
|
|
329
|
+
file_size = path.stat().st_size
|
|
330
|
+
if file_size > MAX_CONFIG_FILE_SIZE:
|
|
331
|
+
raise MonitoringConfigFormatError(f"Config file too large ({file_size} > {MAX_CONFIG_FILE_SIZE} bytes)")
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
with path.open("r", encoding=encoding) as f:
|
|
335
|
+
data = yaml.safe_load(f)
|
|
336
|
+
except yaml.YAMLError as e:
|
|
337
|
+
raise MonitoringConfigFormatError(f"Invalid YAML in {path}: {e}") from e
|
|
338
|
+
|
|
339
|
+
if not isinstance(data, dict):
|
|
340
|
+
raise MonitoringConfigFormatError(f"Monitoring config must be a YAML dictionary, got {type(data).__name__}")
|
|
341
|
+
|
|
342
|
+
return MonitoringConfig.from_dict(data, source_path=path.resolve())
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def discover_monitoring_configs(
|
|
346
|
+
directory: str | pathlib.Path,
|
|
347
|
+
*,
|
|
348
|
+
recursive: bool = False,
|
|
349
|
+
encoding: str = "utf-8",
|
|
350
|
+
) -> dict[str, MonitoringConfig]:
|
|
351
|
+
"""Discover and load all monitoring configs in a directory.
|
|
352
|
+
|
|
353
|
+
Searches for files matching ``*.monitor.yml`` pattern.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
directory: Directory to search.
|
|
357
|
+
recursive: If True, search subdirectories recursively.
|
|
358
|
+
encoding: File encoding (default UTF-8).
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Dictionary mapping config names to MonitoringConfig objects.
|
|
362
|
+
|
|
363
|
+
Raises:
|
|
364
|
+
FileNotFoundError: If directory does not exist.
|
|
365
|
+
MonitoringConfigFormatError: If any config file is invalid.
|
|
366
|
+
|
|
367
|
+
Examples:
|
|
368
|
+
>>> configs = discover_monitoring_configs("./monitoring") # doctest: +SKIP
|
|
369
|
+
>>> for name, config in configs.items(): # doctest: +SKIP
|
|
370
|
+
... print(f"Loaded: {name}") # doctest: +SKIP
|
|
371
|
+
"""
|
|
372
|
+
directory = pathlib.Path(directory)
|
|
373
|
+
if not directory.is_dir():
|
|
374
|
+
raise FileNotFoundError(f"Directory not found: {directory}")
|
|
375
|
+
|
|
376
|
+
pattern = f"**/{MONITORING_CONFIG_PATTERN}" if recursive else MONITORING_CONFIG_PATTERN
|
|
377
|
+
configs: dict[str, MonitoringConfig] = {}
|
|
378
|
+
|
|
379
|
+
for config_path in directory.glob(pattern):
|
|
380
|
+
config = load_monitoring_config(config_path, encoding=encoding)
|
|
381
|
+
configs[config.name] = config
|
|
382
|
+
|
|
383
|
+
return configs
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def create_services_from_directory(
|
|
387
|
+
directory: str | pathlib.Path,
|
|
388
|
+
*,
|
|
389
|
+
recursive: bool = False,
|
|
390
|
+
encoding: str = "utf-8",
|
|
391
|
+
) -> dict[str, MonitoringService]:
|
|
392
|
+
"""Discover configs and create MonitoringService instances.
|
|
393
|
+
|
|
394
|
+
Convenience function that combines discover_monitoring_configs
|
|
395
|
+
with to_service() for each config.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
directory: Directory to search for ``*.monitor.yml`` files.
|
|
399
|
+
recursive: If True, search subdirectories recursively.
|
|
400
|
+
encoding: File encoding (default UTF-8).
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Dictionary mapping config names to MonitoringService instances.
|
|
404
|
+
|
|
405
|
+
Examples:
|
|
406
|
+
>>> services = create_services_from_directory("./monitoring") # doctest: +SKIP
|
|
407
|
+
>>> for name, service in services.items(): # doctest: +SKIP
|
|
408
|
+
... result = service.run_sync() # doctest: +SKIP
|
|
409
|
+
... print(f"{name}: {result.success}") # doctest: +SKIP
|
|
410
|
+
"""
|
|
411
|
+
configs = discover_monitoring_configs(directory, recursive=recursive, encoding=encoding)
|
|
412
|
+
return {name: config.to_service() for name, config in configs.items()}
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
__all__ = [
|
|
416
|
+
"CollectorConfig",
|
|
417
|
+
"MonitoringConfig",
|
|
418
|
+
"MonitoringConfigCollectorError",
|
|
419
|
+
"MonitoringConfigFileNotFoundError",
|
|
420
|
+
"MonitoringConfigFormatError",
|
|
421
|
+
"create_services_from_directory",
|
|
422
|
+
"discover_monitoring_configs",
|
|
423
|
+
"load_monitoring_config",
|
|
424
|
+
]
|