feat: Grafana copy publishing, dashboard examples, and optional OTel metrics

Implements three observability improvements:

#82 — publish_grafana_copy(): Uses SQLite online backup API (WAL-safe) to
atomically publish a consistent read-only copy beside the target. Adds
--publish-copy option to grafana-schema and snapshot CLI commands.

#83 — examples/grafana/: Minimal working Grafana setup with docker-compose,
provisioning datasource/dashboard YAML, and three dashboard JSON files
(mt5cli-overview, mt5cli-trades, mt5cli-market). All queries use grafana_*
views; no credentials or private paths included.

#84 — mt5cli/telemetry.py: Optional OTel metrics behind mt5cli[otel] extra.
Base install is unaffected. Adds _Mt5Metrics singleton (no-op until
configure_metrics() is called), wraps update_history() and
update_observability() with record_history_update / record_snapshot_update
context managers, and emits account/position gauges from snapshots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
agent
2026-06-27 23:17:13 +00:00
parent d27da02f3f
commit 4c2e25af8a
18 changed files with 1609 additions and 35 deletions
+71
View File
@@ -2022,6 +2022,77 @@ class TestSnapshotCommand:
assert result.exit_code == 0, result.output
assert updater.call_args.kwargs["with_grafana_schema"] is False
def test_snapshot_with_publish_copy(
self,
tmp_path: Path,
mocker: MockerFixture,
) -> None:
"""--publish-copy calls publish_grafana_copy after update_observability."""
mocker.patch("mt5cli.cli.sdk.update_observability_with_config")
mock_publish = mocker.patch("mt5cli.grafana.publish_grafana_copy")
copy_path = tmp_path / "grafana.db"
result = runner.invoke(
app,
[
"-o",
str(tmp_path / "out.db"),
"snapshot",
"--publish-copy",
str(copy_path),
],
)
assert result.exit_code == 0, result.output
mock_publish.assert_called_once()
def test_snapshot_no_publish_copy_by_default(
self,
tmp_path: Path,
mocker: MockerFixture,
) -> None:
"""Snapshot does not call publish_grafana_copy without --publish-copy."""
mocker.patch("mt5cli.cli.sdk.update_observability_with_config")
mock_publish = mocker.patch("mt5cli.grafana.publish_grafana_copy")
result = runner.invoke(app, ["-o", str(tmp_path / "out.db"), "snapshot"])
assert result.exit_code == 0, result.output
mock_publish.assert_not_called()
class TestGrafanaSchemaPublishCopy:
"""Tests for grafana-schema --publish-copy option."""
def test_grafana_schema_with_publish_copy(
self,
tmp_path: Path,
mocker: MockerFixture,
) -> None:
"""grafana-schema --publish-copy calls publish_grafana_copy."""
mock_publish = mocker.patch("mt5cli.grafana.publish_grafana_copy")
output = tmp_path / "out.db"
copy_path = tmp_path / "grafana.db"
result = runner.invoke(
app,
[
"-o",
str(output),
"grafana-schema",
"--publish-copy",
str(copy_path),
],
)
assert result.exit_code == 0, result.output
mock_publish.assert_called_once()
def test_grafana_schema_no_publish_copy_by_default(
self,
tmp_path: Path,
mocker: MockerFixture,
) -> None:
"""grafana-schema does not call publish_grafana_copy by default."""
mock_publish = mocker.patch("mt5cli.grafana.publish_grafana_copy")
result = runner.invoke(app, ["-o", str(tmp_path / "out.db"), "grafana-schema"])
assert result.exit_code == 0, result.output
mock_publish.assert_not_called()
class TestMain:
"""Tests for the main entry point."""