Skip to content
Rymflux Documentation
Esc
navigateopen⌘Jpreview
On this page

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. Contains info and above.
  • Stdout (dev mode): Full debug+ level when RUST_LOG=rymflux=debug is 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 8601
  • level — error/warn/info/debug/trace
  • target — Rust module path
  • message — human-readable description
  • request_id — UUID, propagated from Tauri invoke call (see Request tracing)

Contextual fields added per event type:

  • plugin_id — for plugin-related events
  • content_id — for library/playback events
  • source — URL or file path for playback events
  • query — search query string (truncated at 200 chars)

Observability

Approach

v1 is a desktop app with no production infrastructure. Observability means:

  1. Structured logs (see Logging above) — the primary diagnostic tool.
  2. Diagnostics commandrymflux diagnostics CLI 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)
  3. Panic handlerstd::panic::set_hook captures 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)
  4. 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_version pragma. 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:
    1. Rename corrupted file to library.db.corrupted.<timestamp>
    2. Create new database
    3. Log warning
    4. App continues with empty library

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, toml crate). The plugin.toml format 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 (capabilities field).
  • The core only sends requests for declared capabilities. If a plugin declares search but not stream, 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:

  1. Attached to all log entries produced during that request
  2. Included as the id field in JSON-RPC requests to plugins
  3. 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.

Was this page helpful?