Sanitized single-commit public mirror of recipe-maintainer. - Removed test-ssh/.testenv (live creds); added test-ssh/.testenv.example placeholders. - Removed plans/ and planned-updates/ (deployment-planning docs) so no client/ deployment domains appear in the public repo. - All other secret stores were already gitignored. - docs.coopcloud.tech retained as a submodule (public upstream).
107 lines
3.6 KiB
Python
107 lines
3.6 KiB
Python
"""Structured logging for skill operations.
|
|
|
|
Each skill invocation creates a SkillLogger that captures commands,
|
|
their output, and contextual messages. On save(), the log is written
|
|
to logs/<skill>-<recipe>-<date>.md in markdown format.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from lib.config import paths
|
|
|
|
|
|
class SkillLogger:
|
|
"""Captures structured output from a skill invocation."""
|
|
|
|
def __init__(self, skill_name: str, recipe_name: str | None = None):
|
|
self.skill_name = skill_name
|
|
self.recipe_name = recipe_name
|
|
self.started = datetime.now()
|
|
self._entries: list[dict] = []
|
|
|
|
def step(self, description: str) -> None:
|
|
"""Log a major step in the skill execution."""
|
|
self._entries.append({"type": "step", "text": description})
|
|
print(f"\n=== {description} ===", flush=True)
|
|
|
|
def command(self, cmd: str, output: str, returncode: int) -> None:
|
|
"""Log a command and its result."""
|
|
self._entries.append({
|
|
"type": "command",
|
|
"cmd": cmd,
|
|
"output": output,
|
|
"returncode": returncode,
|
|
})
|
|
|
|
def info(self, message: str) -> None:
|
|
"""Log an informational message."""
|
|
self._entries.append({"type": "info", "text": message})
|
|
print(f" {message}", flush=True)
|
|
|
|
def warn(self, message: str) -> None:
|
|
"""Log a warning."""
|
|
self._entries.append({"type": "warn", "text": message})
|
|
print(f" WARNING: {message}", flush=True)
|
|
|
|
def error(self, message: str) -> None:
|
|
"""Log an error."""
|
|
self._entries.append({"type": "error", "text": message})
|
|
print(f" ERROR: {message}", flush=True)
|
|
|
|
def save(self) -> Path:
|
|
"""Write the log to logs/<skill>-<recipe>-<date>.md."""
|
|
date_str = self.started.strftime("%Y-%m-%d")
|
|
if self.recipe_name:
|
|
filename = f"{self.skill_name}-{self.recipe_name}-{date_str}.md"
|
|
else:
|
|
filename = f"{self.skill_name}-{date_str}.md"
|
|
|
|
log_dir = paths.LOGS_DIR
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
log_path = log_dir / filename
|
|
|
|
lines = [
|
|
f"# {self.skill_name}",
|
|
"",
|
|
]
|
|
if self.recipe_name:
|
|
lines.append(f"Recipe: {self.recipe_name}")
|
|
lines.append("")
|
|
lines.append(
|
|
f"Started: {self.started.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
)
|
|
lines.append(
|
|
f"Finished: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
|
)
|
|
lines.append("")
|
|
|
|
for entry in self._entries:
|
|
if entry["type"] == "step":
|
|
lines.append(f"## {entry['text']}")
|
|
lines.append("")
|
|
elif entry["type"] == "command":
|
|
lines.append(f"```")
|
|
lines.append(f"$ {entry['cmd']}")
|
|
if entry["output"]:
|
|
lines.append(entry["output"])
|
|
lines.append(f"```")
|
|
if entry["returncode"] != 0:
|
|
lines.append(f"Exit code: {entry['returncode']}")
|
|
lines.append("")
|
|
elif entry["type"] == "info":
|
|
lines.append(f"{entry['text']}")
|
|
lines.append("")
|
|
elif entry["type"] == "warn":
|
|
lines.append(f"**WARNING:** {entry['text']}")
|
|
lines.append("")
|
|
elif entry["type"] == "error":
|
|
lines.append(f"**ERROR:** {entry['text']}")
|
|
lines.append("")
|
|
|
|
with open(log_path, "w") as f:
|
|
f.write("\n".join(lines))
|
|
|
|
print(f"\n Log saved: {log_path}", flush=True)
|
|
return log_path
|