m11_lib.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. """
  2. m11_lib.py — shared assertion library for M11 smoke scripts.
  3. Used by m11_smoke.py.
  4. All Prometheus queries use URL-encoding-safe Python urllib.
  5. """
  6. import urllib.request
  7. import urllib.parse
  8. import json
  9. PROM = "http://localhost:9090"
  10. PROM_TIMEOUT = 10 # seconds
  11. def scrape(query: str) -> list[dict]:
  12. """
  13. Run a Prometheus query and return the result vector.
  14. Returns [] on error or no data.
  15. """
  16. url = f"{PROM}/api/v1/query?query={urllib.parse.quote(query, safe='')}"
  17. try:
  18. with urllib.request.urlopen(url, timeout=PROM_TIMEOUT) as r:
  19. data = r.read()
  20. d = json.loads(data)
  21. if d.get("status") != "success":
  22. return []
  23. return d.get("data", {}).get("result", [])
  24. except Exception:
  25. return []
  26. def assert_grpc_rate_near(target: float, tolerance: float = 0.10,
  27. window_seconds: int = 30) -> float:
  28. """
  29. Assert the gRPC ingest rate is within tolerance of target.
  30. target: expected alerts/sec cluster-wide
  31. tolerance: fraction (0.10 = ±10%)
  32. Returns the actual rate, or raises AssertionError.
  33. Query: sum(rate(ba_ingestd_alerts_received_total{transport="grpc",result="accepted"}[window]))
  34. """
  35. query = (
  36. f'sum(rate(ba_ingestd_alerts_received_total{{transport="grpc",result="accepted"}}[{window_seconds}s]))'
  37. )
  38. results = scrape(query)
  39. if not results:
  40. raise AssertionError(
  41. f"gRPC rate query returned no data. Is ingestd gRPC server up and receiving load? "
  42. f"(query: {query})"
  43. )
  44. rate = float(results[0]["value"][1])
  45. min_rate = target * (1 - tolerance)
  46. max_rate = target * (1 + tolerance)
  47. if rate < min_rate:
  48. raise AssertionError(
  49. f"gRPC rate {rate:.0f}/s is below target {target:.0f}/s "
  50. f"(tolerance ±{tolerance*100:.0f}%, min allowed: {min_rate:.0f}/s, "
  51. f"window={window_seconds}s)"
  52. )
  53. return rate
  54. def assert_grpc_p99_under(threshold_ms: float, window_seconds: int = 60) -> float:
  55. """
  56. Assert gRPC ack p99 latency is under threshold (in milliseconds).
  57. Uses histogram_quantile over the given window.
  58. Returns the actual p99 in milliseconds, or raises AssertionError.
  59. Query: histogram_quantile(0.99, rate(ba_ingestd_grpc_ack_latency_seconds_bucket[window]))
  60. """
  61. query = (
  62. f'histogram_quantile(0.99, '
  63. f'rate(ba_ingestd_grpc_ack_latency_seconds_bucket[{window_seconds}s]))'
  64. )
  65. results = scrape(query)
  66. if not results:
  67. raise AssertionError(
  68. f"gRPC p99 query returned no data. Is ingestd gRPC server up? "
  69. f"(query: {query})"
  70. )
  71. p99_seconds = float(results[0]["value"][1])
  72. p99_ms = p99_seconds * 1000
  73. if p99_ms > threshold_ms:
  74. raise AssertionError(
  75. f"gRPC ack p99 {p99_ms:.1f}ms exceeds threshold {threshold_ms:.1f}ms "
  76. f"(window={window_seconds}s)"
  77. )
  78. return p99_ms
  79. def assert_dlq_count_equals(expected: int, window_seconds: int) -> int:
  80. """
  81. Assert the DLQ row count (delta over window) equals expected.
  82. Returns the actual count.
  83. """
  84. query = (
  85. f'increase(ba_deliverd_dlq_total[{window_seconds}s])'
  86. )
  87. results = scrape(query)
  88. if not results:
  89. return 0 # No data → assume 0
  90. count = float(results[0]["value"][1])
  91. if abs(count - expected) > 0.5:
  92. raise AssertionError(
  93. f"DLQ count {count:.0f} over last {window_seconds}s does not equal expected {expected}"
  94. )
  95. return int(count)
  96. def get_prometheus_targets() -> dict[str, str]:
  97. """
  98. Return a dict mapping service name → health status from Prometheus target health API.
  99. "1" = healthy, "0" = down, other = unknown state.
  100. """
  101. url = f"{PROM}/api/v1/targets?state=active"
  102. try:
  103. with urllib.request.urlopen(url, timeout=PROM_TIMEOUT) as r:
  104. data = json.loads(r.read())
  105. out = {}
  106. for t in data.get("data", {}).get("activeTargets", []):
  107. labels = t.get("labels", {})
  108. job = labels.get("job", "?")
  109. # Use the job name (e.g. "ingestd", "loadgen-grpc-1") as the key
  110. out[job] = "1" if t.get("health") == "up" else str(t.get("health", "0"))
  111. return out
  112. except Exception:
  113. return {}
  114. def get_grpc_streams_active() -> int:
  115. """
  116. Return the current number of active gRPC streams (ba_ingestd_grpc_streams_active gauge).
  117. Returns 0 if no data.
  118. """
  119. query = 'ba_ingestd_grpc_streams_active'
  120. results = scrape(query)
  121. if not results:
  122. return 0
  123. return int(float(results[0]["value"][1]))
  124. def get_grpc_rate_limited_total() -> int:
  125. """
  126. Return the cumulative ba_ingestd_grpc_rate_limited_total counter.
  127. Returns 0 if no data.
  128. """
  129. query = 'ba_ingestd_grpc_rate_limited_total'
  130. results = scrape(query)
  131. if not results:
  132. return 0
  133. return int(float(results[0]["value"][1]))