Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ COPY --from=download-binaries \
/usr/bin/ttyd \
/usr/bin/

COPY tools/dump_journal/dump_journal.py /usr/bin/dump_journal.py
# Copy frontend built on frontend-builder to this stage
COPY --from=frontend-builder /home/pi/frontend/dist /home/pi/frontend
COPY --from=install-services-and-libs /home/pi/.venv /usr/blueos/venv
Expand Down
24 changes: 23 additions & 1 deletion core/frontend/src/components/app/SettingsMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@
<v-card-title class="align-center">
System Log Files ({{ log_folder_size }})
</v-card-title>

<v-alert v-if="status" dense type="info">
{{ status }}
</v-alert>
<v-card-actions class="flex-row">
<v-btn
v-tooltip="'Download log for all services in BlueOS'"
Expand Down Expand Up @@ -211,6 +213,7 @@ export default Vue.extend({
current_deletion_size: 0,
current_deletion_total_size: 0,
current_deletion_status: '',
status: '',
}
},
computed: {
Expand Down Expand Up @@ -242,6 +245,24 @@ export default Vue.extend({
return prettifySize(bytes)
},
async download_service_log_files(): Promise<void> {
// TODO: this should probably be done more on the backend than here...
// This is currently done here as we are relying on the filebrowser's
// on-the-fly download feature.
this.operation_in_progress = true
const command = 'dump_journal.py 0'
this.status = 'Dumping journal...'
await back_axios({
timeout: 60000,
url: `${API_URL}/command/blueos?command=${command}&i_know_what_i_am_doing=true`,
method: 'post',
}).then(() => {
this.status = ''
})
.catch((error) => {
this.status = String(error)
}).finally(() => {
this.operation_in_progress = false
})
const folder = await filebrowser.fetchFolder('system_logs')
await filebrowser.downloadFolder(folder)
},
Expand Down Expand Up @@ -316,6 +337,7 @@ export default Vue.extend({
this.current_deletion_status = 'Starting deletion...'

try {
this.operation_in_progress = true
await back_axios({
url: `${API_URL}/services/remove_log_stream`,
method: 'post',
Expand Down
15 changes: 15 additions & 0 deletions core/services/commander/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import logging
import os
import shlex
import shutil
import subprocess
import time
Expand Down Expand Up @@ -70,6 +71,20 @@ async def command_host(command: str, i_know_what_i_am_doing: bool = False) -> An
return message


@app.post("/command/blueos", status_code=status.HTTP_200_OK)
@version(1, 0)
async def command_blueos(command: str, i_know_what_i_am_doing: bool = False) -> Any:
check_what_i_am_doing(i_know_what_i_am_doing)
logger.debug(f"Running command: {command}")
return subprocess.run(
shlex.split(command),
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)


@app.post("/set_time", status_code=status.HTTP_200_OK)
@version(1, 0)
async def set_time(unix_time_seconds: int, i_know_what_i_am_doing: bool = False) -> Any:
Expand Down
4 changes: 4 additions & 0 deletions core/start-blueos-core
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,7 @@ for TUPLE in "${SERVICES[@]}"; do
done

echo "BlueOS running!"


# dump last boot journal logs to disk
dump_journal.py -1
86 changes: 86 additions & 0 deletions core/tools/dump_journal/dump_journal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python3

import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Optional

from commonwealth.utils.commands import run_command

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def get_boot_info(boot_index: int) -> Optional[dict]:
try:
result = run_command("journalctl --list-boots --output=json")
if result.returncode != 0:
logger.error(f"Failed to get boot list: {result.stderr}")
return None
boots = json.loads(result.stdout)
boots_dict = {boot["index"]: boot for boot in boots}
if boot_index not in boots_dict:
logger.error(f"Boot index {boot_index} not found in boot list")
return None
return boots_dict[boot_index]
except Exception as e:
logger.error(f"Error getting boot info: {e}")
return None


def dump_latest_journal_logs(output_dir: str = "/var/logs/blueos/services/journal/", boot_index: int = -1) -> bool:
Path(output_dir).mkdir(parents=True, exist_ok=True)

boot_info = get_boot_info(boot_index)
if not boot_info:
return False

# Extract boot ID and timestamp
boot_id = boot_info.get("boot_id", "unknown")
first_entry = boot_info.get("first_entry", 0)

# Convert timestamp to datetime
try:
dt = datetime.fromtimestamp(first_entry / 1000000) # Convert microseconds to seconds
timestamp_formatted = dt.strftime("%Y%m%d_%H%M%S")
except (ValueError, TypeError) as e:
logger.error(f"Error parsing timestamp: {e}")
timestamp_formatted = "unknown_time"

# Create filename: timestamp_uuid_short.log
uuid_short = boot_id[:8] if boot_id != "unknown" else "unknown"
filename = f"{timestamp_formatted}_{uuid_short}.log"
filepath = Path(output_dir) / filename

logger.info(f"Dumping latest journal logs for boot {boot_id}")
logger.info(f"Filename: {filename}")

# Get the journal logs for the specified boot with loguru-like format
# --output=short-iso provides timestamps and log levels in ISO format

cmd = f"journalctl -b {boot_index} --output=short-iso"
result = run_command(cmd)
if result.returncode != 0:
logger.error(f"Failed to get journal logs: {result.stderr}")
return False

with open(filepath, "w", encoding="utf-8") as f:
f.write(f"# Journal logs for boot {boot_id}\n")
f.write(f"# Boot started: {dt.strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write("# Format: ISO timestamps with log levels (loguru-like)\n")
f.write(f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write("#" + "=" * 60 + "\n\n")
f.write(result.stdout)

logger.info(f"Logs saved to: {filepath}")
return True


if __name__ == "__main__":
import sys

boot = int(sys.argv[1]) if len(sys.argv) > 1 else -1

dump_latest_journal_logs(boot_index=boot)