cloudmesh-ai-common 7.0.4__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.
@@ -0,0 +1,48 @@
1
+ """Common utilities for cloudmesh-ai components.
2
+
3
+ This package provides shared functionality for logging, telemetry, system
4
+ information gathering, and other common helper utilities used across
5
+ the cloudmesh-ai ecosystem.
6
+ """
7
+
8
+ from .logging import (
9
+ set_context_id,
10
+ get_context_id,
11
+ ContextFilter,
12
+ JsonFormatter,
13
+ load_logging_config,
14
+ get_log_dir,
15
+ ensure_log_dir,
16
+ get_log_file_path,
17
+ get_logger,
18
+ progress,
19
+ )
20
+ from .aggregation import TelemetryAggregator
21
+ from .telemetry import (
22
+ TelemetryBackend,
23
+ JSONFileBackend,
24
+ SQLiteBackend,
25
+ TextBackend,
26
+ Telemetry,
27
+ AsyncTelemetry,
28
+ )
29
+ from .sys import (
30
+ os_is_windows,
31
+ os_is_mac,
32
+ os_is_linux,
33
+ os_is_pi,
34
+ has_window_manager,
35
+ sys_user,
36
+ get_platform,
37
+ get_cpu_description,
38
+ get_gpu_info,
39
+ get_thermal_info,
40
+ get_numa_info,
41
+ get_container_info,
42
+ get_network_info,
43
+ get_disk_read_speed,
44
+ get_cpu_metrics,
45
+ get_memory_metrics,
46
+ get_disk_metrics,
47
+ systeminfo,
48
+ )
@@ -0,0 +1,130 @@
1
+ """
2
+ Telemetry aggregation utility for cloudmesh-ai.
3
+ Provides tools to analyze and summarize telemetry data from various backends.
4
+ """
5
+
6
+ import json
7
+ import sqlite3
8
+ from pathlib import Path
9
+ from typing import Dict, Any, List, Optional, Union
10
+ from collections import defaultdict
11
+
12
+ class TelemetryAggregator:
13
+ """
14
+ Analyzes telemetry records to provide summaries and statistics.
15
+ Supports loading data from both JSONL files and SQLite databases.
16
+ """
17
+
18
+ def __init__(self, source: Union[str, Path]):
19
+ """
20
+ Initialize the TelemetryAggregator.
21
+
22
+ Args:
23
+ source: Path to the telemetry source (JSONL file or SQLite .db file).
24
+ """
25
+ self.source = Path(source).expanduser()
26
+ self.records: List[Dict[str, Any]] = []
27
+ self._load_data()
28
+
29
+ def _load_data(self) -> None:
30
+ """Detects source type and loads records into memory.
31
+ """
32
+ if self.source.suffix == ".db":
33
+ self._load_from_sqlite()
34
+ else:
35
+ self._load_from_jsonl()
36
+
37
+ def _load_from_jsonl(self) -> None:
38
+ """Loads records from a JSONL file.
39
+ """
40
+ try:
41
+ if not self.source.exists():
42
+ return
43
+ with open(self.source, "r") as f:
44
+ for line in f:
45
+ if line.strip():
46
+ self.records.append(json.loads(line))
47
+ except Exception as e:
48
+ print(f"Error loading JSONL telemetry: {e}")
49
+
50
+ def _load_from_sqlite(self) -> None:
51
+ """Loads records from a SQLite database.
52
+ """
53
+ try:
54
+ with sqlite3.connect(self.source) as conn:
55
+ conn.row_factory = sqlite3.Row
56
+ cursor = conn.execute("SELECT * FROM telemetry")
57
+ for row in cursor:
58
+ record = dict(row)
59
+ # Convert JSON strings back to dicts
60
+ record["metrics"] = json.loads(record["metrics"]) if isinstance(record["metrics"], str) else record["metrics"]
61
+ record["system"] = json.loads(record["system"]) if isinstance(record["system"], str) else record["system"]
62
+ self.records.append(record)
63
+ except Exception as e:
64
+ print(f"Error loading SQLite telemetry: {e}")
65
+
66
+ def get_summary(self) -> Dict[str, Any]:
67
+ """
68
+ Calculates a high-level summary of the telemetry data.
69
+
70
+ Returns:
71
+ A dictionary containing total records, success rate,
72
+ status distribution, and command distribution.
73
+ """
74
+ if not self.records:
75
+ return {"error": "No records found"}
76
+
77
+ total = len(self.records)
78
+ status_counts = defaultdict(int)
79
+ command_counts = defaultdict(int)
80
+
81
+ for r in self.records:
82
+ status_counts[r.get("status", "unknown")] += 1
83
+ command_counts[r.get("command", "unknown")] += 1
84
+
85
+ success_count = status_counts.get("completed", 0)
86
+
87
+ return {
88
+ "total_records": total,
89
+ "success_rate": f"{(success_count / total) * 100:.2f}%",
90
+ "status_distribution": dict(status_counts),
91
+ "command_distribution": dict(command_counts),
92
+ }
93
+
94
+ def aggregate_metric(self, metric_name: str) -> Dict[str, Any]:
95
+ """
96
+ Calculates average, min, and max for a specific metric across all records.
97
+
98
+ Args:
99
+ metric_name: The key of the metric to aggregate from the 'metrics' dictionary.
100
+
101
+ Returns:
102
+ A dictionary containing the count, average, minimum, and maximum values.
103
+ """
104
+ values = []
105
+ for r in self.records:
106
+ metrics = r.get("metrics", {})
107
+ if metric_name in metrics:
108
+ val = metrics[metric_name]
109
+ if isinstance(val, (int, float)):
110
+ values.append(val)
111
+
112
+ if not values:
113
+ return {"metric": metric_name, "status": "no data"}
114
+
115
+ return {
116
+ "metric": metric_name,
117
+ "count": len(values),
118
+ "avg": sum(values) / len(values),
119
+ "min": min(values),
120
+ "max": max(values),
121
+ }
122
+
123
+ if __name__ == "__main__":
124
+ # Simple test if run directly
125
+ import sys
126
+ if len(sys.argv) > 1:
127
+ agg = TelemetryAggregator(sys.argv[1])
128
+ print(json.dumps(agg.get_summary(), indent=4))
129
+ else:
130
+ print("Usage: python aggregation.py <telemetry_file_or_db>")
@@ -0,0 +1,101 @@
1
+ """
2
+ I/O utility functions for cloudmesh-ai.
3
+ Provides helpers for path expansion, YAML handling, and benchmark file creation.
4
+ """
5
+
6
+ import os
7
+ import yaml
8
+ from pathlib import Path
9
+ from typing import Any, Dict, Optional
10
+
11
+ def path_expand(text: str, slashreplace: bool = True) -> str:
12
+ """Expands a path string by resolving '~', environment variables, and relative links.
13
+
14
+ Args:
15
+ text: The path to be expanded (e.g., "~/$PROJECT/./file.txt").
16
+ slashreplace: If True, returns backslashes on Windows. Defaults to True.
17
+
18
+ Returns:
19
+ The fully expanded and absolute path.
20
+ """
21
+ if not text:
22
+ return ""
23
+
24
+ # 1. Expand ~ and Environment Variables
25
+ expanded = os.path.expandvars(os.path.expanduser(text))
26
+
27
+ # 2. Convert to a Path object and make it absolute
28
+ # .resolve() handles the "./" and "../" logic correctly
29
+ path_obj = Path(expanded).resolve()
30
+
31
+ # 3. Handle string conversion and slash preference
32
+ if slashreplace and os.name == 'nt':
33
+ # On Windows, this automatically uses backslashes
34
+ return str(path_obj)
35
+
36
+ # .as_posix() forces forward slashes (/) regardless of OS
37
+ return path_obj.as_posix()
38
+
39
+ def load_yaml(path: Path) -> Optional[Dict[str, Any]]:
40
+ """Safely loads a YAML file from the given path.
41
+
42
+ Args:
43
+ path: Path to the YAML file.
44
+
45
+ Returns:
46
+ The loaded YAML data as a dictionary, or None if the file does not exist
47
+ or an error occurs.
48
+ """
49
+ try:
50
+ if not path.exists():
51
+ return None
52
+ with open(path, 'r') as f:
53
+ return yaml.safe_load(f)
54
+ except (yaml.YAMLError, OSError):
55
+ return None
56
+
57
+ def dump_yaml(path: Path, data: Dict[str, Any]) -> None:
58
+ """Safely writes a dictionary to a YAML file, ensuring the directory exists.
59
+
60
+ Args:
61
+ path: Path where the YAML file should be written.
62
+ data: The dictionary to write to the file.
63
+
64
+ """
65
+ path.parent.mkdir(parents=True, exist_ok=True)
66
+ with open(path, 'w') as f:
67
+ yaml.dump(data, f, default_flow_style=False)
68
+
69
+ def create_benchmark_yaml(path: str, n: int) -> None:
70
+ """Creates a Cloudmesh service YAML test file with specified number of services.
71
+
72
+ Args:
73
+ path: Path where the benchmark YAML file should be created.
74
+ n: Number of services to include in the benchmark file.
75
+
76
+ """
77
+ cm = {"cloudmesh": {}}
78
+ for i in range(0, n):
79
+ cm["cloudmesh"][f"service{i}"] = {"attribute": f"service{i}"}
80
+
81
+ location = path_expand(path)
82
+ with open(location, "w") as yaml_file:
83
+ yaml.dump(cm, yaml_file, default_flow_style=False)
84
+
85
+ def create_benchmark_file(path: str, n: int) -> int:
86
+ """Creates a file of a given size in binary megabytes.
87
+
88
+ Args:
89
+ path: Path where the benchmark file should be created.
90
+ n: Size of the file in megabytes.
91
+
92
+ Returns:
93
+ The actual size of the created file in megabytes.
94
+ """
95
+ location = path_expand(path)
96
+ size = 1048576 * n # size in bytes
97
+ with open(location, "wb") as f:
98
+ f.write(os.urandom(size))
99
+
100
+ s = os.path.getsize(location)
101
+ return int(s / 1048576.0)
@@ -0,0 +1,264 @@
1
+ """
2
+ Logging utility for cloudmesh-ai components.
3
+ Provides centralized management of log directories, file naming, and logger configuration.
4
+ Includes support for JSON logging, log rotation, and request tracing.
5
+ """
6
+
7
+ import logging
8
+ import logging.handlers
9
+ import os
10
+ import sys
11
+ import json
12
+ import threading
13
+ from pathlib import Path
14
+ from datetime import datetime
15
+ from typing import Dict, Optional, Any, Union
16
+
17
+ # Thread-local storage for request tracing
18
+ _thread_local = threading.local()
19
+
20
+ def set_context_id(context_id: str):
21
+ """Sets the context ID for the current thread to enable request tracing.
22
+
23
+ Args:
24
+ context_id: The unique identifier for the current request or context.
25
+ """
26
+ _thread_local.context_id = context_id
27
+
28
+ def get_context_id() -> Optional[str]:
29
+ """Retrieves the context ID for the current thread.
30
+
31
+ Returns:
32
+ The current context ID if set, otherwise None.
33
+ """
34
+ return getattr(_thread_local, "context_id", None)
35
+
36
+ class ContextFilter(logging.Filter):
37
+ """Filter that injects the current context_id into the log record."""
38
+ def filter(self, record: logging.LogRecord) -> bool:
39
+ """Injects the current context_id into the log record.
40
+
41
+ Args:
42
+ record: The log record to be filtered.
43
+
44
+ Returns:
45
+ True to indicate the record should be logged.
46
+ """
47
+ record.context_id = get_context_id() or "system"
48
+ return True
49
+
50
+ class JsonFormatter(logging.Formatter):
51
+ """Formatter that outputs log records in JSON format."""
52
+ def format(self, record: logging.LogRecord) -> str:
53
+ """Formats the log record as a JSON string.
54
+
55
+ Args:
56
+ record: The log record to format.
57
+
58
+ Returns:
59
+ A JSON string representation of the log record.
60
+ """
61
+ log_record = {
62
+ "timestamp": self.formatTime(record, self.datefmt),
63
+ "level": record.levelname,
64
+ "logger": record.name,
65
+ "message": record.getMessage(),
66
+ "context_id": getattr(record, "context_id", "system"),
67
+ }
68
+ if record.exc_info:
69
+ log_record["exception"] = self.formatException(record.exc_info)
70
+ return json.dumps(log_record)
71
+
72
+ # Cache for loggers to prevent duplicate handlers
73
+ _loggers: Dict[str, logging.Logger] = {}
74
+
75
+ # Global logging configuration
76
+ _logging_config: Dict[str, Any] = {}
77
+
78
+ def load_logging_config(config_path: Union[str, Path]) -> None:
79
+ """Loads logging configuration from a JSON file.
80
+
81
+ Example config: {"log_dir": "/var/log/cloudmesh", "level": "DEBUG", "json_format": true}
82
+
83
+ Args:
84
+ config_path: Path to the JSON configuration file.
85
+
86
+ """
87
+ global _logging_config
88
+ try:
89
+ path = Path(config_path).expanduser()
90
+ if path.exists():
91
+ with open(path, "r") as f:
92
+ _logging_config = json.load(f)
93
+ except Exception as e:
94
+ # Use print here because logger might not be configured yet
95
+ print(f"Failed to load logging config from {config_path}: {e}")
96
+
97
+ # Automatically load default config on import
98
+ load_logging_config("~/.config/cloudmesh/ai/config.json")
99
+
100
+ def get_log_dir() -> Path:
101
+ """Returns the expanded path to the AI logs directory, using config if available.
102
+
103
+ Returns:
104
+ The Path object pointing to the logs directory.
105
+ """
106
+ log_dir = _logging_config.get("log_dir", "~/.config/cloudmesh/ai/logs")
107
+ return Path(log_dir).expanduser()
108
+
109
+ def ensure_log_dir() -> Path:
110
+ """Ensures the log directory exists and returns it.
111
+
112
+ Returns:
113
+ The Path object pointing to the ensured logs directory.
114
+ """
115
+ log_dir = get_log_dir()
116
+ log_dir.mkdir(parents=True, exist_ok=True)
117
+ return log_dir
118
+
119
+ def get_log_file_path(script_name: str) -> Path:
120
+ """Generates a timestamped log file path for a given script.
121
+
122
+ Example: ~/.config/cloudmesh/ai/logs/test_20260412_124500.log
123
+
124
+ Args:
125
+ script_name: The name of the script for which to generate the log path.
126
+
127
+ Returns:
128
+ The Path object pointing to the generated log file.
129
+ """
130
+ log_dir = ensure_log_dir()
131
+
132
+ # Use custom filename if provided in config, otherwise use script_name
133
+ prefix = _logging_config.get("log_prefix", script_name)
134
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
135
+ return log_dir / f"{prefix}_{timestamp}.log"
136
+
137
+ def get_logger(
138
+ script_name: str,
139
+ level: Optional[int] = None,
140
+ json_format: Optional[bool] = None,
141
+ max_bytes: Optional[int] = None,
142
+ backup_count: Optional[int] = None
143
+ ) -> logging.Logger:
144
+ """Returns a configured logger instance for the given script.
145
+
146
+ Configures both a rotating file handler and a stream handler.
147
+
148
+ Args:
149
+ script_name: Name of the logger/script.
150
+ level: Logging level.
151
+ json_format: If True, uses JSON formatting for logs.
152
+ max_bytes: Max size of a log file before rotation.
153
+ backup_count: Number of backup log files to keep.
154
+
155
+ Returns:
156
+ A configured logging.Logger instance.
157
+ """
158
+ if script_name in _loggers:
159
+ return _loggers[script_name]
160
+
161
+ # Resolve settings: Argument > Config File > Default
162
+ final_level = level if level is not None else _logging_config.get("level", logging.INFO)
163
+ if isinstance(final_level, str):
164
+ final_level = getattr(logging, final_level.upper(), logging.INFO)
165
+
166
+ final_json = json_format if json_format is not None else _logging_config.get("json_format", False)
167
+ final_max_bytes = max_bytes if max_bytes is not None else _logging_config.get("max_bytes", 10 * 1024 * 1024)
168
+ final_backup_count = backup_count if backup_count is not None else _logging_config.get("backup_count", 5)
169
+
170
+ logger = logging.getLogger(script_name)
171
+ logger.setLevel(final_level)
172
+
173
+ # Prevent adding handlers if they already exist
174
+ if not logger.handlers:
175
+ # Add Context Filter for request tracing
176
+ logger.addFilter(ContextFilter())
177
+
178
+ if final_json:
179
+ formatter = JsonFormatter()
180
+ else:
181
+ formatter = logging.Formatter('%(asctime)s - %(name)s - [%(context_id)s] - %(levelname)s - %(message)s')
182
+
183
+ # 1. Rotating File Handler
184
+ log_file = get_log_file_path(script_name)
185
+ try:
186
+ fh = logging.handlers.RotatingFileHandler(
187
+ log_file, maxBytes=final_max_bytes, backupCount=final_backup_count
188
+ )
189
+ fh.setFormatter(formatter)
190
+ logger.addHandler(fh)
191
+ except Exception as e:
192
+ print(f"Failed to initialize rotating file logger at {log_file}: {e}")
193
+
194
+ # 2. Stream Handler (Console)
195
+ sh = logging.StreamHandler()
196
+ sh.setFormatter(formatter)
197
+ logger.addHandler(sh)
198
+
199
+ _loggers[script_name] = logger
200
+ return logger
201
+
202
+ def progress(
203
+ filename: Optional[str] = None,
204
+ status: str = "ready",
205
+ progress: Union[int, str, float] = 0,
206
+ pid: Optional[Union[int, str]] = None,
207
+ timestamp: bool = False,
208
+ stdout: bool = True,
209
+ stderr: bool = True,
210
+ append: Optional[str] = None,
211
+ **kwargs,
212
+ ) -> str:
213
+ """Creates a printed line of the form
214
+ "# cloudmesh status=ready progress=0 pid=$$ time='2022-08-05 16:29:40.228901'".
215
+
216
+ Args:
217
+ filename: Optional file to append the progress line to.
218
+ status: The current status string.
219
+ progress: The current progress value (int, float, or string).
220
+ pid: Process ID. If None, it will be automatically detected.
221
+ timestamp: If True, includes a timestamp in the output.
222
+ stdout: If True, prints to stdout.
223
+ stderr: If True, prints to stderr.
224
+ append: Optional string to append to the end of the line.
225
+ **kwargs: Additional key-value pairs to include in the progress line.
226
+
227
+ Returns:
228
+ The formatted progress string.
229
+ """
230
+ if isinstance(progress, (int, float)):
231
+ progress = str(progress)
232
+
233
+ if pid is None:
234
+ if "SLURM_JOB_ID" in os.environ:
235
+ pid = os.environ["SLURM_JOB_ID"]
236
+ elif "LSB_JOBID" in os.environ:
237
+ pid = os.environ["LSB_JOBID"]
238
+ else:
239
+ pid = os.getpid()
240
+
241
+ variables = ""
242
+ msg = f"# cloudmesh status={status} progress={progress} pid={pid}"
243
+
244
+ if timestamp:
245
+ t = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')
246
+ msg = msg + f" time='{t}'"
247
+
248
+ if kwargs:
249
+ for name, value in kwargs.items():
250
+ variables = variables + f" {name}={value}"
251
+ msg = msg + variables
252
+
253
+ if append is not None:
254
+ msg = msg + " " + append
255
+
256
+ if stdout:
257
+ print(msg, file=sys.stdout)
258
+ if stderr:
259
+ print(msg, file=sys.stderr)
260
+ if filename is not None:
261
+ with open(filename, "a") as f:
262
+ f.write(msg + "\n")
263
+
264
+ return msg