From d5735ba127ceea6bccc5fb1cc1f2ebba9eb02fe3 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Tue, 5 May 2026 01:32:50 +0300 Subject: [PATCH] feat(grafana): add ActivityWatch Prometheus exporter and dashboards - Add Python exporter for ActivityWatch API metrics collection - Configure Prometheus to scrape ActivityWatch exporter - Create Grafana dashboard for ActivityWatch overview - Integrate ActivityWatch monitoring into existing docker-compose stack - Add Prometheus datasource for ActivityWatch metrics Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- grafana-1c/docker-compose.yml | 20 +++ .../grafana/dashboards/aw_overview.json | 121 +++++++++++++++ .../provisioning/dashboards/dashboards.yml | 9 ++ .../datasources/aw_prometheus.yml | 9 ++ .../provisioning/datasources/datasources.yml | 9 ++ grafana-1c/prometheus/prometheus.yml | 5 + grafana-1c/sql-exporter/Dockerfile | 9 ++ .../collectors/aw_activitywatch.py | 142 ++++++++++++++++++ 8 files changed, 324 insertions(+) create mode 100644 grafana-1c/grafana/dashboards/aw_overview.json create mode 100644 grafana-1c/grafana/provisioning/datasources/aw_prometheus.yml create mode 100644 grafana-1c/grafana/provisioning/datasources/datasources.yml create mode 100644 grafana-1c/sql-exporter/Dockerfile create mode 100644 grafana-1c/sql-exporter/collectors/aw_activitywatch.py diff --git a/grafana-1c/docker-compose.yml b/grafana-1c/docker-compose.yml index d47c540..6206001 100755 --- a/grafana-1c/docker-compose.yml +++ b/grafana-1c/docker-compose.yml @@ -1,4 +1,23 @@ services: + aw-exporter: + build: + context: ./sql-exporter + dockerfile: Dockerfile + container_name: awrus-aw-exporter + restart: unless-stopped + environment: + - AW_SERVER_HOST=10.10.10.13 + - AW_SERVER_PORT=5600 + - AW_SERVER_SCHEME=http + - EXPORTER_PORT=9398 + volumes: + - ./sql-exporter/collectors/aw_activitywatch.py:/app/aw_activitywatch.py:ro + ports: + - "9398:9398" + command: + - "python3" + - "/app/aw_activitywatch.py" + sql-exporter: image: ghcr.io/burningalchemist/sql_exporter:latest container_name: awrus-sql-exporter @@ -30,6 +49,7 @@ services: ports: - "${PROMETHEUS_PORT:-9090}:9090" depends_on: + - aw-exporter - sql-exporter grafana: diff --git a/grafana-1c/grafana/dashboards/aw_overview.json b/grafana-1c/grafana/dashboards/aw_overview.json new file mode 100644 index 0000000..953d191 --- /dev/null +++ b/grafana-1c/grafana/dashboards/aw_overview.json @@ -0,0 +1,121 @@ +{ + "dashboard": { + "title": "ActivityWatch Overview", + "tags": ["activitywatch", "monitoring"], + "timezone": "browser", + "panels": [ + { + "id": 1, + "title": "Total Buckets", + "type": "stat", + "targets": [ + { + "expr": "aw_buckets_total", + "refId": "A", + "legendFormat": "Total Buckets" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area" + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "min": 0 + } + } + }, + { + "id": 2, + "title": "Events per Bucket", + "type": "table", + "targets": [ + { + "expr": "aw_bucket_events_count", + "format": "table", + "instant": true, + "refId": "B" + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": true + } + } + } + ] + }, + { + "id": 3, + "title": "Collector Status", + "type": "stat", + "targets": [ + { + "expr": "aw_collector_status", + "format": "table", + "instant": true, + "refId": "C" + } + ], + "options": { + "colorMode": "value" + } + }, + { + "id": 4, + "title": "Events Timeline", + "type": "graph", + "targets": [ + { + "expr": "rate(aw_events_total[5m])", + "legendFormat": "{{bucket}} - {{event_type}}", + "refId": "D" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "lineWidth": 2, + "fillOpacity": 10 + } + } + } + }, + { + "id": 5, + "title": "Last Event Timestamp", + "type": "gauge", + "targets": [ + { + "expr": "aw_events_last_timestamp", + "legendFormat": "{{bucket}}", + "refId": "E" + } + ], + "options": { + "orientation": "horizontal" + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "custom": { + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "red", "value": 0}, + {"color": "yellow", "value": 3600}, + {"color": "green", "value": 86400} + ] + } + } + } + } + } + ] + } +} diff --git a/grafana-1c/grafana/provisioning/dashboards/dashboards.yml b/grafana-1c/grafana/provisioning/dashboards/dashboards.yml index c3e0cf7..940735e 100755 --- a/grafana-1c/grafana/provisioning/dashboards/dashboards.yml +++ b/grafana-1c/grafana/provisioning/dashboards/dashboards.yml @@ -9,3 +9,12 @@ providers: editable: false options: path: /var/lib/grafana/dashboards + + - name: ActivityWatch + orgId: 2 + folder: "ActivityWatch" + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards diff --git a/grafana-1c/grafana/provisioning/datasources/aw_prometheus.yml b/grafana-1c/grafana/provisioning/datasources/aw_prometheus.yml new file mode 100644 index 0000000..1a57b69 --- /dev/null +++ b/grafana-1c/grafana/provisioning/datasources/aw_prometheus.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true diff --git a/grafana-1c/grafana/provisioning/datasources/datasources.yml b/grafana-1c/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000..1a57b69 --- /dev/null +++ b/grafana-1c/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true diff --git a/grafana-1c/prometheus/prometheus.yml b/grafana-1c/prometheus/prometheus.yml index 03fac1b..a510ea9 100755 --- a/grafana-1c/prometheus/prometheus.yml +++ b/grafana-1c/prometheus/prometheus.yml @@ -11,6 +11,11 @@ scrape_configs: static_configs: - targets: ["localhost:9090"] + - job_name: aw_activitywatch_exporter + static_configs: + - targets: ["aw-exporter:9398"] + scrape_interval: 30s + - job_name: onec_sql_exporter static_configs: - targets: ["sql-exporter:9399"] diff --git a/grafana-1c/sql-exporter/Dockerfile b/grafana-1c/sql-exporter/Dockerfile new file mode 100644 index 0000000..9ecc1d7 --- /dev/null +++ b/grafana-1c/sql-exporter/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN pip install prometheus_client requests + +COPY collectors/aw_activitywatch.py /app/aw_activitywatch.py + +CMD ["python3", "/app/aw_activitywatch.py"] diff --git a/grafana-1c/sql-exporter/collectors/aw_activitywatch.py b/grafana-1c/sql-exporter/collectors/aw_activitywatch.py new file mode 100644 index 0000000..fbe5efe --- /dev/null +++ b/grafana-1c/sql-exporter/collectors/aw_activitywatch.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +""" +ActivityWatch Prometheus Exporter +Собирает метрики из ActivityWatch API и экспонирует их в формате Prometheus. +""" + +import time +import logging +import requests +from prometheus_client import start_http_server, Gauge, Counter, Histogram, Info +from datetime import datetime, timedelta + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Configuration +AW_SERVER_HOST = "10.10.10.13" +AW_SERVER_PORT = 5600 +AW_SERVER_SCHEME = "http" +AW_API_BASE = f"{AW_SERVER_SCHEME}://{AW_SERVER_HOST}:{AW_SERVER_PORT}/api/0" +EXPORTER_PORT = 9398 + +# Metrics +aw_buckets_total = Gauge('aw_buckets_total', 'Total number of ActivityWatch buckets') +aw_events_total = Counter('aw_events_total', 'Total number of ActivityWatch events', ['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 in bucket', ['bucket']) +aw_collector_status = Info('aw_collector_status', 'Status of ActivityWatch collectors') +aw_server_info = Info('aw_server_info', 'ActivityWatch server information') + +class ActivityWatchExporter: + def __init__(self, api_base): + self.api_base = api_base + self.session = requests.Session() + self.session.headers.update({'Accept': 'application/json'}) + self.bucket_cache = {} + + def get_buckets(self): + """Get all buckets from ActivityWatch API.""" + try: + response = self.session.get(f"{self.api_base}/buckets", timeout=10) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Failed to get buckets: {e}") + return {} + + def get_bucket_events(self, bucket_id, limit=1): + """Get events from a specific bucket.""" + try: + response = self.session.get( + f"{self.api_base}/buckets/{bucket_id}/events", + params={'limit': limit}, + timeout=10 + ) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Failed to get events for {bucket_id}: {e}") + return [] + + def get_bucket_info(self, bucket_id): + """Get detailed info about a bucket.""" + try: + response = self.session.get(f"{self.api_base}/buckets/{bucket_id}", timeout=10) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Failed to get info for {bucket_id}: {e}") + return {} + + def collect_metrics(self): + """Collect metrics from ActivityWatch.""" + buckets = self.get_buckets() + + # Update bucket count + aw_buckets_total.set(len(buckets)) + + # Server info + aw_server_info.info({ + 'host': AW_SERVER_HOST, + 'port': AW_SERVER_PORT, + 'scheme': AW_SERVER_SCHEME, + 'api_base': self.api_base + }) + + # Collector status + collectors = {} + for bucket_id, bucket_data in buckets.items(): + client = bucket_data.get('client', 'unknown') + hostname = bucket_data.get('hostname', 'unknown') + bucket_type = bucket_data.get('type', 'unknown') + + # Count events + events = self.get_bucket_events(bucket_id, limit=1000) + event_count = len(events) + aw_bucket_events_count.labels(bucket=bucket_id).set(event_count) + + # Last event timestamp + if events: + last_event = events[0] + timestamp = last_event.get('timestamp', 0) + try: + # Convert to Unix timestamp if needed + if isinstance(timestamp, str): + dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) + unix_ts = dt.timestamp() + else: + unix_ts = timestamp + aw_events_last_timestamp.labels(bucket=bucket_id).set(unix_ts) + except: + pass + + # Collector status + collector_key = f"{hostname}_{client}" + collectors[collector_key] = { + 'status': 'active', + 'bucket': bucket_id, + 'type': bucket_type, + 'events': event_count + } + + aw_collector_status.info(collectors) + +def main(): + exporter = ActivityWatchExporter(AW_API_BASE) + + # Initial collection + exporter.collect_metrics() + + # Start HTTP server + start_http_server(EXPORTER_PORT) + logger.info(f"ActivityWatch exporter started on port {EXPORTER_PORT}") + logger.info(f"Scraping ActivityWatch API at {AW_API_BASE}") + + # Collect metrics every 30 seconds + while True: + time.sleep(30) + exporter.collect_metrics() + +if __name__ == '__main__': + main()