Your single pane of glass for real-time analytics into MySQL/MariaDB & ProxySQL https://pypi.org/project/dolphie/
Find a file
Charles Thompson df673ebc98
Modernize metric graphs, UI architecture, and Python tooling (#123)
* Refactor metric graphs and modernize Python support

* Exclude local Docker development assets

* Migrate dependency management to uv

* Add repository link to package metadata

* Align CI workflow with Clickie

* refactor(replication): streamline topology and replica handling

Improve multi-topology correctness, replica discovery and polling, tab synchronization, and replication panel presentation with expanded regression coverage.

* style: apply required Ruff formatting

* fix(filters): resolve basedpyright type errors from filter merge

Filter attributes and helpers can hold None (unset filters), but were
typed as non-optional. Also fixes an invalid Textual notify severity
and a test passing a mock where DolphieApp is expected.

* ci: split lint/typecheck/build from the version matrix

Ruff and basedpyright are pinned to a fixed target Python version in
pyproject.toml, so running format/lint/type-check/build under both
matrix legs was pure duplication - only pytest needs both versions.
Also restricts push to main so feature-branch pushes with an open PR
don't double-run CI, adds fail-fast: false so both test versions
report failures independently, and scopes the concurrency group by
workflow name.

* refactor: apply reuse/simplification/efficiency cleanups from review

- Consolidate the triple MetricKey resolver into resolve_metric_data()
- Merge duplicate Theme renderable helpers and refresh_screen_* methods
- Replace CPU_Percent name check with a declarative smooth_extreme_values flag
- Declare Y-axis formatting per metric group via ValueFormat instead of
  frozensets of group types in MetricGraph
- Dedupe ProxySQL command-stat buckets via a shared registry and subclass
- Drop MetricManager's dead re-export shim, derivable name dict, unused
  source_key field, and lock-wrapper method pairs
- Guard SHOW REPLICAS behind a discovery signature; reuse one replica poll
  executor; change-guard dashboard availability syncs
- Cache metric timestamp parsing/formatting and add bounded MetricData
  accessors to avoid full-history copies on hot paths
- Derive graph colors from Theme constants; misc small cleanups

* docs: justify parse_metric_datetime cache by the replay-rebuild path

* fix: restore replica-topology behaviors and other correctness bugs from code review

- verify_config_value: handle float config values (refresh_interval)
- kill-by-parameter: drop isinstance guard that silently broke ProxySQL mass kill
- _poll_replica: allow None user/password (auth-socket/passwordless setups)
- fetch_replication_data: fall back to the first channel when no channel
  matches the monitored source UUID, instead of failing the poll
- _refresh_replica_discovery: publish processlist-based discovery even when
  SHOW REPLICAS/SHOW SLAVE HOSTS fails (e.g. missing privilege), and don't
  cache a failed reported-replica fetch so it retries next cycle
- create_replica_panel: don't show a permanent loading spinner for replay
  tabs, which never run the replicas worker
- _refresh_errant_transactions: re-exclude the monitored source's own/related
  UUIDs when Retrieved_Gtid_Set is empty, avoiding a stale-snapshot false
  positive after a replica restart
- build_replica_discovery: connect via the processlist IP, not report_host
  (may be unreachable from the monitor); restore the MariaDB rotation
  fallback for the unambiguous single-replica/single-report case
- ReplicaManager.upsert_replica: close the stale connection outside the lock,
  matching remove_replica/remove_all_replicas, so a wedged socket can't
  block UI-thread readers
- MetricGraph: use the chronological last x value instead of lexicographic
  max() on day-first date strings
- WorkerManager: bound non-sequential replay rebuilds to the default rolling
  window instead of the whole file when trimming is disabled (window=0)
- MetricGraphDashboard: restore the tab that availability auto-switched away
  from once it becomes available again, instead of leaving it stranded

* fix: revert MySQL replica host resolution back to preferring report_host

Verified against dolphie's own GR docker fixture (docker/gr): the async
replica's report_host/report_port (127.0.0.1:3324, its host-published
address) is the reachable one, while the processlist-visible peer address
(172.28.1.5, its Docker-bridge-internal IP) is not reachable from outside
the container network. The prior commit's rationale — that report_host
could be internal-only while the processlist IP is reachable — was the
opposite of this repo's real, tested scenario (NAT/containers/report_host's
actual purpose), so it broke real replica connectivity. Restore the
original preference: report_host when present, processlist host as a
fallback only when nothing was reported.

