invarlock 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. invarlock/__init__.py +33 -0
  2. invarlock/__main__.py +10 -0
  3. invarlock/_data/runtime/profiles/ci_cpu.yaml +15 -0
  4. invarlock/_data/runtime/profiles/release.yaml +23 -0
  5. invarlock/_data/runtime/tiers.yaml +76 -0
  6. invarlock/adapters/__init__.py +102 -0
  7. invarlock/adapters/_capabilities.py +45 -0
  8. invarlock/adapters/auto.py +99 -0
  9. invarlock/adapters/base.py +530 -0
  10. invarlock/adapters/base_types.py +85 -0
  11. invarlock/adapters/hf_bert.py +852 -0
  12. invarlock/adapters/hf_gpt2.py +403 -0
  13. invarlock/adapters/hf_llama.py +485 -0
  14. invarlock/adapters/hf_mixin.py +383 -0
  15. invarlock/adapters/hf_onnx.py +112 -0
  16. invarlock/adapters/hf_t5.py +137 -0
  17. invarlock/adapters/py.typed +1 -0
  18. invarlock/assurance/__init__.py +43 -0
  19. invarlock/cli/__init__.py +8 -0
  20. invarlock/cli/__main__.py +8 -0
  21. invarlock/cli/_evidence.py +25 -0
  22. invarlock/cli/_json.py +75 -0
  23. invarlock/cli/adapter_auto.py +162 -0
  24. invarlock/cli/app.py +287 -0
  25. invarlock/cli/commands/__init__.py +26 -0
  26. invarlock/cli/commands/certify.py +403 -0
  27. invarlock/cli/commands/doctor.py +1358 -0
  28. invarlock/cli/commands/explain_gates.py +151 -0
  29. invarlock/cli/commands/export_html.py +100 -0
  30. invarlock/cli/commands/plugins.py +1331 -0
  31. invarlock/cli/commands/report.py +354 -0
  32. invarlock/cli/commands/run.py +4146 -0
  33. invarlock/cli/commands/verify.py +1040 -0
  34. invarlock/cli/config.py +396 -0
  35. invarlock/cli/constants.py +68 -0
  36. invarlock/cli/device.py +92 -0
  37. invarlock/cli/doctor_helpers.py +74 -0
  38. invarlock/cli/errors.py +6 -0
  39. invarlock/cli/overhead_utils.py +60 -0
  40. invarlock/cli/provenance.py +66 -0
  41. invarlock/cli/utils.py +41 -0
  42. invarlock/config.py +56 -0
  43. invarlock/core/__init__.py +62 -0
  44. invarlock/core/abi.py +15 -0
  45. invarlock/core/api.py +274 -0
  46. invarlock/core/auto_tuning.py +317 -0
  47. invarlock/core/bootstrap.py +226 -0
  48. invarlock/core/checkpoint.py +221 -0
  49. invarlock/core/contracts.py +73 -0
  50. invarlock/core/error_utils.py +64 -0
  51. invarlock/core/events.py +298 -0
  52. invarlock/core/exceptions.py +95 -0
  53. invarlock/core/registry.py +481 -0
  54. invarlock/core/retry.py +146 -0
  55. invarlock/core/runner.py +2041 -0
  56. invarlock/core/types.py +154 -0
  57. invarlock/edits/__init__.py +12 -0
  58. invarlock/edits/_edit_utils.py +249 -0
  59. invarlock/edits/_external_utils.py +268 -0
  60. invarlock/edits/noop.py +47 -0
  61. invarlock/edits/py.typed +1 -0
  62. invarlock/edits/quant_rtn.py +801 -0
  63. invarlock/edits/registry.py +166 -0
  64. invarlock/eval/__init__.py +23 -0
  65. invarlock/eval/bench.py +1207 -0
  66. invarlock/eval/bootstrap.py +50 -0
  67. invarlock/eval/data.py +2052 -0
  68. invarlock/eval/metrics.py +2167 -0
  69. invarlock/eval/primary_metric.py +767 -0
  70. invarlock/eval/probes/__init__.py +24 -0
  71. invarlock/eval/probes/fft.py +139 -0
  72. invarlock/eval/probes/mi.py +213 -0
  73. invarlock/eval/probes/post_attention.py +323 -0
  74. invarlock/eval/providers/base.py +67 -0
  75. invarlock/eval/providers/seq2seq.py +111 -0
  76. invarlock/eval/providers/text_lm.py +113 -0
  77. invarlock/eval/providers/vision_text.py +93 -0
  78. invarlock/eval/py.typed +1 -0
  79. invarlock/guards/__init__.py +18 -0
  80. invarlock/guards/_contracts.py +9 -0
  81. invarlock/guards/invariants.py +640 -0
  82. invarlock/guards/policies.py +805 -0
  83. invarlock/guards/py.typed +1 -0
  84. invarlock/guards/rmt.py +2097 -0
  85. invarlock/guards/spectral.py +1419 -0
  86. invarlock/guards/tier_config.py +354 -0
  87. invarlock/guards/variance.py +3298 -0
  88. invarlock/guards_ref/__init__.py +15 -0
  89. invarlock/guards_ref/rmt_ref.py +40 -0
  90. invarlock/guards_ref/spectral_ref.py +135 -0
  91. invarlock/guards_ref/variance_ref.py +60 -0
  92. invarlock/model_profile.py +353 -0
  93. invarlock/model_utils.py +221 -0
  94. invarlock/observability/__init__.py +10 -0
  95. invarlock/observability/alerting.py +535 -0
  96. invarlock/observability/core.py +546 -0
  97. invarlock/observability/exporters.py +565 -0
  98. invarlock/observability/health.py +588 -0
  99. invarlock/observability/metrics.py +457 -0
  100. invarlock/observability/py.typed +1 -0
  101. invarlock/observability/utils.py +553 -0
  102. invarlock/plugins/__init__.py +12 -0
  103. invarlock/plugins/hello_guard.py +33 -0
  104. invarlock/plugins/hf_awq_adapter.py +82 -0
  105. invarlock/plugins/hf_bnb_adapter.py +79 -0
  106. invarlock/plugins/hf_gptq_adapter.py +78 -0
  107. invarlock/plugins/py.typed +1 -0
  108. invarlock/py.typed +1 -0
  109. invarlock/reporting/__init__.py +7 -0
  110. invarlock/reporting/certificate.py +3221 -0
  111. invarlock/reporting/certificate_schema.py +244 -0
  112. invarlock/reporting/dataset_hashing.py +215 -0
  113. invarlock/reporting/guards_analysis.py +948 -0
  114. invarlock/reporting/html.py +32 -0
  115. invarlock/reporting/normalizer.py +235 -0
  116. invarlock/reporting/policy_utils.py +517 -0
  117. invarlock/reporting/primary_metric_utils.py +265 -0
  118. invarlock/reporting/render.py +1442 -0
  119. invarlock/reporting/report.py +903 -0
  120. invarlock/reporting/report_types.py +278 -0
  121. invarlock/reporting/utils.py +175 -0
  122. invarlock/reporting/validate.py +631 -0
  123. invarlock/security.py +176 -0
  124. invarlock/sparsity_utils.py +323 -0
  125. invarlock/utils/__init__.py +150 -0
  126. invarlock/utils/digest.py +45 -0
  127. invarlock-0.2.0.dist-info/METADATA +586 -0
  128. invarlock-0.2.0.dist-info/RECORD +132 -0
  129. invarlock-0.2.0.dist-info/WHEEL +5 -0
  130. invarlock-0.2.0.dist-info/entry_points.txt +20 -0
  131. invarlock-0.2.0.dist-info/licenses/LICENSE +201 -0
  132. invarlock-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,565 @@
