ced4bdb737
- New whisper_live/metrics.py with Counter, Gauge, Histogram metrics - Track connections (opened/closed/rejected), transcription latency, audio processed, segments emitted, REST requests, and errors - All metric helpers are no-ops when prometheus_client not installed - --metrics_port CLI flag to expose /metrics endpoint (0 = disabled) - Metrics integrated into server.py, base.py at key instrumentation points - 17 new tests in tests/test_metrics.py (178 total passing)
138 lines
5.7 KiB
Python
138 lines
5.7 KiB
Python
import unittest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from whisper_live import metrics as wl_metrics
|
|
|
|
_skip_no_prometheus = unittest.skipUnless(
|
|
wl_metrics.is_available(), "prometheus_client not installed"
|
|
)
|
|
|
|
|
|
class TestMetricsAvailability(unittest.TestCase):
|
|
def test_is_available_returns_bool(self):
|
|
self.assertIsInstance(wl_metrics.is_available(), bool)
|
|
|
|
|
|
@_skip_no_prometheus
|
|
class TestTrackConnectionOpened(unittest.TestCase):
|
|
def test_increments_total_and_active(self):
|
|
total_before = wl_metrics.CONNECTIONS_TOTAL._value.get()
|
|
active_before = wl_metrics.CONNECTIONS_ACTIVE._value.get()
|
|
wl_metrics.track_connection_opened()
|
|
self.assertEqual(wl_metrics.CONNECTIONS_TOTAL._value.get(), total_before + 1)
|
|
self.assertEqual(wl_metrics.CONNECTIONS_ACTIVE._value.get(), active_before + 1)
|
|
|
|
|
|
@_skip_no_prometheus
|
|
class TestTrackConnectionClosed(unittest.TestCase):
|
|
def test_decrements_active(self):
|
|
wl_metrics.track_connection_opened()
|
|
active_before = wl_metrics.CONNECTIONS_ACTIVE._value.get()
|
|
wl_metrics.track_connection_closed()
|
|
self.assertEqual(wl_metrics.CONNECTIONS_ACTIVE._value.get(), active_before - 1)
|
|
|
|
|
|
@_skip_no_prometheus
|
|
class TestTrackConnectionRejected(unittest.TestCase):
|
|
def test_rejected_full(self):
|
|
before = wl_metrics.CONNECTIONS_REJECTED.labels(reason="full")._value.get()
|
|
wl_metrics.track_connection_rejected(reason="full")
|
|
self.assertEqual(wl_metrics.CONNECTIONS_REJECTED.labels(reason="full")._value.get(), before + 1)
|
|
|
|
def test_rejected_auth(self):
|
|
before = wl_metrics.CONNECTIONS_REJECTED.labels(reason="auth")._value.get()
|
|
wl_metrics.track_connection_rejected(reason="auth")
|
|
self.assertEqual(wl_metrics.CONNECTIONS_REJECTED.labels(reason="auth")._value.get(), before + 1)
|
|
|
|
|
|
@_skip_no_prometheus
|
|
class TestTrackTranscriptionLatency(unittest.TestCase):
|
|
def test_observe_records_value(self):
|
|
count_before = wl_metrics.TRANSCRIPTION_LATENCY._sum.get()
|
|
wl_metrics.track_transcription_latency(0.5)
|
|
self.assertAlmostEqual(wl_metrics.TRANSCRIPTION_LATENCY._sum.get(), count_before + 0.5, places=3)
|
|
|
|
|
|
@_skip_no_prometheus
|
|
class TestTrackAudioProcessed(unittest.TestCase):
|
|
def test_increments_by_duration(self):
|
|
before = wl_metrics.AUDIO_PROCESSED._value.get()
|
|
wl_metrics.track_audio_processed(3.5)
|
|
self.assertAlmostEqual(wl_metrics.AUDIO_PROCESSED._value.get(), before + 3.5, places=3)
|
|
|
|
|
|
@_skip_no_prometheus
|
|
class TestTrackSegmentEmitted(unittest.TestCase):
|
|
def test_completed_true(self):
|
|
before = wl_metrics.SEGMENTS_EMITTED.labels(completed="true")._value.get()
|
|
wl_metrics.track_segment_emitted(completed=True)
|
|
self.assertEqual(wl_metrics.SEGMENTS_EMITTED.labels(completed="true")._value.get(), before + 1)
|
|
|
|
def test_completed_false(self):
|
|
before = wl_metrics.SEGMENTS_EMITTED.labels(completed="false")._value.get()
|
|
wl_metrics.track_segment_emitted(completed=False)
|
|
self.assertEqual(wl_metrics.SEGMENTS_EMITTED.labels(completed="false")._value.get(), before + 1)
|
|
|
|
|
|
@_skip_no_prometheus
|
|
class TestTrackRestRequest(unittest.TestCase):
|
|
def test_tracks_200(self):
|
|
before = wl_metrics.REST_REQUESTS.labels(endpoint="transcriptions", status="200")._value.get()
|
|
wl_metrics.track_rest_request(endpoint="transcriptions", status=200)
|
|
self.assertEqual(wl_metrics.REST_REQUESTS.labels(endpoint="transcriptions", status="200")._value.get(), before + 1)
|
|
|
|
def test_tracks_500(self):
|
|
before = wl_metrics.REST_REQUESTS.labels(endpoint="transcriptions", status="500")._value.get()
|
|
wl_metrics.track_rest_request(endpoint="transcriptions", status=500)
|
|
self.assertEqual(wl_metrics.REST_REQUESTS.labels(endpoint="transcriptions", status="500")._value.get(), before + 1)
|
|
|
|
|
|
@_skip_no_prometheus
|
|
class TestTrackError(unittest.TestCase):
|
|
def test_tracks_transcription_error(self):
|
|
before = wl_metrics.ERRORS.labels(type="transcription")._value.get()
|
|
wl_metrics.track_error("transcription")
|
|
self.assertEqual(wl_metrics.ERRORS.labels(type="transcription")._value.get(), before + 1)
|
|
|
|
def test_tracks_rest_error(self):
|
|
before = wl_metrics.ERRORS.labels(type="rest_transcription")._value.get()
|
|
wl_metrics.track_error("rest_transcription")
|
|
self.assertEqual(wl_metrics.ERRORS.labels(type="rest_transcription")._value.get(), before + 1)
|
|
|
|
|
|
class TestStartMetricsServer(unittest.TestCase):
|
|
@patch("whisper_live.metrics.start_http_server")
|
|
def test_starts_on_given_port(self, mock_start):
|
|
wl_metrics.start_metrics_server(9999)
|
|
mock_start.assert_called_once_with(9999)
|
|
|
|
@patch("whisper_live.metrics.start_http_server", side_effect=OSError("port in use"))
|
|
def test_logs_error_on_failure(self, mock_start):
|
|
with self.assertLogs(level="ERROR") as cm:
|
|
wl_metrics.start_metrics_server(9999)
|
|
self.assertTrue(any("Failed to start" in msg for msg in cm.output))
|
|
|
|
|
|
class TestNoOpWhenUnavailable(unittest.TestCase):
|
|
"""Verify helper functions are no-ops when _AVAILABLE is False."""
|
|
|
|
def test_all_helpers_are_noop(self):
|
|
original = wl_metrics._AVAILABLE
|
|
try:
|
|
wl_metrics._AVAILABLE = False
|
|
# None of these should raise
|
|
wl_metrics.track_connection_opened()
|
|
wl_metrics.track_connection_closed()
|
|
wl_metrics.track_connection_rejected("full")
|
|
wl_metrics.track_transcription_latency(1.0)
|
|
wl_metrics.track_audio_processed(1.0)
|
|
wl_metrics.track_segment_emitted()
|
|
wl_metrics.track_rest_request()
|
|
wl_metrics.track_error()
|
|
finally:
|
|
wl_metrics._AVAILABLE = original
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|