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.
- cloudmesh/ai/common/__init__.py +48 -0
- cloudmesh/ai/common/aggregation.py +130 -0
- cloudmesh/ai/common/io.py +101 -0
- cloudmesh/ai/common/logging.py +264 -0
- cloudmesh/ai/common/stopwatch.py +407 -0
- cloudmesh/ai/common/sys.py +589 -0
- cloudmesh/ai/common/telemetry.py +408 -0
- cloudmesh/ai/common/time.py +35 -0
- cloudmesh/ai/common/user.py +87 -0
- cloudmesh_ai_common-7.0.4.dist-info/METADATA +20 -0
- cloudmesh_ai_common-7.0.4.dist-info/RECORD +13 -0
- cloudmesh_ai_common-7.0.4.dist-info/WHEEL +5 -0
- cloudmesh_ai_common-7.0.4.dist-info/top_level.txt +1 -0
|
@@ -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
|