Cross-cutting concerns
Cross-cutting concerns
Logging
Approach
Use the tracing crate (Rust) and console / debug (Svelte/TypeScript). All backend logging goes through tracing with structured fields.
Log levels
| Level | When |
|---|---|
error |
Plugin crash, database corruption, audio device failure, unexpected IPC disconnection |
warn |
Slow plugin response (>10s), malformed manifest (skipped), content resolution miss, deduplication edge case |
info |
Plugin registered, plugin launched/terminated, search dispatched, track played, queue modified, library mutation |
debug |
Every JSON-RPC message (request + response), Tauri command invocation, SQL query, audio buffer state |
trace |
Raw bytes on stdin/stdout, audio sample-level data (off by default) |
Log output
- File:
~/.rymflux/logs/app.log— rolling file, 10MB per file, 5 rotations. Containsinfoand above. - Stdout (dev mode): Full
debug+ level whenRUST_LOG=rymflux=debugis set. - Plugin logs: Captured from plugin stderr (per ADR-001), stored per-plugin in
~/.rymflux/logs/plugins/<plugin_id>.log. Surfaced through the diagnostics system (M16).
Structured fields
Every log entry includes:
timestamp— ISO 8601level— error/warn/info/debug/tracetarget— Rust module pathmessage— human-readable descriptionrequest_id— UUID, propagated from Tauri invoke call (see Request tracing)
Contextual fields added per event type:
plugin_id— for plugin-related eventscontent_id— for library/playback eventssource— URL or file path for playback eventsquery— search query string (truncated at 200 chars)
Observability
Approach
v1 is a desktop app with no production infrastructure. Observability means:
- Structured logs (see Logging above) — the primary diagnostic tool.
- Diagnostics command —
rymflux diagnosticsCLI command (could be a hidden Tauri command or shell alias) that dumps:- Registered plugins + their capabilities + process status (running / idle / crashed)
- Library size (total items, playlists, history entries)
- Audio device info (name, sample rate, channels)
- Recent log tail (last 50 lines)
- Panic handler —
std::panic::set_hookcaptures Rust panics to:- Write a crash report to
~/.rymflux/crash-reports/(panicked thread, stack trace, last 50 log lines) - Show a non-blocking error dialog to the user (“Something went wrong. A crash report was saved.”)
- Do NOT terminate on plugin-related panics (plugins are separate processes)
- Write a crash report to
- Frontend error boundary — Each room has a Svelte error boundary. A room crash does not take down the shell.
Metrics (v1 minimal)
No metrics infrastructure in v1. Key tracking numbers computed from logs:
- Plugin crash rate (crashes / total requests)
- Search latency per plugin
- Library mutation rate
These become structured metrics (Prometheus / OpenTelemetry) in a future version. For v1, log-based analysis is sufficient.
Error handling
Approach
Errors use a three-tier severity model:
| Severity | User impact | Response |
|---|---|---|
| Fatal | App cannot function | Crash dialog + crash report. Terminal only for core initialization failures (e.g., database corruption that cannot be repaired). |
| Recoverable | Feature degraded, app continues | Notification to user. Automatic retry or graceful degradation. |
| Informational | No user impact | Logged at warn level. May be surfaced in diagnostics. |
Error types
| Scenario | Severity | Handling |
|---|---|---|
| Plugin crash during request | Recoverable | Return PLUGIN_CRASHED error to caller. Mark plugin for restart. Log full error. |
| Plugin timeout | Recoverable | Return PLUGIN_TIMEOUT error. Kill plugin process. Mark for restart on next request. |
| Plugin manifest parse failure | Recoverable | Skip plugin. Log error with path and parse failure reason (FR-PLUGIN-1). |
| Audio device unavailable at playback time | Recoverable | Show “No audio output device found” banner. Retry on device change events. |
| Audio device unavailable at startup | Recoverable | Show non-blocking “No audio output detected” banner. App loads fully — library, search, queue, and room switching all work. Deferred retry: attempt device re-initialization on first playback trigger. |
| Database migration failure | Fatal | Show error dialog. Log full migration error to crash report. App cannot start without database. |
| Unsupported audio format | Recoverable | Return UNSUPPORTED_FORMAT error. Skip track in queue. Notify user. |
| Search with no plugins | Informational | Return empty results with message “No search plugins installed” (FR-CORE-4). |
| Content resolution miss (UUID not found) | Recoverable | Return CONTENT_NOT_FOUND. Log the miss for diagnostics. Plugin may have been removed. |
JSON-RPC error codes
Standard error codes used across all plugin communication (aligned with JSON-RPC 2.0):
| Code | Name | When |
|---|---|---|
| -32700 | PARSE_ERROR | Invalid JSON was received by the server (core or plugin) |
| -32600 | INVALID_REQUEST | The JSON sent is not a valid request object |
| -32601 | METHOD_NOT_FOUND | The method does not exist / is not supported |
| -32602 | INVALID_PARAMS | Invalid method parameter(s) |
| -32603 | INTERNAL_ERROR | Internal JSON-RPC error |
| 1 | PLUGIN_CRASHED | Plugin process exited unexpectedly before responding |
| 2 | PLUGIN_TIMEOUT | Plugin did not respond within the configured timeout (30s) |
| 3 | CONTENT_NOT_FOUND | The requested content ID does not exist in this plugin |
| 4 | UNSUPPORTED_FORMAT | The audio source format cannot be decoded |
| 5 | NOT_IMPLEMENTED | The capability is declared but not yet functional in this version |
Resilience
Plugin crash recovery
- Plugin process exit (expected or unexpected) is handled by the Plugin Runtime.
- If a plugin crashes during a request: return PLUGIN_CRASHED, mark plugin for restart.
- The plugin is restarted on the next request (lazy restart). No automatic restart loop.
- If a plugin crashes 5 times in 5 minutes: disable the plugin and notify the user.
- Plugin crash does not affect any other plugin or the core.
Audio engine resilience
- Audio decode failure for one track: skip to next track in queue. Do not crash the engine.
- Audio device lost (headphones unplugged, sink changed): pause playback, resume on device available.
- Audio thread panic: caught by the panic handler, engine restarts the audio thread.
Database resilience
- SQLite WAL mode enabled for crash recovery.
- Schema version stored in a
_schema_versionpragma. Migration runs at startup. - All writes are transactional. Partial writes are rolled back on crash.
- Database file corruption: detected by
PRAGMA integrity_check. On failure:- Rename corrupted file to
library.db.corrupted.<timestamp> - Create new database
- Log warning
- App continues with empty library
- Rename corrupted file to
Async timeout safety
All plugin IPC operations use tokio::time::timeout. Every operation has a configured timeout:
- Plugin launch: 5 seconds (FR-PLUGIN-3)
- Plugin request/response: 30 seconds (configurable, FR-PLUGIN-3)
- Plugin idle: 5 minutes (configurable)
- Search per-plugin: inherits the per-request timeout (30s default)
Caching
What is cached
| Cache | Location | TTL | Invalidation |
|---|---|---|---|
| Plugin search results | Frontend (Svelte store) | Session (cleared on app restart) | New search for same query replaces |
| Album artwork (from plugins) | None in v1 | — | — |
| Audio decode buffer | phonic (internal) | Duration of track | Track change |
| Plugin manifest parse result | Backend (in-memory map) | Process lifetime | App restart |
| SQLite page cache | SQLite (default cache_size) | Managed by SQLite | SQLite internal |
What is not cached
- Search results across sessions (library is persistent, search is realtime)
- Audio files (no local copy — stream URLs are ephemeral)
- Plugin binaries (launched fresh each time per idle timeout)
Future caching considerations (v2+)
- LRU cache for album artwork (most recently viewed rooms have instant artwork)
- Audio pre-decode for gapless playback (v2, W8)
- Search result cache with invalidation on library mutation
- Offline audio cache (core-managed downloads, W9)
Directory layout
~/.rymflux/
├── config.toml # User-editable configuration (TOML)
├── data/
│ └── library.db # SQLite database (machine-generated)
├── logs/
│ ├── app.log # Rolling log, info+, 10MB per file, 5 rotations
│ └── plugins/
│ └── <plugin-id>.log # Per-plugin stderr capture
├── plugins/
│ └── <plugin-id>/ # Plugin directories (user-installed)
│ ├── plugin.toml # Plugin manifest
│ └── ... # Plugin executable and state
└── crash-reports/ # Rust panic crash reports
The split between user-editable files at the root (config) and machine-generated data in subdirectories is deliberate. Config is hand-edited; data, logs, and crash reports are managed by the platform.
XDG consideration: On Linux, the XDG Base Directory Specification suggests $XDG_CONFIG_HOME/rymflux/ for config and $XDG_DATA_HOME/rymflux/ for data. Rymflux uses ~/.rymflux/ (a single top-level directory) for simplicity in v1, prioritizing macOS-first ergonomics. XDG compliance is a future consideration.
Configuration
Approach
Configuration is stored in a TOML file at ~/.rymflux/config.toml. Written by the app on first launch, editable by the user.
Config categories
| Section | Keys | Default | User-editable |
|---|---|---|---|
[core] |
data_dir, log_level |
~/.rymflux, info |
Yes |
[plugins] |
dir, request_timeout_secs, idle_timeout_secs, max_restarts |
~/.rymflux/plugins, 30, 300, 5 |
Yes |
[audio] |
device, sample_rate, buffer_size |
default, 44100, 1024 |
Yes |
[library] |
db_path, dedup_enabled |
~/.rymflux/data/library.db, true |
Partially |
[search] |
per_plugin_timeout_secs, max_results_per_plugin, ranking |
30, 100, default weights | Yes |
Config validation
- All config values are validated on read. Invalid values fall back to defaults with a warning logged.
- File format is TOML (standard Rust ecosystem,
tomlcrate). Theplugin.tomlformat extends this convention to plugins.
Secrets
- No secrets in v1. No API keys, no tokens, no user credentials managed by the core.
- Plugins that require API keys (e.g., a Jamendo API token) manage their own secrets. The plugin stores them in its state directory (
~/.rymflux/plugins/<id>/state/) or environment variables. - The core does not read, store, or transmit plugin secrets.
Security
Threat model
v1 is local-first, single-user (constraint C6, scope non-goals). The threat model is limited:
| Threat | Risk | Mitigation |
|---|---|---|
| Malicious plugin reads files outside its scope | Low (user-installed plugins) | Plugin is a separate OS process. OS file permissions apply. Core does not grant additional file access. |
| Malicious plugin sends malformed data to crash the core | Low | JSON-RPC parser is defensive (serde deserialization with strict types). Invalid JSON returns PARSE_ERROR without unwinding. Payload size limited to 10MB per message. |
| Malicious plugin consumes excessive resources | Low | Idle timeout terminates plugin after 5 minutes. Configurable max restarts. OS process scheduler handles CPU contention. |
| Local file access to library database | Medium | Database file is in user’s data directory with default OS permissions. No encryption in v1. |
| Plugin reads another plugin’s state | Low | Each plugin gets its own state directory. OS file permissions apply. |
Capability-based security
- Plugin capabilities are declared in the manifest (
capabilitiesfield). - The core only sends requests for declared capabilities. If a plugin declares
searchbut notstream, the core will never send a stream request. - Unknown capabilities in the manifest are ignored with a warning (FR-PLUGIN-2).
Request tracing
Approach
Every Tauri invoke call generates a request_id (UUIDv4). This ID is:
- Attached to all log entries produced during that request
- Included as the
idfield in JSON-RPC requests to plugins - Emitted in Tauri events so the frontend can correlate events to requests
This enables end-to-end tracing of a single user action across the Svelte frontend, Rust backend, and plugin processes — all from log files.