#!/usr/bin/env python3 """ ActivityWatch Prometheus Exporter Собирает метрики из ActivityWatch API и экспонирует их в формате Prometheus. """ import logging import os import time from datetime import datetime import requests from prometheus_client import Counter, Gauge, Info, start_http_server logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Configuration AW_SERVER_HOST = os.getenv("AW_SERVER_HOST", "192.0.2.13") AW_SERVER_PORT = int(os.getenv("AW_SERVER_PORT", "5600")) AW_SERVER_SCHEME = os.getenv("AW_SERVER_SCHEME", "http") AW_API_BASE = os.getenv( "AW_API_BASE", f"{AW_SERVER_SCHEME}://{AW_SERVER_HOST}:{AW_SERVER_PORT}/api/0", ) EXPORTER_PORT = int(os.getenv("EXPORTER_PORT", "9398")) SCRAPE_INTERVAL_SECONDS = int(os.getenv("SCRAPE_INTERVAL_SECONDS", "30")) # Metrics aw_up = Gauge("aw_up", "ActivityWatch API availability: 1 if the last scrape succeeded, 0 otherwise") aw_buckets_total = Gauge("aw_buckets_total", "Total number of ActivityWatch buckets") aw_events_total = Counter("aw_events_total", "Total number of ActivityWatch events observed", ["bucket", "event_type"]) aw_events_last_timestamp = Gauge("aw_events_last_timestamp", "Timestamp of last event in bucket", ["bucket"]) aw_bucket_events_count = Gauge("aw_bucket_events_count", "Number of events sampled from bucket", ["bucket"]) aw_collector_status = Gauge( "aw_collector_status", "ActivityWatch bucket collector status: 1 if bucket was observed during the last scrape", ["bucket", "client", "hostname", "type"], ) aw_server_info = Info("aw_server", "ActivityWatch server information") class ActivityWatchExporter: def __init__(self, api_base): self.api_base = api_base.rstrip("/") self.session = requests.Session() self.session.headers.update({"Accept": "application/json"}) self.bucket_event_counts = {} def get_buckets(self): """Get all buckets from ActivityWatch API.""" response = self.session.get(f"{self.api_base}/buckets", timeout=10) response.raise_for_status() return response.json() def get_bucket_events(self, bucket_id, limit=1000): """Get events from a specific bucket.""" response = self.session.get( f"{self.api_base}/buckets/{bucket_id}/events", params={"limit": limit}, timeout=10, ) response.raise_for_status() return response.json() @staticmethod def event_type(event): data = event.get("data") or {} return str(data.get("app") or data.get("title") or event.get("$schema") or "unknown") @staticmethod def event_timestamp(event): timestamp = event.get("timestamp", 0) if isinstance(timestamp, str): return datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp() return float(timestamp or 0) def collect_metrics(self): """Collect metrics from ActivityWatch.""" try: buckets = self.get_buckets() aw_up.set(1) except Exception as exc: logger.error("Failed to get buckets: %s", exc) aw_up.set(0) return aw_buckets_total.set(len(buckets)) aw_server_info.info( { "host": AW_SERVER_HOST, "port": str(AW_SERVER_PORT), "scheme": AW_SERVER_SCHEME, "api_base": self.api_base, } ) aw_collector_status.clear() for bucket_id, bucket_data in buckets.items(): client = str(bucket_data.get("client", "unknown")) hostname = str(bucket_data.get("hostname", "unknown")) bucket_type = str(bucket_data.get("type", "unknown")) try: events = self.get_bucket_events(bucket_id) except Exception as exc: logger.error("Failed to get events for %s: %s", bucket_id, exc) events = [] event_count = len(events) aw_bucket_events_count.labels(bucket=bucket_id).set(event_count) aw_collector_status.labels(bucket=bucket_id, client=client, hostname=hostname, type=bucket_type).set(1) previous_count = self.bucket_event_counts.get(bucket_id) if previous_count is not None and event_count > previous_count: for event in events[: event_count - previous_count]: aw_events_total.labels(bucket=bucket_id, event_type=self.event_type(event)).inc() self.bucket_event_counts[bucket_id] = event_count if events: try: aw_events_last_timestamp.labels(bucket=bucket_id).set(self.event_timestamp(events[0])) except Exception as exc: logger.warning("Failed to parse last event timestamp for %s: %s", bucket_id, exc) def main(): exporter = ActivityWatchExporter(AW_API_BASE) exporter.collect_metrics() start_http_server(EXPORTER_PORT) logger.info("ActivityWatch exporter started on port %s", EXPORTER_PORT) logger.info("Scraping ActivityWatch API at %s", AW_API_BASE) while True: time.sleep(SCRAPE_INTERVAL_SECONDS) exporter.collect_metrics() if __name__ == "__main__": main()