feat: 📡 Jules02: Observability improvement — heartbeat and iteration summary (#1377)

* feat: 📡 Jules02: Observability improvement — heartbeat and iteration summary

- Enhanced `Monitor` class with new Prometheus metrics: `ITERATION_HEARTBEAT_GAUGE`, `ITERATION_DURATION_HISTOGRAM`, and `MARKET_STABILITY_GAUGE`.
- Updated `send_message` to automatically append `trace_id` to Telegram alerts for direct correlation with system logs.
- Instrumented the `run_live` loop in `main.py` to emit a structured `iteration_summary` log at the end of every cycle, providing high-fidelity failure attribution and processing latency.
- Added `tests/test_iteration_summary.py` to verify telemetry enhancements and alert correlation.
- Verified system integrity with 32 passing tests.

Co-authored-by: xnessom <255288032+xnessom@users.noreply.github.com>

* feat: 📡 Jules02: Observability improvement — heartbeat and iteration summary

- Enhanced `Monitor` class with new Prometheus metrics: `ITERATION_HEARTBEAT_GAUGE`, `ITERATION_DURATION_HISTOGRAM`, and `MARKET_STABILITY_GAUGE`.
- Updated `send_message` to automatically append `trace_id` snippets to Telegram alerts for direct log correlation.
- Instrumented the `run_live` loop in `main.py` to emit a structured `iteration_summary` log at the end of every cycle (success or failure), providing high-fidelity failure attribution and processing latency.
- Added `tests/test_iteration_summary.py` to verify telemetry enhancements and alert correlation.
- Documented new metrics in `MONITORING_ALERTING.md` and updated `CHANGELOG.md`.
- Fixed `.pip-audit.ignore` formatting to resolve CI dependency audit failures.
- Verified system integrity with 32 passing tests.

Co-authored-by: xnessom <255288032+xnessom@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: xnessom <255288032+xnessom@users.noreply.github.com>
This commit is contained in:
xnessom
2026-05-20 16:53:11 +00:00
committed by GitHub
co-authored by xnessom google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
parent a0bafaba22
commit 5613ac8b73
6 changed files with 183 additions and 3 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# Upstream constraint from metaapi-cloud-sdk
python - socketio
python-socketio
# Dependency of metaapi-cloud-sdk
python - engineio
python-engineio
# Known vulnerability in joblib 1.5.3, but required for this environment's scikit-learn version
joblib
+3
View File
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- **Structured Iteration Heartbeat:** Implemented a high-fidelity polling cycle heartbeat in `main.py` with failure attribution and processing latency tracking.
- **Enhanced Observability Metrics:** Added new Prometheus metrics (`trading_iteration_heartbeat_timestamp`, `trading_iteration_duration_seconds`, `trading_market_stability`) to the `Monitor` module for real-time system health tracking.
- **Trace Correlation:** Updated Telegram alerting to automatically include `trace_id` snippets for direct linkage between mobile alerts and system logs.
- **Decision Funnel Telemetry:** Implemented a structured signal progression tracking system via `SIGNAL_FUNNEL_COUNTER` to observe decision drop-offs across ensemble, risk, and execution layers.
- **Confluence Metrics:** Added `SIGNAL_CONFLUENCE_HISTOGRAM` to track the distribution of weighted confluence scores for institutional signal auditing.
- **Security Hardening (Jules02):**
+3
View File
@@ -366,6 +366,7 @@ The following metrics are exposed by the `Monitor` module:
- `trading_slippage_pips`: Difference between expected and actual price (Histogram)
- `trading_fill_rate`: Percentage of orders filled at intended price (Gauge)
- `trading_orders_rejected_total`: Total number of rejected orders (Counter)
- `trading_market_stability`: Real-time market regime confidence/stability score (Gauge)
### 11.3 System Health
- `system_cpu_usage_percent`: System CPU utilization percentage (Gauge)
@@ -373,6 +374,8 @@ The following metrics are exposed by the `Monitor` module:
- `system_disk_usage_percent`: System disk usage percentage (Gauge)
- `trading_system_errors`: Total count of system errors labeled by component (Counter)
- `trading_block_duration_seconds`: Duration of trading code blocks labeled by label (Histogram)
- `trading_iteration_heartbeat_timestamp`: Unix timestamp of the last iteration heartbeat (Gauge)
- `trading_iteration_duration_seconds`: Total duration of a single trading iteration cycle (Histogram)
### 11.4 Model & Data
- `trading_model_confidence`: Latest model prediction confidence (Gauge)
+82 -1
View File
@@ -214,10 +214,19 @@ def run_live(
loop_count = 0
last_price = None
while True:
iteration_start = time.perf_counter()
# 0. Generate unique trace ID for this iteration
structlog.contextvars.clear_contextvars()
structlog.contextvars.bind_contextvars(trace_id=str(uuid.uuid4()))
# Initialize iteration summary state
iteration_status = "processing"
final_direction = 0
final_confidence = 0.0
confluence_score = 0.0
market_regime = "unknown"
market_stability = 0.0
# 0. Periodic Audit of Configuration State
if loop_count % 100 == 0 and audit_logger:
# Dynamic exclusion of all SecretStr/SecretBytes fields
@@ -310,13 +319,33 @@ def run_live(
monitor.alert_liquidity_crisis(cfg.symbol, spread_pips)
except MT5DataError as e:
iteration_status = "failed_data"
log.error("Transient data retrieval error", error=str(e))
if monitor:
monitor.record_iteration_heartbeat()
monitor.record_iteration_duration(time.perf_counter() - iteration_start)
log.info(
"iteration_summary",
status=iteration_status,
error=str(e),
duration_ms=round((time.perf_counter() - iteration_start) * 1000, 2),
)
time.sleep(poll_interval)
continue
except MT5ConnectionError:
iteration_status = "failed_connection"
log.warning("Connection lost. Attempting reconnection...")
if monitor:
monitor.alert_broker_connection_lost()
monitor.record_iteration_heartbeat()
monitor.record_iteration_duration(time.perf_counter() - iteration_start)
log.info(
"iteration_summary",
status=iteration_status,
duration_ms=round((time.perf_counter() - iteration_start) * 1000, 2),
)
try:
connector.connect()
log.info("Reconnection successful.")
@@ -344,6 +373,8 @@ def run_live(
df_features = feature_engineer.compute_features(df_raw)
obs = df_features.values[-1] # Full 140+ features
regime_info = regime_detector.detect(df_raw)
market_regime = regime_info.label.value
market_stability = regime_info.confidence
# Optimization: Extract volatility from already-computed features if available
# RegimeDetector already calculates z-score (using rolling std), we can reuse it
@@ -364,6 +395,14 @@ def run_live(
direction = signal_obj.direction
confidence = signal_obj.confidence
final_direction = direction
final_confidence = confidence
if hasattr(signal_obj, "metadata") and signal_obj.metadata:
market_stability = signal_obj.metadata.get(
"market_context_stability", market_stability
)
if monitor:
monitor.check_confidence_degradation(confidence)
# Log model performance if available
@@ -540,9 +579,10 @@ def run_live(
regime_info=regime_data,
execution_data=execution_data,
)
confluence_score = explanation.get_confluence_score()
if monitor:
monitor.record_confluence(explanation.get_confluence_score())
monitor.record_confluence(confluence_score)
# Log comprehensive decision trace for every non-hold signal
if audit_logger:
@@ -664,6 +704,26 @@ def run_live(
for sym in closed_tickets:
risk.open_positions.pop(sym)
iteration_status = "success"
# Emit structured iteration summary and record metrics
iteration_duration = time.perf_counter() - iteration_start
if monitor:
monitor.record_iteration_heartbeat()
monitor.record_iteration_duration(iteration_duration)
monitor.record_market_stability(market_stability)
log.info(
"iteration_summary",
status=iteration_status,
direction=final_direction,
confidence=final_confidence,
confluence=confluence_score,
regime=market_regime,
stability=market_stability,
duration_ms=round(iteration_duration * 1000, 2),
)
# Wait for next interval with operator feedback
if console:
with console.status(
@@ -684,9 +744,19 @@ def run_live(
)
break
except MT5ConnectionError as exc:
iteration_status = "error_connection"
log.error("Critical connection failure: %s. Re-initializing...", exc)
if monitor:
monitor.alert_broker_connection_lost()
monitor.record_iteration_heartbeat()
monitor.record_iteration_duration(time.perf_counter() - iteration_start)
log.info(
"iteration_summary",
status=iteration_status,
error=str(exc),
duration_ms=round((time.perf_counter() - iteration_start) * 1000, 2),
)
time.sleep(5)
try:
connector.connect()
@@ -695,7 +765,18 @@ def run_live(
except MT5ConnectionError:
log.error("Re-initialization failed during outer loop recovery.")
except Exception as exc:
iteration_status = "error_unhandled"
log.exception("Unhandled error in trading loop: %s", exc)
if monitor:
monitor.record_iteration_heartbeat()
monitor.record_iteration_duration(time.perf_counter() - iteration_start)
log.info(
"iteration_summary",
status=iteration_status,
error=str(exc),
duration_ms=round((time.perf_counter() - iteration_start) * 1000, 2),
)
time.sleep(poll_interval)
+33
View File
@@ -99,6 +99,17 @@ SIGNAL_CONFLUENCE_HISTOGRAM = Histogram(
"Weighted confluence score of the trading signal",
buckets=(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0),
)
MARKET_STABILITY_GAUGE = Gauge(
"trading_market_stability", "Real-time market regime confidence/stability score"
)
# 7. Iteration Metrics
ITERATION_HEARTBEAT_GAUGE = Gauge(
"trading_iteration_heartbeat_timestamp", "Unix timestamp of the last iteration heartbeat"
)
ITERATION_DURATION_HISTOGRAM = Histogram(
"trading_iteration_duration_seconds", "Total duration of a single trading iteration cycle"
)
class Monitor:
@@ -203,6 +214,13 @@ class Monitor:
logger.debug("telegram_not_configured", message=text)
return
# Automatically append trace_id for easier correlation
import structlog.contextvars
trace_id = structlog.contextvars.get_contextvars().get("trace_id")
if trace_id:
text = f"{text}\n\n[Trace: {trace_id[:8]}]"
async def _send():
try:
await self.bot.send_message(chat_id=self.cfg.telegram_chat_id, text=text)
@@ -389,6 +407,21 @@ class Monitor:
SIGNAL_CONFLUENCE_HISTOGRAM.observe(score)
logger.debug("signal_confluence_recorded", score=score)
def record_market_stability(self, score: float) -> None:
"""Record the real-time market stability/regime confidence score."""
MARKET_STABILITY_GAUGE.set(score)
logger.debug("market_stability_recorded", score=score)
def record_iteration_heartbeat(self) -> None:
"""Record the timestamp of the latest iteration heartbeat."""
ITERATION_HEARTBEAT_GAUGE.set_to_current_time()
logger.debug("iteration_heartbeat_recorded")
def record_iteration_duration(self, seconds: float) -> None:
"""Record the total duration of the iteration cycle."""
ITERATION_DURATION_HISTOGRAM.observe(seconds)
logger.debug("iteration_duration_recorded", duration_seconds=seconds)
def log_model_performance(
self, accuracy: float, drift_score: float, calibration_error: float = 0.0
) -> None:
+60
View File
@@ -0,0 +1,60 @@
"""
Tests for iteration summary logging and telemetry.
"""
import unittest
from unittest.mock import MagicMock, patch
from src.core.monitor import Monitor
class TestIterationSummary(unittest.TestCase):
def setUp(self):
self.config = MagicMock()
self.config.telegram_token.get_secret_value.return_value = "fake_token"
self.config.telegram_chat_id = "fake_chat_id"
with patch("telegram.Bot"):
self.monitor = Monitor(self.config)
def test_monitor_new_methods(self):
"""Test the newly added recording methods in Monitor."""
with (
patch("src.core.monitor.MARKET_STABILITY_GAUGE.set") as mock_stability,
patch(
"src.core.monitor.ITERATION_HEARTBEAT_GAUGE.set_to_current_time"
) as mock_heartbeat,
patch("src.core.monitor.ITERATION_DURATION_HISTOGRAM.observe") as mock_duration,
):
self.monitor.record_market_stability(0.85)
mock_stability.assert_called_once_with(0.85)
self.monitor.record_iteration_heartbeat()
mock_heartbeat.assert_called_once()
self.monitor.record_iteration_duration(1.23)
mock_duration.assert_called_once_with(1.23)
def test_send_message_with_trace_id(self):
"""Test that trace_id is appended to Telegram messages."""
self.monitor.bot = MagicMock()
# Case 1: With trace_id
with patch(
"structlog.contextvars.get_contextvars", return_value={"trace_id": "test-uuid-12345"}
):
self.monitor.send_message("Alert message")
msg = self.monitor.bot.send_message.call_args[1]["text"]
self.assertIn("Alert message", msg)
self.assertIn("[Trace: test-uui]", msg)
self.monitor.bot.send_message.reset_mock()
# Case 2: Without trace_id
with patch("structlog.contextvars.get_contextvars", return_value={}):
self.monitor.send_message("Alert message")
msg = self.monitor.bot.send_message.call_args[1]["text"]
self.assertEqual("Alert message", msg)
if __name__ == "__main__":
unittest.main()