import importlib import time import unittest from typing import Set, Tuple from unittest.mock import patch, MagicMock from o11ydiagnosis.subtasks.slowlogconverter.metrics import Metrics from o11ydiagnosis.subtasks.slowlogconverter.task import log_metrics_periodically # The slowlogconverter package __init__ does `from .metrics import metrics`, so # `o11ydiagnosis.subtasks.slowlogconverter.metrics` is the singleton, not metrics.py. # Patch the real metrics module object from sys.modules. _METRICS_MODULE = importlib.import_module("o11ydiagnosis.subtasks.slowlogconverter.metrics") def _remove_all_labelsets(metric) -> None: """Drop all label combinations for a Prometheus labeled metric (test cleanup).""" labelnames = getattr(metric, "_labelnames", ()) seen: Set[Tuple[str, ...]] = set() for family in metric.collect(): for sample in family.samples: if sample.name.endswith("_created"): continue if not sample.labels: continue key = tuple(sample.labels.get(ln) for ln in labelnames) if None in key or key in seen: continue seen.add(key) try: metric.remove(*key) except KeyError: pass class TestLogMetricsPeriodically(unittest.TestCase): def setUp(self): # Create a real metrics instance self.real_metrics = Metrics() # Mock the log object self.mock_log = MagicMock() # Patch the modules self.patchers = [ patch.object(_METRICS_MODULE, "metrics", self.real_metrics), patch("o11ydiagnosis.subtasks.slowlogconverter.task.metrics", self.real_metrics), patch('o11ydiagnosis.subtasks.slowlogconverter.task.log', self.mock_log), patch('time.time', return_value=1000), patch('pympler.muppy.get_objects', return_value=[]), patch('pympler.summary.summarize', return_value=[]), # Prevent the timer from actually starting during tests patch('threading.Timer', side_effect=lambda *args, **kwargs: MagicMock()) ] for patcher in self.patchers: patcher.start() def tearDown(self): # Clean up all metrics after each test for patcher in self.patchers: patcher.stop() # Clear any remaining metrics self._clear_metrics() def _clear_metrics(self): """Helper method to clear all metrics""" for metric in [ "processed_hour", "logs_written", "logs_total", "logs_parse_error", "logs_written_1m", "last_update", "last_optimize_timestamp", "latest_record_timestamp", ]: _remove_all_labelsets(getattr(self.real_metrics, metric)) # Clear cluster hours self.real_metrics._cluster_hours.clear() def test_no_clusters(self): """Test when there are no clusters with metrics""" # Call the function directly log_metrics_periodically() # Should log that no metrics are available self.mock_log.info.assert_called_with("[Metrics] Logs written in last 1 minute: 0") def test_active_cluster_metrics(self): """Test with active clusters that shouldn't be cleaned up""" # Setup active cluster (updated within last 15 minutes) cluster = "test-cluster" # Record some metrics for the cluster self.real_metrics.record_logs_written(1, cluster, "2023010101", 1000, 1000, 0) # Call the function log_metrics_periodically() # Should log the cluster's metrics self.mock_log.info.assert_called_with("[Metrics] Logs written in last 1 minute: 1") # Verify no metrics were removed for active cluster self.assertTrue(self.real_metrics.cluster_has_last_update_series(cluster)) def test_stale_cluster_cleanup(self): """Test that stale clusters are cleaned up""" # Setup clusters stale_cluster = "stale-cluster" active_cluster = "active-cluster" # Record logs for both clusters self.real_metrics.record_logs_written(1, stale_cluster, "2023010101", 100, 100, 0, 99) # Old timestamp self.real_metrics.record_logs_written(1, active_cluster, "2023010102", 1000, 1000, 0, 100) # Current timestamp # Call the function log_metrics_periodically() # Verify the cleanup log message self.mock_log.info.assert_any_call( "[Metrics] Cleaning up stale metrics for cluster stale-cluster (inactive for >15 minutes)" ) import re found = False for call in self.mock_log.info.call_args_list: if re.search(r'\[Metrics\] Oldest processed hour - Cluster: active-cluster, UTC Hour: \d{10}', call[0][0]): found = True break self.assertTrue(found, "Expected log message about oldest processed hour not found") def test_multiple_active_clusters(self): """Test with multiple active clusters""" # Setup multiple active clusters for i in range(3): cluster = f"cluster-{i}" self.real_metrics.record_logs_written(1, cluster, f"202301010{i}", 1000, 1000, 0) log_metrics_periodically() # Should log the oldest cluster import re # 检查日志中是否包含预期的消息模式 found = False for call in self.mock_log.info.call_args_list: if re.search(r'\[Metrics\] Oldest processed hour - Cluster: cluster-0, UTC Hour: \d{10}', call[0][0]): found = True break self.assertTrue(found, "Expected log message about oldest processed hour not found") self.mock_log.info.assert_any_call("[Metrics] Logs written in last 1 minute: 3") if __name__ == '__main__': unittest.main()