* fix: display MariaDB replicas' true advertised port when report_host is ambiguous

When multiple MariaDB replicas share an identical report_host (e.g. all
published via 127.0.0.1 with distinct ports, as in docker/mariadb's test
fixture), discovery has no correlating key in the processlist (no UUID, no
server_id) to tell them apart, so it falls back to connecting via the
processlist host with a guessed port. That connection is correct — verified
against the real fixture, it reaches the right server — but the guessed
port doesn't match what the replica actually advertised.

Resolving this at discovery time would require speculative probe
connections, and patching Replica.host/port after the fact would fight
upsert_replica's change-detection (it compares against discovery's
deterministic output every cycle, causing reconnect churn). Instead, keep
connecting via the address that already works, and separately resolve the
replica's true advertised endpoint for display once connected: its own
@@server_id (already fetched for free via fetch_status_and_variables)
looks up a Server_id -> (Host, Port) map built from the latest SHOW SLAVE
HOSTS, stored on ReplicaManager. Replica.host_with_port prefers this
resolved endpoint when present, falling back to host/port otherwise.

No effect on replicas without report_host configured at all (SHOW SLAVE
HOSTS never lists them, so the map has no entry and this is a no-op) or on
replicas with an unambiguous report_host (already handled correctly by
discovery's existing correlation).

* fix: render the tip lightbulb as a literal emoji instead of Rich's 💡 shortcode

Textual's Label widget parses its own Content markup, which resolves
$theme-variable tokens but does not expand Rich's :emoji_name: shortcodes
the way Text.from_markup/console.print do. The three tips using 💡
(Statements Summary panel, PFS Table I/O Waits tab, EXPLAIN failure tip)
rendered the literal text '💡' instead of 💡. Use the Unicode character
directly so it renders correctly regardless of which markup engine handles
the string.

* refactor: eliminate cast() in favor of accurate types and runtime narrowing

Removes trivial single-use wrapper methods around a lock-free MariaDB
port map, and replaces every typing.cast() call across the codebase
with either a more accurate field type (matching what fetchall()/
fetchone()/replay data actually produce), a real isinstance()/assert
narrowing check, or a targeted type: ignore where a third-party stub
(plotext, zstandard) is simply wrong. Also tightens two Any usages
(system_utilization on the replay dataclasses) to match the already-
precise Dolphie field type.

* fix: shut down daemon mode cleanly on SIGINT and fix CI lint failures

Ctrl-C in --daemon mode raised an uncaught KeyboardInterrupt straight
out of asyncio (headless mode has no terminal driver to intercept it
as a key event the way the interactive TUI does), dumping a raw
traceback instead of shutting down. main() now catches it, logs a
shutdown message, and closes each tab's DB and replay connections.

Also fixes ruff format/lint failures from the previous commit: an
unused cast import, a wrapped line exceeding the length limit, and
reformatting a dict comprehension.

* fix: resolve basedpyright type errors introduced by the cast() removal

The project's actual CI type checker is basedpyright, not mypy (which
I'd been validating against) - it correctly flagged 7 real regressions
from the earlier cast-removal pass: dict/list invariance breaks when a
narrower, verified-accurate type (e.g. proxysql_command_stats, which
is always int/str per its query's schema) is assigned from a wider
generic source like fetchall()'s DatabaseRow or a replay dataclass
field.

Restores narrower field types where the wider DatabaseRow type wasn't
actually warranted (disk_io_metrics, proxysql_command_stats), and
re-introduces cast() at the handful of sites where it's the more
honest and readable choice: narrowing a verified-accurate type from a
generically-typed source (driver output, JSON-deserialized replay
data) or bridging a third-party stub (plotext, zstandard) that's
simply wrong about what it accepts.

Verified with basedpyright (0 errors, matching the pre-session
baseline), ruff format/check, and the full test suite (197 passed).

* refactor: revert CommandPalette's App narrowing back to TYPE_CHECKING + cast

The isinstance()-based version traded a zero-cost TYPE_CHECKING import
for a real one executed on every instantiation, purely to re-derive a
circular-import problem TYPE_CHECKING already avoided for free. It also
asserted an invariant that can't actually fail: DolphieApp is the only
App this program ever instantiates, and this Provider is only ever
registered with it, so the isinstance check was verification theater,
not a real safety net.

* fix: harden worker/replica reliability and show ESC hint when a panel is maximized

- Show a footer with an ESC hint when a panel is maximized via the M command
- Skip starting the replicas worker in daemon mode
- Reset all Replica dataclass fields (not a hand-kept list) on host/port change
- Add a configurable read/write timeout to Database connections, applied to
  replica polling so a stuck socket can't starve the shared poll executor
- Retry the main worker with backoff instead of leaving a tab's polling dead
  after an unhandled exception
- Build the replica poll executor eagerly to avoid a check-then-act race
- Fix a runtime crash from passing a subscripted Select[str] to query_one
2026-08-24 11:10:27 -04:00
.github Modernize metric graphs, UI architecture, and Python tooling (#123) 2026-08-24 11:10:27 -04:00
dolphie Modernize metric graphs, UI architecture, and Python tooling (#123) 2026-08-24 11:10:27 -04:00
examples Verbiage changes + daemon mode config example 2024-11-30 18:26:11 -05:00
tests Modernize metric graphs, UI architecture, and Python tooling (#123) 2026-08-24 11:10:27 -04:00
.gitignore Modernize metric graphs, UI architecture, and Python tooling (#123) 2026-08-24 11:10:27 -04:00
.python-version Modernize metric graphs, UI architecture, and Python tooling (#123) 2026-08-24 11:10:27 -04:00
Dockerfile Update packages + update Dockerfile to not use gawk 2025-07-31 00:39:51 -04:00
LICENSE Initial commit 2022-09-04 20:43:49 -04:00
pyproject.toml Modernize metric graphs, UI architecture, and Python tooling (#123) 2026-08-24 11:10:27 -04:00
README.md Modernize metric graphs, UI architecture, and Python tooling (#123) 2026-08-24 11:10:27 -04:00
uv.lock Modernize metric graphs, UI architecture, and Python tooling (#123) 2026-08-24 11:10:27 -04:00

Dolphie


Your single pane of glass for real-time analytics into MySQL/MariaDB & ProxySQL

Untitled

Installation

Requires Python 3.10+

Using PyPi

$ pip install dolphie

Using uv

$ uv tool install dolphie

Using Homebrew

If you are a Homebrew user, you can install dolphie via

$ brew install dolphie

Using Docker

$ docker pull ghcr.io/charles-001/dolphie:latest
$ docker run -dit --name dolphie ghcr.io/charles-001/dolphie:latest
$ docker exec -it dolphie dolphie --tab-setup

Usage

positional arguments:
  uri                   Use a URI string for credentials (mysql/proxysql) - format: mysql://user:password@host:port (port is optional with default 3306, or 6032 for ProxySQL)

options:
  --help                show this help message and exit
  --tab-setup           Start Dolphie by showing the Tab Setup modal instead of automatically connecting with the specified options
  -C , --cred-profile   Credential profile to use. See below for more information
  -u , --user           Username
  -p , --password       Password
  -h , --host           Hostname/IP address
  -P , --port           Port (socket has precedence)
  -S , --socket         Socket file
  -c , --config-file    Dolphie's config file to use, takes precedence over DOLPHIE_CONFIG environment variable. If neither is set, options are read from these files in the given order: ['/etc/dolphie.cnf', '/etc/dolphie/dolphie.cnf', '~/.dolphie.cnf']
  -m , --mycnf-file     MySQL config file path to use. This should use [client] section [default: ~/.my.cnf]
  -l , --login-path     Specify login path to use with mysql_config_editor's file ~/.mylogin.cnf for encrypted login credentials [default: client]
  -r , --refresh-interval
                        The time, in seconds, between each data collection and processing cycle [default: 1]
  --host-cache-file     Resolve IPs to hostnames when your DNS is unable to. Each IP/hostname pair should be on its own line using format ip=hostname [default: ~/dolphie_host_cache]
  --tab-setup-file      Specify location of file that stores the available hosts to use in Tab Setup modal [default: ~/dolphie_hosts]
  --heartbeat-table     (MySQL only) If your hosts use pt-heartbeat, specify table in format db.table to use the timestamp it has for replication lag instead of Seconds_Behind_Master from SHOW REPLICA STATUS
  --ssl-mode            Desired security state of the connection to the host. Supports: REQUIRED/VERIFY_CA/VERIFY_IDENTITY [default: OFF]
  --ssl-ca              Path to the file that contains a CA (certificate authority)
  --ssl-cert            Path to the file that contains a certificate
  --ssl-key             Path to the file that contains a private key for the certificate
  --panels              What panels to display on startup separated by a comma. Supports: ['dashboard', 'processlist', 'graphs', 'replication', 'metadata_locks', 'ddl', 'pfs_metrics', 'statements_summary', 'proxysql_hostgroup_summary', 'proxysql_mysql_query_rules', 'proxysql_command_stats'], [default: ['dashboard', 'processlist']]
  --graph-marker        What marker to use for graphs (available options: https://tinyurl.com/dolphie-markers) [default: braille]
  --pypi-repo           What PyPi repository to use when checking for a new version default: [https://pypi.org/pypi/dolphie/json]
  -H , --hostgroup      This is used for creating tabs and connecting to them for hosts you specify in Dolphie's config file under a hostgroup section. As an example, you'll have a section called [cluster1] then below it you will list each host on a new line in the format key=host (keys have no meaning). Hosts support optional port (default is whatever port parameter is) in the format host:port. You can also name the tabs by suffixing ~tab_name to the host (i.e. 1=host~tab_name)
  -R, --record          Enables recording of Dolphie's data to a replay file. Note: This can use significant disk space. Monitor accordingly!
  -D, --daemon          Starts Dolphie in daemon mode. This will not show the TUI and is designed be put into the background with whatever solution you decide to use. Automatically enables --record. This mode is solely used for recording data to a replay file
  --daemon-log-file     Full path of the log file for daemon mode
  --daemon-panels       Which panels to run queries for in daemon mode separated by a comma. This can control significant load if the queries are responsible. Dashboard/Replication panels cannot be turned off. Supports: ['processlist', 'metadata_locks', 'pfs_metrics', 'statements_summary', 'proxysql_hostgroup_summary'], [default: ['processlist', 'metadata_locks', 'pfs_metrics']]
  --replay-file         Specify the full path of the replay file to load and enable replay mode
  --replay-dir          Directory to store replay data files
  --replay-retention-hours
                        Number of hours to keep replay data. Data will be purged every hour [default: 48]
  --exclude-notify-vars
                        Dolphie will let you know when a global variable has been changed. If you have variables that change frequently and you don't want to see them, you can specify which ones with this option separated by a comma (i.e. --exclude-notify-vars=variable1,variable2)
  --filters             Start with filters applied to threads, separated by a comma in the format name=value. Supports: user, host, db, hostgroup, time (minimum query time), query (partial query text). Prefix a value with ! to exclude what it matches (i.e. --filters user=!azure_superuser,time=5). Filters set by Dolphie's config, a credential profile and this option are merged, with the more specific source winning for the filters it sets. A name with no value (i.e. time=) unsets an inherited filter
  --show-trxs-only      (MySQL only) Start with only showing threads that have an active transaction
  --additional-columns  Start with additional columns in Processlist panel
  --debug-options       Display options that are set and what they're set by (command-line, dolphie config, etc) then exit. WARNING: This will show passwords and other sensitive information in plain text
  -V, --version         Display version and exit

Order of precedence for methods that pass options to Dolphie:
	1. Command-line
	2. Credential profile (set by --cred-profile)
	3. Environment variables
	4. Dolphie's config (set by --config-file OR DOLPHIE_CONFIG)
	5. ~/.mylogin.cnf (mysql_config_editor)
	6. ~/.my.cnf (set by --mycnf-file)

Credential profiles can be defined in Dolphie's config file as a way to store credentials for easy access.
A profile can be created by adding a section in the config file with the format: [credential_profile_<name>]
When using a credential profile, do not include the prefix 'credential_profile' (i.e. -C production)
The following options are supported in credential profiles:
	host
	port (default is 3306)
	user
	password
	socket
	ssl_mode REQUIRED/VERIFY_CA/VERIFY_IDENTITY
	ssl_ca
	ssl_cert
	ssl_key
	tab_title
	filters
	mycnf_file
	login_path

MySQL my.cnf file supports these options under [client] section:
	host
	user
	password
	port
	socket
	ssl_mode REQUIRED/VERIFY_CA/VERIFY_IDENTITY
	ssl_ca
	ssl_cert
	ssl_key

Login path file supports these options:
	host
	user
	password
	port
	socket

Environment variables support these options:
	DOLPHIE_USER
	DOLPHIE_PASSWORD
	DOLPHIE_HOST
	DOLPHIE_PORT
	DOLPHIE_SOCKET
	DOLPHIE_SSL_MODE REQUIRED/VERIFY_CA/VERIFY_IDENTITY
	DOLPHIE_SSL_CA
	DOLPHIE_SSL_CERT
	DOLPHIE_SSL_KEY
	DOLPHIE_CONFIG

Dolphie's config supports these options under [dolphie] section:
	(bool) tab_setup
	(str) credential_profile
	(str) user
	(str) password
	(str) host
	(int) port
	(str) socket
	(str) ssl_mode
	(str) ssl_ca
	(str) ssl_cert
	(str) ssl_key
	(str) mycnf_file
	(str) login_path
	(str) host_cache_file
	(str) tab_setup_file
	(int) refresh_interval
	(str) heartbeat_table
	(comma-separated str) startup_panels
	(str) graph_marker
	(str) pypi_repository
	(str) hostgroup
	(bool) show_trxs_only
	(bool) show_additional_query_columns
	(comma-separated str) filters
	(bool) record_for_replay
	(bool) daemon_mode
	(comma-separated str) daemon_mode_panels
	(str) daemon_mode_log_file
	(str) replay_file
	(str) replay_dir
	(int) replay_retention_hours
	(comma-separated str) exclude_notify_global_vars

Supported MySQL versions

  • MySQL/Percona Server 5.6/5.7/8.x/9.x
  • AWS RDS/Aurora
  • Azure MySQL

Supported MariaDB versions

  • MariaDB 5.5/10.0/11.0+
  • AWS RDS
  • Azure MariaDB

Supported ProxySQL versions

  • ProxySQL 2.6+ (could work on previous versions but not tested)

Note: Use admin user instead of stats user so you can use all features

MySQL Grants required

Least privilege

  1. PROCESS (only if you switch to using processlist via P command)
  2. SELECT to performance_schema + pt-heartbeat table (if used)
  3. REPLICATION CLIENT/REPLICATION SLAVE
  1. PROCESS (only if you switch to using processlist via P command)
  2. Global SELECT access (good for explaining queries, listing all databases, etc)
  3. REPLICATION CLIENT/REPLICATION SLAVE
  4. SUPER (required if you want to kill queries)

Record & Replay

Dolphie is capable of recording your live session data that can be used in a future replay if needed. To begin recording, specify the --record option along with --replay-dir and you're good to go! The data will be saved in a SQLite database compressed with ZSTD for efficient storage management.

To view a replay from either a live session or daemon mode, specify the --replay-file option or bring up the Tab Setup modal. Replays enable you to navigate through the recorded data as if you were observing Dolphie in real-time at the exact time you need to investigate. The replay interface features intuitive controls for stepping backward, moving forward, playing/pausing, and jumping to specific timestamps. While some commands or features may be restricted in replay mode, all core functionalities for effective review and troubleshooting remain accessible.

Daemon Mode

If you need Dolphie running incognito while always recording data to capture those critical moments when a database stall causes an incident or a tricky performance issue slips past other monitoring tools, then look no further! Daemon mode is the solution. Purpose-built for nonstop recording, it ensures you never miss the insights that matter most.

To activate Daemon mode, specify the --daemon option, which will automatically enable --record. This will transform Dolphie into a resource-efficient, passive, always-on monitoring process that continuously records data. It removes Textual's TUI and creates a log file for messages while also printing them to the console.

To run Dolphie in the background using daemon mode, I recommend systemctl for its flexibility and management capabilities. To see how to set that up, refer to the service configuration example. While alternatives like nohup or tmux can be used, they are not advisable due to their limited management features. Additionally, check out the config example as a helpful starting point for setting up this mode.

In Daemon mode, metrics are retained for the last 10 minutes to support graphing, with performance schema metric deltas automatically reset at 10-minute intervals. This approach keeps data fresh and relevant, providing an accurate view of recent activity.

Note: Daemon mode's replay file can consume significant disk space, particularly on busy servers. To minimize disk usage, adjust the --replay-retention-hours and --refresh-interval options to control data retention and collection frequency.

Example log messages in daemon mode:

[INFO] Starting Dolphie in daemon mode with a refresh interval of 1s
[INFO] Log file: /var/log/dolphie/dolphie.log
[INFO] Connected to MySQL with Process ID 324
[INFO] Replay SQLite file: /var/lib/dolphie/replays/localhost/daemon.db (24 hours retention)
[INFO] Connected to SQLite
[INFO] Replay database metadata - Host: localhost, Port: 3306, Source: MySQL (Percona Server), Dolphie: 6.3.0
[INFO] ZSTD compression dictionary trained with 10 samples (size: 52.56KB)
[WARNING] Read-only mode changed: R/W -> RO
[INFO] Global variable innodb_io_capacity changed: 1000 -> 2000

System Utilization in the Dashboard Panel

The System Utilization section in the Dashboard panel will only display when Dolphie is running on the same host as the server you're connected to. It displays the following information:

  • Uptime
  • CPU Usage (Percentage, # of cores)
  • Load Averages (1, 5, and 15 minutes)
  • Memory Usage (Percentage, Used/Total)
  • Swap Usage (Used/Total)
  • Network Traffic (Down, Up)

Example:

Screenshot 2024-11-03 at 1 19 00 AM

Credential Profiles

Credential profiles can be defined in Dolphie's config file as a way to store credentials for easy access. A profile can be created by adding a section in the config file with the format: [credential_profile_<name>]

The following options are supported in credential profiles:

  • host
  • port (default is 3306)
  • user
  • password
  • socket
  • ssl_mode REQUIRED/VERIFY_CA/VERIFY_IDENTITY
  • ssl_ca
  • ssl_cert
  • ssl_key
  • tab_title
  • filters
  • mycnf_file
  • login_path

filters starts the profile's tabs with filters applied to threads, separated by a comma in the format name=value. It supports user, host, db, hostgroup, time (minimum query time) and query (partial query text). Prefix a value with ! to exclude what it matches, the same as the filter modal (f) does.

Filters merge from the least specific source to the most: Dolphie's config, then the credential profile, then --filters. Each one only overrides the filters it sets, so a profile that sets user keeps a time filter that Dolphie's config set. To drop an inherited filter instead, give its name with no value (i.e. time=).

Example:

[credential_profile_dev]
user = dev_user
password = dev_password

[credential_profile_prod]
mycnf_file = /secure/path/to/prod.cnf
filters = user=!azure_superuser

To use a credential profile, you can specify it with -C/--cred-profile option without using the prefix credential_profile (i.e. -C prod) when starting Dolphie. Hostgroups can also use credential profiles (see below)

Hostgroups

Hostgroups are a way to easily connect to multiple hosts at once. To set this up, you will create a section in Dolphie's config file with the name you want the hostgroup to be and list each host on a new line in the format key=<json> (keys have no meaning). Hosts support optional port (default is whatever port parameter is) in the format host:port. Once ready, you will use the parameter hostgroup or Host Setup modal to see it in action!

Note: Colors can be used in the tab name by using the format [color]text[/color] (i.e. [red]production[/red]). You can also use emojis supported by Rich (can see them by running python -m rich.emoji) by using the format :emoji: (i.e. :ghost:). Rich supports the normal emoji shortcodes.

Example:

[cluster1]
1={"host": "host1", "tab_title": "[yellow]host1[/yellow] :ghost:", "credential_profile": "dev"}
2={"host": "host2", "tab_title": "[blue]host2[/blue] :ghost:", "credential_profile": "dev"}
3={"host": "host3:3307", "tab_title": "[red]production[/red]", "credential_profile": "prod"}
4={"host": "host4"}

Development

uv sync --all-groups
uv run dolphie --help
uv run ruff format .
uv run ruff check .
uv run basedpyright
uv run pytest

BasedPyright runs without a baseline; new diagnostics must be fixed rather than suppressed as accepted debt.

Feedback

I welcome all questions, bug reports, and requests. If you enjoy Dolphie, please let me know! I'd love to hear from you 🐬