142 lines
5.2 KiB
Python
Executable File
142 lines
5.2 KiB
Python
Executable File
#!/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()
|