1
+ """
2
+ Metrics exporters for various monitoring systems.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ import threading
8
+ import time
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+ from invarlock.core.exceptions import ObservabilityError
14
+
15
+
16
+ @dataclass
17
+ class ExportedMetric:
18
+ """Represents a metric for export."""
19
+
20
+ name: str
21
+ value: float | int
22
+ timestamp: float
23
+ labels: dict[str, str] = field(default_factory=dict)
24
+ metric_type: str = "gauge" # gauge, counter, histogram, summary
25
+ help_text: str = ""
26
+
27
+ def to_prometheus_format(self) -> str:
28
+ """Convert to Prometheus exposition format."""
29
+ lines = []
30
+
31
+ # Add help text
32
+ if self.help_text:
33
+ lines.append(f"# HELP {self.name} {self.help_text}")
34
+
35
+ # Add type
36
+ lines.append(f"# TYPE {self.name} {self.metric_type}")
37
+
38
+ # Format labels
39
+ if self.labels:
40
+ label_str = ",".join([f'{k}="{v}"' for k, v in self.labels.items()])
41
+ metric_line = (
42
+ f"{self.name}{{{label_str}}} {self.value} {int(self.timestamp * 1000)}"
43
+ )
44
+ else:
45
+ metric_line = f"{self.name} {self.value} {int(self.timestamp * 1000)}"
46
+
47
+ lines.append(metric_line)
48
+ return "\n".join(lines)
49
+
50
+ def to_json_format(self) -> dict[str, Any]:
51
+ """Convert to JSON format."""
52
+ return {
53
+ "metric": self.name,
54
+ "value": self.value,
55
+ "timestamp": self.timestamp,
56
+ "labels": self.labels,
57
+ "type": self.metric_type,
58
+ "help": self.help_text,
59
+ }
60
+
61
+
62
+ class MetricsExporter(ABC):
63
+ """Base class for metrics exporters."""
64
+
65
+ def __init__(self, name: str):
66
+ self.name = name
67
+ self.logger = logging.getLogger(f"{__name__}.{name}")
68
+ self.enabled = True
69
+ self.last_export_time: float = 0.0
70
+ self.export_count = 0
71
+ self.error_count = 0
72
+
73
+ @abstractmethod
74
+ def export(self, metrics: list[ExportedMetric]) -> bool:
75
+ """Export metrics. Returns True if successful."""
76
+ pass
77
+
78
+ def get_stats(self) -> dict[str, Any]:
79
+ """Get exporter statistics."""
80
+ return {
81
+ "name": self.name,
82
+ "enabled": self.enabled,
83
+ "last_export_time": self.last_export_time,
84
+ "export_count": self.export_count,
85
+ "error_count": self.error_count,
86
+ "success_rate": (self.export_count - self.error_count)
87
+ / max(1, self.export_count),
88
+ }
89
+
90
+
91
+ class PrometheusExporter(MetricsExporter):
92
+ """Exporter for Prometheus format."""
93
+
94
+ def __init__(
95
+ self,
96
+ gateway_url: str | None = None,
97
+ job_name: str = "invarlock",
98
+ push_interval: int = 15,
99
+ instance: str | None = None,
100
+ ):
101
+ super().__init__("prometheus")
102
+ self.gateway_url = gateway_url
103
+ self.job_name = job_name
104
+ self.push_interval = push_interval
105
+ self.instance = instance or "localhost"
106
+
107
+ # For HTTP server mode
108
+ self._metrics_cache: dict[str, ExportedMetric] = {}
109
+ self._cache_lock = threading.Lock()
110
+
111
+ def export(self, metrics: list[ExportedMetric]) -> bool:
112
+ """Export metrics to Prometheus."""
113
+ try:
114
+ if self.gateway_url:
115
+ return self._push_to_gateway(metrics)
116
+ else:
117
+ return self._update_cache(metrics)
118
+ except Exception as e:
119
+ self.logger.error(f"Failed to export to Prometheus: {e}")
120
+ self.error_count += 1
121
+ return False
122
+
123
+ def _push_to_gateway(self, metrics: list[ExportedMetric]) -> bool:
124
+ """Push metrics to Prometheus Gateway."""
125
+ try:
126
+ import requests
127
+ except ImportError:
128
+ self.logger.error("requests library required for Prometheus Gateway")
129
+ return False
130
+
131
+ # Convert metrics to Prometheus format
132
+ prometheus_data = "\n".join([m.to_prometheus_format() for m in metrics])
133
+
134
+ # Push to gateway
135
+ url = f"{self.gateway_url}/metrics/job/{self.job_name}/instance/{self.instance}"
136
+
137
+ response = requests.post(
138
+ url,
139
+ data=prometheus_data,
140
+ headers={"Content-Type": "text/plain; version=0.0.4; charset=utf-8"},
141
+ timeout=10,
142
+ )
143
+
144
+ if response.status_code == 200:
145
+ self.export_count += 1
146
+ self.last_export_time = time.time()
147
+ return True
148
+ else:
149
+ self.logger.error(
150
+ f"Prometheus Gateway returned {response.status_code}: {response.text}"
151
+ )
152
+ self.error_count += 1
153
+ return False
154
+
155
+ def _update_cache(self, metrics: list[ExportedMetric]) -> bool:
156
+ """Update internal cache for HTTP server mode."""
157
+ with self._cache_lock:
158
+ for metric in metrics:
159
+ key = f"{metric.name}_{hash(str(sorted(metric.labels.items())))}"
160
+ self._metrics_cache[key] = metric
161
+
162
+ self.export_count += 1
163
+ self.last_export_time = time.time()
164
+ return True
165
+
166
+ def get_metrics_text(self) -> str:
167
+ """Get current metrics in Prometheus format (for HTTP server)."""
168
+ with self._cache_lock:
169
+ return "\n".join(
170
+ [m.to_prometheus_format() for m in self._metrics_cache.values()]
171
+ )
172
+
173
+
174
+ class JSONExporter(MetricsExporter):
175
+ """Exporter for JSON format."""
176
+
177
+ def __init__(self, output_file: str | None = None, pretty_print: bool = True):
178
+ super().__init__("json")
179
+ self.output_file = output_file
180
+ self.pretty_print = pretty_print
181
+ self._metrics_buffer: list[dict[str, Any]] = []
182
+
183
+ def export(self, metrics: list[ExportedMetric]) -> bool:
184
+ """Export metrics to JSON."""
185
+ try:
186
+ json_metrics = [m.to_json_format() for m in metrics]
187
+
188
+ if self.output_file:
189
+ return self._write_to_file(json_metrics)
190
+ else:
191
+ return self._buffer_metrics(json_metrics)
192
+
193
+ except Exception as e:
194
+ self.logger.error(f"Failed to export to JSON: {e}")
195
+ self.error_count += 1
196
+ return False
197
+
198
+ def _write_to_file(self, json_metrics: list[dict[str, Any]]) -> bool:
199
+ """Write metrics to JSON file."""
200
+ if self.output_file is None:
201
+ self.logger.error("No output file specified")
202
+ self.error_count += 1
203
+ return False
204
+
205
+ try:
206
+ with open(self.output_file, "w") as f:
207
+ if self.pretty_print:
208
+ json.dump(json_metrics, f, indent=2, default=str)
209
+ else:
210
+ json.dump(json_metrics, f, default=str)
211
+
212
+ self.export_count += 1
213
+ self.last_export_time = time.time()
214
+ return True
215
+
216
+ except Exception as e:
217
+ self.logger.error(f"Failed to write JSON file: {e}")
218
+ self.error_count += 1
219
+ return False
220
+
221
+ def _buffer_metrics(self, json_metrics: list[dict[str, Any]]) -> bool:
222
+ """Buffer metrics in memory."""
223
+ self._metrics_buffer.extend(json_metrics)
224
+
225
+ # Keep buffer size limited
226
+ if len(self._metrics_buffer) > 10000:
227
+ self._metrics_buffer = self._metrics_buffer[-10000:]
228
+
229
+ self.export_count += 1
230
+ self.last_export_time = time.time()
231
+ return True
232
+
233
+ def get_buffered_metrics(self) -> list[dict[str, Any]]:
234
+ """Get buffered metrics."""
235
+ return self._metrics_buffer.copy()
236
+
237
+ def clear_buffer(self):
238
+ """Clear metrics buffer."""
239
+ self._metrics_buffer.clear()
240
+
241
+
242
+ def export_or_raise(exporter: MetricsExporter, metrics: list[ExportedMetric]) -> None:
243
+ """Export metrics via an exporter or raise a typed ObservabilityError.
244
+
245
+ - Raises ObservabilityError(E801) when export returns False or raises.
246
+ - Includes exporter name and reason in details for debugging.
247
+ """
248
+ try:
249
+ ok = exporter.export(metrics)
250
+ except (
251
+ Exception
252
+ ) as e: # pragma: no cover - covered via tests using failing exporter
253
+ raise ObservabilityError(
254
+ code="E801",
255
+ message="OBSERVABILITY-EXPORT-FAILED",
256
+ details={"exporter": exporter.name, "reason": type(e).__name__},
257
+ ) from e
258
+ if not ok:
259
+ raise ObservabilityError(
260
+ code="E801",
261
+ message="OBSERVABILITY-EXPORT-FAILED",
262
+ details={"exporter": exporter.name, "reason": "returned_false"},
263
+ )
264
+
265
+
266
+ class InfluxDBExporter(MetricsExporter):
267
+ """Exporter for InfluxDB."""
268
+
269
+ def __init__(
270
+ self,
271
+ url: str,
272
+ database: str,
273
+ username: str | None = None,
274
+ password: str | None = None,
275
+ retention_policy: str = "autogen",
276
+ ):
277
+ super().__init__("influxdb")
278
+ self.url = url.rstrip("/")
279
+ self.database = database
280
+ self.username = username
281
+ self.password = password
282
+ self.retention_policy = retention_policy
283
+
284
+ def export(self, metrics: list[ExportedMetric]) -> bool:
285
+ """Export metrics to InfluxDB."""
286
+ try:
287
+ import requests
288
+ except ImportError:
289
+ self.logger.error("requests library required for InfluxDB")
290
+ return False
291
+
292
+ try:
293
+ # Convert metrics to InfluxDB line protocol
294
+ lines = []
295
+ for metric in metrics:
296
+ line = self._to_line_protocol(metric)
297
+ if line:
298
+ lines.append(line)
299
+
300
+ if not lines:
301
+ return True
302
+
303
+ # Write to InfluxDB
304
+ write_url = f"{self.url}/write"
305
+ params = {
306
+ "db": self.database,
307
+ "rp": self.retention_policy,
308
+ "precision": "ms",
309
+ }
310
+
311
+ auth = None
312
+ if self.username and self.password:
313
+ auth = (self.username, self.password)
314
+
315
+ response = requests.post(
316
+ write_url,
317
+ params=params,
318
+ data="\n".join(lines),
319
+ auth=auth,
320
+ headers={"Content-Type": "text/plain"},
321
+ timeout=10,
322
+ )
323
+
324
+ if response.status_code == 204:
325
+ self.export_count += 1
326
+ self.last_export_time = time.time()
327
+ return True
328
+ else:
329
+ self.logger.error(
330
+ f"InfluxDB returned {response.status_code}: {response.text}"
331
+ )
332
+ self.error_count += 1
333
+ return False
334
+
335
+ except Exception as e:
336
+ self.logger.error(f"Failed to export to InfluxDB: {e}")
337
+ self.error_count += 1
338
+ return False
339
+
340
+ def _to_line_protocol(self, metric: ExportedMetric) -> str:
341
+ """Convert metric to InfluxDB line protocol."""
342
+ # Escape special characters in measurement name
343
+ measurement = (
344
+ metric.name.replace(" ", "\\ ").replace(",", "\\,").replace("=", "\\=")
345
+ )
346
+
347
+ # Build tags
348
+ tag_parts = []
349
+ for key, value in metric.labels.items():
350
+ escaped_key = (
351
+ key.replace(" ", "\\ ").replace(",", "\\,").replace("=", "\\=")
352
+ )
353
+ escaped_value = (
354
+ str(value).replace(" ", "\\ ").replace(",", "\\,").replace("=", "\\=")
355
+ )
356
+ tag_parts.append(f"{escaped_key}={escaped_value}")
357
+
358
+ tags = "," + ",".join(tag_parts) if tag_parts else ""
359
+
360
+ # Build fields (for InfluxDB, we need at least one field)
361
+ fields = f"value={metric.value}"
362
+
363
+ # Timestamp in milliseconds
364
+ timestamp = int(metric.timestamp * 1000)
365
+
366
+ return f"{measurement}{tags} {fields} {timestamp}"
367
+
368
+
369
+ class StatsExporter(MetricsExporter):
370
+ """Exporter for StatsD protocol."""
371
+
372
+ def __init__(
373
+ self, host: str = "localhost", port: int = 8125, prefix: str = "invarlock"
374
+ ):
375
+ super().__init__("statsd")
376
+ self.host = host
377
+ self.port = port
378
+ self.prefix = prefix
379
+ self._socket: Any | None = None
380
+
381
+ def export(self, metrics: list[ExportedMetric]) -> bool:
382
+ """Export metrics to StatsD."""
383
+ try:
384
+ import socket
385
+ except ImportError:
386
+ self.logger.error("socket library required for StatsD")
387
+ return False
388
+
389
+ try:
390
+ if not self._socket:
391
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
392
+
393
+ for metric in metrics:
394
+ statsd_line = self._to_statsd_format(metric)
395
+ if statsd_line and self._socket:
396
+ self._socket.sendto(
397
+ statsd_line.encode("utf-8"), (self.host, self.port)
398
+ )
399
+
400
+ self.export_count += 1
401
+ self.last_export_time = time.time()
402
+ return True
403
+
404
+ except Exception as e:
405
+ self.logger.error(f"Failed to export to StatsD: {e}")
406
+ self.error_count += 1
407
+ return False
408
+
409
+ def _to_statsd_format(self, metric: ExportedMetric) -> str:
410
+ """Convert metric to StatsD format."""
411
+ # Build metric name with prefix
412
+ name_parts = [self.prefix] if self.prefix else []
413
+ name_parts.append(metric.name.replace(".", "_").replace(" ", "_"))
414
+
415
+ # Add labels as tags (if supported)
416
+ if metric.labels:
417
+ label_parts = [f"{k}:{v}" for k, v in metric.labels.items()]
418
+ name_parts.extend(label_parts)
419
+
420
+ metric_name = ".".join(name_parts)
421
+
422
+ # Determine StatsD type
423
+ if metric.metric_type == "counter":
424
+ return f"{metric_name}:{metric.value}|c"
425
+ elif metric.metric_type == "histogram":
426
+ return f"{metric_name}:{metric.value}|h"
427
+ else: # gauge
428
+ return f"{metric_name}:{metric.value}|g"
429
+
430
+
431
+ class ExportManager:
432
+ """Manages multiple metrics exporters."""
433
+
434
+ def __init__(self):
435
+ self.logger = logging.getLogger(__name__)
436
+ self.exporters: dict[str, MetricsExporter] = {}
437
+ self.export_interval = 10 # seconds
438
+ self._running = False
439
+ self._export_thread = None
440
+ self._metrics_queue = []
441
+ self._queue_lock = threading.Lock()
442
+
443
+ def add_exporter(self, exporter: MetricsExporter):
444
+ """Add a metrics exporter."""
445
+ self.exporters[exporter.name] = exporter
446
+ self.logger.info(f"Added exporter: {exporter.name}")
447
+
448
+ def remove_exporter(self, name: str):
449
+ """Remove a metrics exporter."""
450
+ self.exporters.pop(name, None)
451
+ self.logger.info(f"Removed exporter: {name}")
452
+
453
+ def queue_metrics(self, metrics: list[ExportedMetric]):
454
+ """Queue metrics for export."""
455
+ with self._queue_lock:
456
+ self._metrics_queue.extend(metrics)
457
+
458
+ def export_now(
459
+ self, metrics: list[ExportedMetric] | None = None
460
+ ) -> dict[str, bool]:
461
+ """Export metrics immediately."""
462
+ if metrics is None:
463
+ with self._queue_lock:
464
+ metrics = self._metrics_queue.copy()
465
+ self._metrics_queue.clear()
466
+
467
+ results = {}
468
+ for name, exporter in self.exporters.items():
469
+ if exporter.enabled:
470
+ try:
471
+ results[name] = exporter.export(metrics)
472
+ except Exception as e:
473
+ self.logger.error(f"Exporter {name} failed: {e}")
474
+ results[name] = False
475
+ else:
476
+ results[name] = False # Disabled
477
+
478
+ return results
479
+
480
+ def start_background_export(self):
481
+ """Start background export thread."""
482
+ if self._running:
483
+ return
484
+
485
+ self._running = True
486
+ self._export_thread = threading.Thread(target=self._export_loop, daemon=True)
487
+ self._export_thread.start()
488
+ self.logger.info("Started background metrics export")
489
+
490
+ def stop_background_export(self):
491
+ """Stop background export thread."""
492
+ self._running = False
493
+ if self._export_thread:
494
+ self._export_thread.join(timeout=5)
495
+ self.logger.info("Stopped background metrics export")
496
+
497
+ def _export_loop(self):
498
+ """Background export loop."""
499
+ while self._running:
500
+ try:
501
+ time.sleep(self.export_interval)
502
+
503
+ # Get queued metrics
504
+ with self._queue_lock:
505
+ if self._metrics_queue:
506
+ metrics = self._metrics_queue.copy()
507
+ self._metrics_queue.clear()
508
+
509
+ # Export to all enabled exporters
510
+ self.export_now(metrics)
511
+
512
+ except Exception as e:
513
+ self.logger.error(f"Error in export loop: {e}")
514
+
515
+ def get_exporter_stats(self) -> dict[str, dict[str, Any]]:
516
+ """Get statistics for all exporters."""
517
+ return {name: exporter.get_stats() for name, exporter in self.exporters.items()}
518
+
519
+ def get_summary(self) -> dict[str, Any]:
520
+ """Get export manager summary."""
521
+ total_exports = sum(e.export_count for e in self.exporters.values())
522
+ total_errors = sum(e.error_count for e in self.exporters.values())
523
+
524
+ with self._queue_lock:
525
+ queue_size = len(self._metrics_queue)
526
+
527
+ return {
528
+ "total_exporters": len(self.exporters),
529
+ "enabled_exporters": len([e for e in self.exporters.values() if e.enabled]),
530
+ "total_exports": total_exports,
531
+ "total_errors": total_errors,
532
+ "success_rate": (total_exports - total_errors) / max(1, total_exports),
533
+ "queue_size": queue_size,
534
+ "background_running": self._running,
535
+ "export_interval": self.export_interval,
536
+ }
537
+
538
+
539
+ # Utility functions for common exporter setups
540
+ def setup_prometheus_exporter(
541
+ gateway_url: str | None = None, job_name: str = "invarlock"
542
+ ) -> PrometheusExporter:
543
+ """Setup Prometheus exporter."""
544
+ return PrometheusExporter(gateway_url=gateway_url, job_name=job_name)
545
+
546
+
547
+ def setup_json_file_exporter(output_file: str) -> JSONExporter:
548
+ """Setup JSON file exporter."""
549
+ return JSONExporter(output_file=output_file)
550
+
551
+
552
+ def setup_influxdb_exporter(
553
+ url: str, database: str, username: str | None = None, password: str | None = None
554
+ ) -> InfluxDBExporter:
555
+ """Setup InfluxDB exporter."""
556
+ return InfluxDBExporter(
557
+ url=url, database=database, username=username, password=password
558
+ )
559
+
560
+
561
+ def setup_statsd_exporter(
562
+ host: str = "localhost", port: int = 8125, prefix: str = "invarlock"
563
+ ) -> StatsExporter:
564
+ """Setup StatsD exporter."""
565
+ return StatsExporter(host=host, port=port, prefix=prefix)