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>
This commit is contained in:
igor04091968
2026-05-05 01:32:50 +03:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 10c22a6c88
commit d5735ba127
8 changed files with 324 additions and 0 deletions
+20
View File
@@ -1,4 +1,23 @@
services: 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: sql-exporter:
image: ghcr.io/burningalchemist/sql_exporter:latest image: ghcr.io/burningalchemist/sql_exporter:latest
container_name: awrus-sql-exporter container_name: awrus-sql-exporter
@@ -30,6 +49,7 @@ services:
ports: ports:
- "${PROMETHEUS_PORT:-9090}:9090" - "${PROMETHEUS_PORT:-9090}:9090"
depends_on: depends_on:
- aw-exporter
- sql-exporter - sql-exporter
grafana: grafana:
@@ -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}
]
}
}
}
}
}
]
}
}
@@ -9,3 +9,12 @@ providers:
editable: false editable: false
options: options:
path: /var/lib/grafana/dashboards path: /var/lib/grafana/dashboards
- name: ActivityWatch
orgId: 2
folder: "ActivityWatch"
type: file
disableDeletion: false
editable: true
options:
path: /var/lib/grafana/dashboards
@@ -0,0 +1,9 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
@@ -0,0 +1,9 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
+5
View File
@@ -11,6 +11,11 @@ scrape_configs:
static_configs: static_configs:
- targets: ["localhost:9090"] - targets: ["localhost:9090"]
- job_name: aw_activitywatch_exporter
static_configs:
- targets: ["aw-exporter:9398"]
scrape_interval: 30s
- job_name: onec_sql_exporter - job_name: onec_sql_exporter
static_configs: static_configs:
- targets: ["sql-exporter:9399"] - targets: ["sql-exporter:9399"]
+9
View File
@@ -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"]
@@ -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()