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:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
10c22a6c88
commit
d5735ba127
@@ -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
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user