diff --git a/create_dashboard.py b/create_dashboard.py new file mode 100644 index 00000000..a6b235b5 --- /dev/null +++ b/create_dashboard.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import json + +dashboard = { + "annotations": {"list": []}, + "editable": True, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": None, + "links": [], + "panels": [ + { + "title": "Air Quality (PM2.5)", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [{"query": 'from(bucket:"smartcity") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r["_measurement"] == "airquality") |> filter(fn: (r) => r["_field"] == "pm25_ugm3") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: "mean")'}], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0} + }, + { + "title": "Traffic Flow (Vehicles)", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [{"query": 'from(bucket:"smartcity") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r["_measurement"] == "traffic") |> filter(fn: (r) => r["_field"] == "vehicle_count") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: "mean")'}], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0} + }, + { + "title": "Parking Occupancy (%)", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [{"query": 'from(bucket:"smartcity") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r["_measurement"] == "parking") |> filter(fn: (r) => r["_field"] == "occupancy_percent") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: "mean")'}], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8} + }, + { + "title": "Noise Levels (dB)", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [{"query": 'from(bucket:"smartcity") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r["_measurement"] == "noise") |> filter(fn: (r) => r["_field"] == "noise_level_db") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: "mean")'}], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8} + }, + { + "title": "Weather (Temperature °C)", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [{"query": 'from(bucket:"smartcity") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r["_measurement"] == "weather") |> filter(fn: (r) => r["_field"] == "temperature_c") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: "mean")'}], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 16} + }, + { + "title": "Light Levels", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [{"query": 'from(bucket:"smartcity") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r["_measurement"] == "light") |> filter(fn: (r) => r["_field"] == "luminosity") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: "mean")'}], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 16} + } + ], + "schemaVersion": 36, + "style": "dark", + "tags": ["smartcity", "martinique", "iot"], + "templating": {"list": []}, + "time": {"from": "now-1h", "to": "now"}, + "title": "Smart City Digital Twin - Martinique", + "uid": "smartcity-martinique-v2", + "version": 1 +} + +with open('/home/eric/smart-city-digital-twin-martinique/grafana-dashboard-smartcity.json', 'w') as f: + json.dump(dashboard, f, indent=2) +print("Dashboard JSON created successfully") diff --git a/create_dashboard_docker.py b/create_dashboard_docker.py new file mode 100644 index 00000000..55f80799 --- /dev/null +++ b/create_dashboard_docker.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +import json +import requests + +# UID de la datasource Prometheus (smart-city-prometheus-brokers) +# À récupérer via l'API Grafana +try: + resp = requests.get('https://grafana.digitribe.fr/api/datasources', + auth=('admin', 'Digitribe972'), verify=False) + ds_uid = None + if resp.ok: + for ds in resp.json(): + if 'prometheus' in ds['type'].lower() and 'broker' in ds['name'].lower(): + ds_uid = ds['uid'] + print(f"Datasource Prometheus trouvée: {ds['name']} (UID: {ds_uid})") + break +except: + pass + +if not ds_uid: + ds_uid = 'f9ddd651-33ec-4dad-a950-e1375a964315' # Fallback Prometheus Brokers + print(f"Utilisation UID par défaut: {ds_uid}") + +dashboard = { + "annotations": {"list": []}, + "editable": True, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": None, + "links": [], + "panels": [ + # CPU Usage + { + "title": "CPU Usage (Docker Containers)", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}, + "datasource": {"type": "prometheus", "uid": ds_uid}, + "targets": [ + { + "expr": 'rate(docker_container_cpu_usage_seconds_total[5m]) * 100', + "legendFormat": "{{container}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percent" + } + } + }, + # Memory Usage + { + "title": "Memory Usage (Docker Containers)", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}, + "datasource": {"type": "prometheus", "uid": ds_uid}, + "targets": [ + { + "expr": 'docker_container_memory_usage_bytes / 1024 / 1024 / 1024', + "legendFormat": "{{container}} (GB)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "decbytes" + } + } + }, + # Network Traffic (RX) + { + "title": "Network Receive (Docker Containers)", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 16}, + "datasource": {"type": "prometheus", "uid": ds_uid}, + "targets": [ + { + "expr": 'rate(docker_container_network_receive_bytes_total[5m]) * 8', + "legendFormat": "{{container}} RX", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "bps" + } + } + }, + # Network Traffic (TX) + { + "title": "Network Transmit (Docker Containers)", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 16}, + "datasource": {"type": "prometheus", "uid": ds_uid}, + "targets": [ + { + "expr": 'rate(docker_container_network_transmit_bytes_total[5m]) * 8', + "legendFormat": "{{container}} TX", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "bps" + } + } + }, + # Container Status + { + "title": "Container Status (1=Running, 0=Stopped)", + "type": "state-timeline", + "gridPos": {"h": 8, "w": 24, "x": 0, "y": 32}, + "datasource": {"type": "prometheus", "uid": ds_uid}, + "targets": [ + { + "expr": 'docker_container_status', + "legendFormat": "{{container}}", + "refId": "A" + } + ] + } + ], + "schemaVersion": 38, + "style": "dark", + "tags": ["docker", "containers", "metrics", "prometheus"], + "templating": {"list": []}, + "time": {"from": "now-1h", "to": "now"}, + "title": "Smart City - Docker Containers Metrics", + "uid": "smartcity-docker-metrics", + "version": 1 +} + +# Sauvegarder localement +with open('/home/eric/smart-city-digital-twin-martinique/grafana-dashboard-docker-metrics.json', 'w') as f: + json.dump(dashboard, f, indent=2) + +print("✅ Dashboard Docker Metrics généré") +print(f" Fichier: grafana-dashboard-docker-metrics.json") +print(f" UID: {dashboard['uid']}") +print(f" Datasource Prometheus: {ds_uid}") diff --git a/create_dashboard_fixed.py b/create_dashboard_fixed.py new file mode 100644 index 00000000..2e7e3757 --- /dev/null +++ b/create_dashboard_fixed.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +import json + +# UID de la source InfluxDB (à récupérer via l'API Grafana) +# On va utiliser l'UID par défaut ou le récupérer +import requests +import os + +# Récupérer l'UID de la datasource InfluxDB +try: + resp = requests.get('http://grafana.digitribe.fr/api/datasources', auth=('admin', 'Digitribe972')) + ds_uid = None + if resp.ok: + for ds in resp.json(): + if 'influx' in ds['type'].lower(): + ds_uid = ds['uid'] + print(f"Datasource InfluxDB trouvée: {ds['name']} (UID: {ds_uid})") + break +except: + pass + +if not ds_uid: + ds_uid = 'influxdb' # Fallback + print(f"Utilisation UID par défaut: {ds_uid}") + +dashboard = { + "annotations": {"list": []}, + "editable": True, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": None, + "links": [], + "panels": [ + # Air Quality Panel + { + "title": "Air Quality - PM2.5", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}, + "targets": [ + { + "query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "airquality")\n |> filter(fn: (r) => r["_field"] == "pm25_ugm3")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")', + "refId": "A" + } + ], + "datasource": {"type": "influxdb", "uid": ds_uid}, + }, + { + "title": "Air Quality - CO", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}, + "targets": [ + { + "query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "airquality")\n |> filter(fn: (r) => r["_field"] == "co_mgm3")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")', + "refId": "A" + } + ], + "datasource": {"type": "influxdb", "uid": ds_uid}, + }, + # Traffic Panel + { + "title": "Traffic - Average Speed", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}, + "targets": [ + { + "query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "traffic")\n |> filter(fn: (r) => r["_field"] == "average_speed_kmh")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")', + "refId": "A" + } + ], + "datasource": {"type": "influxdb", "uid": ds_uid}, + }, + { + "title": "Traffic - Congestion", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}, + "targets": [ + { + "query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "traffic")\n |> filter(fn: (r) => r["_field"] == "congestion_level")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")', + "refId": "A" + } + ], + "datasource": {"type": "influxdb", "uid": ds_uid}, + }, + # Parking Panel + { + "title": "Parking - Available Spots", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 16}, + "targets": [ + { + "query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "parking")\n |> filter(fn: (r) => r["_field"] == "available_spots")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")', + "refId": "A" + } + ], + "datasource": {"type": "influxdb", "uid": ds_uid}, + }, + { + "title": "Parking - Occupancy %", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 16}, + "targets": [ + { + "query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "parking")\n |> filter(fn: (r) => r["_field"] == "occupancy_percent")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")', + "refId": "A" + } + ], + "datasource": {"type": "influxdb", "uid": ds_uid}, + }, + # Noise Panel + { + "title": "Noise Level (dB)", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 24}, + "targets": [ + { + "query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "noise")\n |> filter(fn: (r) => r["_field"] == "noise_level_db")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")', + "refId": "A" + } + ], + "datasource": {"type": "influxdb", "uid": ds_uid}, + }, + # Weather Panel + { + "title": "Weather - Temperature", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 24}, + "targets": [ + { + "query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "weather")\n |> filter(fn: (r) => r["_field"] == "temperature_celsius")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")', + "refId": "A" + } + ], + "datasource": {"type": "influxdb", "uid": ds_uid}, + }, + # Light Panel + { + "title": "Light - Brightness", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 32}, + "targets": [ + { + "query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "light")\n |> filter(fn: (r) => r["_field"] == "brightness_lux")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")', + "refId": "A" + } + ], + "datasource": {"type": "influxdb", "uid": ds_uid}, + }, + { + "title": "Light - Power Consumption", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 32}, + "targets": [ + { + "query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "light")\n |> filter(fn: (r) => r["_field"] == "power_consumption_w")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")', + "refId": "A" + } + ], + "datasource": {"type": "influxdb", "uid": ds_uid}, + } + ], + "schemaVersion": 38, + "style": "dark", + "tags": ["smart-city", "martinique", "iot"], + "templating": {"list": []}, + "time": {"from": "now-1h", "to": "now"}, + "title": "Smart City Digital Twin - Martinique (Fixed)", + "uid": "smartcity-martinique-2026-v2", + "version": 2 +} + +# Sauvegarder +with open('/home/eric/smart-city-digital-twin-martinique/grafana-dashboard-fixed.json', 'w') as f: + json.dump(dashboard, f, indent=2) + +print("✅ Dashboard avec bonnes requêtes Flux créé") +print(f" Fichier: grafana-dashboard-fixed.json") +print(f" Datasource UID: {ds_uid}") \ No newline at end of file diff --git a/docker-compose.ditto.yml b/docker-compose.ditto.yml new file mode 100644 index 00000000..a1175823 --- /dev/null +++ b/docker-compose.ditto.yml @@ -0,0 +1,91 @@ +# Eclipse Ditto - Smart City Digital Twin (MongoDB fix) +version: '3.8' + +services: + ditto-mongodb: + image: mongo:6 + container_name: smart-city-ditto-mongodb + restart: unless-stopped + networks: + traefik-public: + aliases: + - ditto-mongodb + volumes: + - ditto-mongo-data:/data/db + + ditto-policies: + image: eclipse/ditto-policies:latest + container_name: smart-city-ditto-policies + restart: unless-stopped + hostname: ditto-policies + depends_on: + - ditto-mongodb + environment: + - DITTO_JWT_SECRET=my-ditto-secret-12345 + - MONGO_HOST=ditto-mongodb + - MONGO_PORT=27017 + - MONGO_DB=Policies + # Supprimer MONGO_URI pour éviter confusion + networks: + traefik-public: + aliases: + - ditto-cluster + - ditto-policies + labels: + - "traefik.enable=true" + - "traefik.http.routers.ditto-policies.rule=Host(`ditto-policies.digitribe.fr`)" + - "traefik.http.routers.ditto-policies.entrypoints=web" + - "traefik.http.services.ditto-policies.loadbalancer.server.port=8080" + + ditto-things: + image: eclipse/ditto-things:latest + container_name: smart-city-ditto-things + restart: unless-stopped + hostname: ditto-things + depends_on: + - ditto-mongodb + - ditto-policies + environment: + - DITTO_JWT_SECRET=my-ditto-secret-12345 + - MONGO_HOST=ditto-mongodb + - MONGO_PORT=27017 + - MONGO_DB=Things + networks: + traefik-public: + aliases: + - ditto-cluster + - ditto-things + labels: + - "traefik.enable=true" + - "traefik.http.routers.ditto-things.rule=Host(`ditto-things.digitribe.fr`)" + - "traefik.http.routers.ditto-things.entrypoints=web" + - "traefik.http.services.ditto-things.loadbalancer.server.port=8080" + + ditto-gateway: + image: eclipse/ditto-gateway:latest + container_name: smart-city-ditto-gateway + restart: unless-stopped + hostname: ditto-gateway + depends_on: + - ditto-things + - ditto-policies + environment: + - DITTO_JWT_SECRET=my-ditto-secret-12345 + - DITTO_GATEWAY_PROXY_ENABLED=false + networks: + traefik-public: + aliases: + - ditto-cluster + - ditto-gateway + labels: + - "traefik.enable=true" + - "traefik.http.routers.ditto-gateway.rule=Host(`ditto.digitribe.fr`)" + - "traefik.http.routers.ditto-gateway.entrypoints=web" + - "traefik.http.services.ditto-gateway.loadbalancer.server.port=8080" + +networks: + traefik-public: + external: true + +volumes: + ditto-mongo-data: diff --git a/docker-compose.quantumleap.yml b/docker-compose.quantumleap.yml index ba60fd16..5fc03d0a 100644 --- a/docker-compose.quantumleap.yml +++ b/docker-compose.quantumleap.yml @@ -31,7 +31,10 @@ services: retries: 5 quantumleap: - image: fiware/quantum-leap:latest + build: + context: ./quantumleap + dockerfile: Dockerfile + image: quantumleap-patched:latest container_name: smart-city-quantumleap restart: unless-stopped environment: diff --git a/docker_exporter.py b/docker_exporter.py new file mode 100644 index 00000000..5e852189 --- /dev/null +++ b/docker_exporter.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Simple Docker metrics exporter for Prometheus""" +import docker +from prometheus_client import Gauge, Counter, generate_latest, CONTENT_TYPE_LATEST +from http.server import HTTPServer, BaseHTTPRequestHandler +import sys + +# Métriques +container_cpu_usage = Gauge('docker_container_cpu_usage_seconds_total', 'CPU usage in seconds', ['container', 'image']) +container_memory_usage = Gauge('docker_container_memory_usage_bytes', 'Memory usage in bytes', ['container', 'image']) +container_network_rx = Counter('docker_container_network_receive_bytes_total', 'Network receive bytes', ['container', 'image']) +container_network_tx = Counter('docker_container_network_transmit_bytes_total', 'Network transmit bytes', ['container', 'image']) +container_status = Gauge('docker_container_status', 'Container status (1=running, 0=stopped)', ['container', 'image']) + +client = docker.from_env() + +class MetricsHandler(BaseHTTPRequestHandler): + def do_GET(self): + # Mettre à jour les métriques + for container in client.containers.list(): + name = container.name + image = container.image.tags[0] if container.image.tags else 'unknown' + + try: + stats = container.stats(stream=False) + + # CPU + cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - stats['precpu_stats']['cpu_usage']['total_usage'] + system_delta = stats['cpu_stats']['system_cpu_usage'] - stats['precpu_stats']['system_cpu_usage'] + if system_delta > 0: + cpu_usage = (cpu_delta / system_delta) * stats['cpu_stats'].get('online_cpus', 1) + container_cpu_usage.labels(name, image).set(cpu_usage) + + # Memory + mem_usage = stats['memory_stats']['usage'] + container_memory_usage.labels(name, image).set(mem_usage) + + # Network + if 'networks' in stats: + for iface, data in stats['networks'].items(): + container_network_rx.labels(name, image).inc(data.get('rx_bytes', 0)) + container_network_tx.labels(name, image).inc(data.get('tx_bytes', 0)) + + # Status + container_status.labels(name, image).set(1 if container.status == 'running' else 0) + + except Exception as e: + print(f"Error getting stats for {name}: {e}") + + # Exposer les métriques + self.send_response(200) + self.send_header('Content-Type', CONTENT_TYPE_LATEST) + self.end_headers() + self.wfile.write(generate_latest()) + + def log_message(self, format, *args): + pass # Suppress logs + +if __name__ == '__main__': + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8005 + server = HTTPServer(('0.0.0.0', port), MetricsHandler) + print(f"Docker metrics exporter listening on port {port}") + server.serve_forever() diff --git a/grafana-dashboard-docker-metrics.json b/grafana-dashboard-docker-metrics.json new file mode 100644 index 00000000..6981e956 --- /dev/null +++ b/grafana-dashboard-docker-metrics.json @@ -0,0 +1,155 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "panels": [ + { + "title": "CPU Usage (Docker Containers)", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "datasource": { + "type": "prometheus", + "uid": "f9ddd651-33ec-4dad-a950-e1375a964315" + }, + "targets": [ + { + "expr": "rate(docker_container_cpu_usage_seconds_total[5m]) * 100", + "legendFormat": "{{container}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percent" + } + } + }, + { + "title": "Memory Usage (Docker Containers)", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "datasource": { + "type": "prometheus", + "uid": "f9ddd651-33ec-4dad-a950-e1375a964315" + }, + "targets": [ + { + "expr": "docker_container_memory_usage_bytes / 1024 / 1024 / 1024", + "legendFormat": "{{container}} (GB)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "decbytes" + } + } + }, + { + "title": "Network Receive (Docker Containers)", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "datasource": { + "type": "prometheus", + "uid": "f9ddd651-33ec-4dad-a950-e1375a964315" + }, + "targets": [ + { + "expr": "rate(docker_container_network_receive_bytes_total[5m]) * 8", + "legendFormat": "{{container}} RX", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "bps" + } + } + }, + { + "title": "Network Transmit (Docker Containers)", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "datasource": { + "type": "prometheus", + "uid": "f9ddd651-33ec-4dad-a950-e1375a964315" + }, + "targets": [ + { + "expr": "rate(docker_container_network_transmit_bytes_total[5m]) * 8", + "legendFormat": "{{container}} TX", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "bps" + } + } + }, + { + "title": "Container Status (1=Running, 0=Stopped)", + "type": "state-timeline", + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 32 + }, + "datasource": { + "type": "prometheus", + "uid": "f9ddd651-33ec-4dad-a950-e1375a964315" + }, + "targets": [ + { + "expr": "docker_container_status", + "legendFormat": "{{container}}", + "refId": "A" + } + ] + } + ], + "schemaVersion": 38, + "style": "dark", + "tags": [ + "docker", + "containers", + "metrics", + "prometheus" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "title": "Smart City - Docker Containers Metrics", + "uid": "smartcity-docker-metrics", + "version": 1 +} \ No newline at end of file diff --git a/grafana-dashboard-fixed.json b/grafana-dashboard-fixed.json new file mode 100644 index 00000000..e041b1ba --- /dev/null +++ b/grafana-dashboard-fixed.json @@ -0,0 +1,229 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "title": "Air Quality - PM2.5", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\")\n |> filter(fn: (r) => r[\"_field\"] == \"pm25_ugm3\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "datasource": { + "type": "influxdb", + "uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d" + } + }, + { + "title": "Air Quality - CO", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\")\n |> filter(fn: (r) => r[\"_field\"] == \"co_mgm3\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "datasource": { + "type": "influxdb", + "uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d" + } + }, + { + "title": "Traffic - Average Speed", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"traffic\")\n |> filter(fn: (r) => r[\"_field\"] == \"average_speed_kmh\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "datasource": { + "type": "influxdb", + "uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d" + } + }, + { + "title": "Traffic - Congestion", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"traffic\")\n |> filter(fn: (r) => r[\"_field\"] == \"congestion_level\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "datasource": { + "type": "influxdb", + "uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d" + } + }, + { + "title": "Parking - Available Spots", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"parking\")\n |> filter(fn: (r) => r[\"_field\"] == \"available_spots\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "datasource": { + "type": "influxdb", + "uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d" + } + }, + { + "title": "Parking - Occupancy %", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"parking\")\n |> filter(fn: (r) => r[\"_field\"] == \"occupancy_percent\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "datasource": { + "type": "influxdb", + "uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d" + } + }, + { + "title": "Noise Level (dB)", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"noise\")\n |> filter(fn: (r) => r[\"_field\"] == \"noise_level_db\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "datasource": { + "type": "influxdb", + "uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d" + } + }, + { + "title": "Weather - Temperature", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"weather\")\n |> filter(fn: (r) => r[\"_field\"] == \"temperature_celsius\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "datasource": { + "type": "influxdb", + "uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d" + } + }, + { + "title": "Light - Brightness", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"light\")\n |> filter(fn: (r) => r[\"_field\"] == \"brightness_lux\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "datasource": { + "type": "influxdb", + "uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d" + } + }, + { + "title": "Light - Power Consumption", + "type": "timeseries", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"light\")\n |> filter(fn: (r) => r[\"_field\"] == \"power_consumption_w\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "datasource": { + "type": "influxdb", + "uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d" + } + } + ], + "schemaVersion": 38, + "style": "dark", + "tags": [ + "smart-city", + "martinique", + "iot" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "title": "Smart City Digital Twin - Martinique (Fixed)", + "uid": "smartcity-martinique-2026-v2", + "version": 2 +} \ No newline at end of file diff --git a/grafana-dashboard-smartcity.json b/grafana-dashboard-smartcity.json new file mode 100644 index 00000000..f8c8d7c9 --- /dev/null +++ b/grafana-dashboard-smartcity.json @@ -0,0 +1,143 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "title": "Air Quality (PM2.5)", + "type": "timeseries", + "datasource": { + "type": "influxdb", + "uid": "influxdb-smartcity" + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\") |> filter(fn: (r) => r[\"_field\"] == \"pm25_ugm3\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + } + ], + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + } + }, + { + "title": "Traffic Flow (Vehicles)", + "type": "timeseries", + "datasource": { + "type": "influxdb", + "uid": "influxdb-smartcity" + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"traffic\") |> filter(fn: (r) => r[\"_field\"] == \"vehicle_count\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + } + ], + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + } + }, + { + "title": "Parking Occupancy (%)", + "type": "timeseries", + "datasource": { + "type": "influxdb", + "uid": "influxdb-smartcity" + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"parking\") |> filter(fn: (r) => r[\"_field\"] == \"occupancy_percent\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + } + ], + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + } + }, + { + "title": "Noise Levels (dB)", + "type": "timeseries", + "datasource": { + "type": "influxdb", + "uid": "influxdb-smartcity" + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"noise\") |> filter(fn: (r) => r[\"_field\"] == \"noise_level_db\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + } + ], + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + } + }, + { + "title": "Weather (Temperature \u00b0C)", + "type": "timeseries", + "datasource": { + "type": "influxdb", + "uid": "influxdb-smartcity" + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"weather\") |> filter(fn: (r) => r[\"_field\"] == \"temperature_c\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + } + ], + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + } + }, + { + "title": "Light Levels", + "type": "timeseries", + "datasource": { + "type": "influxdb", + "uid": "influxdb-smartcity" + }, + "targets": [ + { + "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"light\") |> filter(fn: (r) => r[\"_field\"] == \"luminosity\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + } + ], + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + } + } + ], + "schemaVersion": 36, + "style": "dark", + "tags": [ + "smartcity", + "martinique", + "iot" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "title": "Smart City Digital Twin - Martinique", + "uid": "smartcity-martinique-v2", + "version": 1 +} \ No newline at end of file diff --git a/grafana-dashboard-test.json b/grafana-dashboard-test.json new file mode 100644 index 00000000..24f28c86 --- /dev/null +++ b/grafana-dashboard-test.json @@ -0,0 +1,29 @@ +{ + "annotations": {"list": []}, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "title": "TEST - Air Quality PM2.5 (Last 5 min)", + "type": "timeseries", + "gridPos": {"h": 8, "w": 24, "x": 0, "y": 0}, + "datasource": {"type": "influxdb", "uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: -5m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\")\n |> filter(fn: (r) => r[\"_field\"] == \"pm25_ugm3\")\n |> aggregateWindow(every: 10s, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "refId": "A" + } + ] + } + ], + "schemaVersion": 38, + "style": "dark", + "tags": ["test"], + "time": {"from": "now-5m", "to": "now"}, + "title": "Smart City - TEST DATA", + "uid": "smartcity-test-v1", + "version": 1 +} \ No newline at end of file diff --git a/import_dashboard.py b/import_dashboard.py new file mode 100644 index 00000000..91a53607 --- /dev/null +++ b/import_dashboard.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +import json +import requests + +# Read dashboard JSON +with open('/home/eric/smart-city-digital-twin-martinique/grafana-dashboard-smartcity.json', 'r') as f: + dashboard = json.load(f) + +# Prepare payload for Grafana API +payload = { + "dashboard": dashboard, + "overwrite": True, + "message": "Smart City Dashboard - Martinique" +} + +# Import to Grafana +url = "http://grafana.digitribe.fr/api/dashboards/db" +auth = ('admin', 'Digitribe972') + +try: + r = requests.post(url, json=payload, auth=auth) + print(f"Status: {r.status_code}") + print(r.json()) +except Exception as e: + print(f"Error: {e}") diff --git a/import_dashboard_fixed.py b/import_dashboard_fixed.py new file mode 100644 index 00000000..c86e74a7 --- /dev/null +++ b/import_dashboard_fixed.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +import json +import requests + +# Read the fixed dashboard +with open('/home/eric/smart-city-digital-twin-martinique/grafana-dashboard-fixed.json', 'r') as f: + dashboard = json.load(f) + +# Import to Grafana +url = "http://grafana.digitribe.fr/api/dashboards/db" +auth = ('admin', 'Digitribe972') + +payload = { + "dashboard": dashboard, + "overwrite": True +} + +try: + resp = requests.post(url, json=payload, auth=auth) + print(f"Status: {resp.status_code}") + print(resp.json()) +except Exception as e: + print(f"Error: {e}") diff --git a/init/iot-agent-provision.sh b/init/iot-agent-provision.sh new file mode 100755 index 00000000..ca90a0e6 --- /dev/null +++ b/init/iot-agent-provision.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Wait for IoT Agent to be ready +sleep 10 +# Provision Service +curl -s -X POST http://localhost:4041/iot/services \ + -H "Content-Type: application/json" \ + -H "fiware-service: smartcity" \ + -H "fiware-servicepath: /" \ + -d '{ + "services": [{ + "apikey": "smartcity-api-key", + "cbroker": "http://smart-city-orion-ld:1026", + "entity_type": "Thing", + "resource": "/iot/json" + }] +}' +# Provision Devices (Traffic, Parking, Noise, Weather, Light) +for i in 0 1 2; do + curl -s -X POST http://localhost:4041/iot/devices \ + -H "Content-Type: application/json" \ + -H "fiware-service: smartcity" \ + -H "fiware-servicepath: /" \ + -d "{\"devices\": [{\"device_id\": \"traffic_00${i}\", \"entity_name\": \"urn:ngsi-ld:TrafficFlowObserved:traffic_00${i}\", \"entity_type\": \"TrafficFlowObserved\", \"protocol\": \"PDI-IoTA-JSON\", \"transport\": \"MQTT\"}]}"; +done +# (Add other types similarly) +echo "Provisioning done" diff --git a/prometheus.yml b/prometheus.yml index d87fac2a..282223e0 100644 --- a/prometheus.yml +++ b/prometheus.yml @@ -93,3 +93,11 @@ scrape_configs: labels: service: grafana environment: martinique + + # ── Docker Exporter (Custom Python exporter) ────────────────────── + - job_name: 'docker-exporter' + static_configs: + - targets: ['172.17.0.1:8005'] + labels: + service: docker-exporter + environment: martinique diff --git a/quantumleap/Dockerfile b/quantumleap/Dockerfile new file mode 100644 index 00000000..38fdc851 --- /dev/null +++ b/quantumleap/Dockerfile @@ -0,0 +1,5 @@ +FROM fiware/quantum-leap:latest +USER root +# Patch _filter_empty_entities to handle flat NGSI-v2 attribute values +RUN sed -i "s/if 'value' in payload\[j\]:/if isinstance(payload[j], dict) and 'value' in payload[j]:/" /src/ngsi-timeseries-api/src/reporter/reporter.py && \ + sed -i "s/if isinstance(value, int) and value is not None:/if isinstance(value, (int, float)) and value is not None:/" /src/ngsi-timeseries-api/src/reporter/reporter.py diff --git a/telegraf.conf b/telegraf.conf index cf23e30c..ece83106 100644 --- a/telegraf.conf +++ b/telegraf.conf @@ -9,18 +9,56 @@ flush_interval = "10s" flush_jitter = "0s" -# Input: MQTT Consumer +# Input: MQTT Consumer - EMQX [[inputs.mqtt_consumer]] servers = ["tcp://emqx_emqx_1:1883"] topics = [ "airquality/#", "traffic/#", + "parking/#", + "noise/#", + "weather/#", + "light/#", "sensor/#", "smartcity/#" ] data_format = "json" qos = 0 +# Input: MQTT Consumer - Mosquitto +[[inputs.mqtt_consumer]] + servers = ["tcp://smart-city-mosquitto:1883"] + topics = [ + "airquality/#", + "traffic/#", + "parking/#", + "noise/#", + "weather/#", + "light/#", + "sensor/#", + "smartcity/#" + ] + data_format = "json" + qos = 0 + +# Input: MQTT Consumer - BunkerM (with auth) +[[inputs.mqtt_consumer]] + servers = ["tcp://bunkerm_bunkerm_1:1900"] + topics = [ + "airquality/#", + "traffic/#", + "parking/#", + "noise/#", + "weather/#", + "light/#", + "sensor/#", + "smartcity/#" + ] + data_format = "json" + qos = 0 + username = "bunker" + password = "bunker" + # Output: InfluxDB v2 [[outputs.influxdb_v2]] urls = ["http://smart-city-influxdb:8086"]