From e618cbfcb9e7a7f0d9b9e2c44308a8c561e0d7ba Mon Sep 17 00:00:00 2001 From: Eric FELIXINE Date: Tue, 5 May 2026 01:53:37 -0400 Subject: [PATCH] feat: migrate InfluxDB and Grafana from digital-twin/ to smart-city/ stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-compose.influxdb.yml: InfluxDB v2 on smartcity-shared + traefik-public - docker-compose.grafana.yml: Grafana 10.2 on smartcity-shared + traefik-public - grafana/provisioning/: dashboards + datasources updated for smart-city - pulsar/docker-compose.yml: added smartcity-shared network for simulator access Services migrated (preserving existing volumes): - digital-twin-influxdb → smart-city-influxdb - digital-twin-grafana → smart-city-grafana Traefik routes updated: - influxdb.digitribe.fr → smart-city-influxdb:8086 - grafana.digitribe.fr → smart-city-grafana:3000 --- docker-compose.grafana.yml | 38 ++ docker-compose.influxdb.yml | 38 ++ .../provisioning/dashboards/dashboards.yml | 13 + .../dashboards/smart-city-dashboards.json | 16 + .../dashboards/smart-city-overview.json | 348 ++++++++++++++++++ .../dashboards/twin-overview.json | 84 +++++ .../dashboards/twin-supply-chain.json | 16 + .../provisioning/datasources/datasources.yml | 51 +++ pulsar/docker-compose.yml | 43 +++ 9 files changed, 647 insertions(+) create mode 100644 docker-compose.grafana.yml create mode 100644 docker-compose.influxdb.yml create mode 100644 grafana/provisioning/dashboards/dashboards.yml create mode 100644 grafana/provisioning/dashboards/smart-city-dashboards.json create mode 100644 grafana/provisioning/dashboards/smart-city-overview.json create mode 100644 grafana/provisioning/dashboards/twin-overview.json create mode 100644 grafana/provisioning/dashboards/twin-supply-chain.json create mode 100644 grafana/provisioning/datasources/datasources.yml create mode 100644 pulsar/docker-compose.yml diff --git a/docker-compose.grafana.yml b/docker-compose.grafana.yml new file mode 100644 index 00000000..959ff177 --- /dev/null +++ b/docker-compose.grafana.yml @@ -0,0 +1,38 @@ +# Grafana - Visualization dashboards for Smart City Digital Twin Martinique +# Usage: docker compose -f docker-compose.grafana.yml up -d +# Note: run from the project root or pass -p smart-city to attach to the smart-city project + +networks: + smartcity-shared: + external: true + traefik-public: + external: true + +volumes: + grafana_data: + external: false + name: digital-twin_grafana_data + +services: + grafana: + image: grafana/grafana:10.2.0 + container_name: smart-city-grafana + networks: + - smartcity-shared + - traefik-public + ports: + - "3001:3000" + environment: + # Anonymous auth - must match the org name in Grafana's database + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_NAME=Digitribe + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + # Admin credentials + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=Digitribe972 + # Plugins + - GF_INSTALL_PLUGINS=grafana-piechart-panel,grafana-simple-json-datasource + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + restart: unless-stopped diff --git a/docker-compose.influxdb.yml b/docker-compose.influxdb.yml new file mode 100644 index 00000000..9ab92de8 --- /dev/null +++ b/docker-compose.influxdb.yml @@ -0,0 +1,38 @@ +# InfluxDB v2 - Time-series database for Smart City IoT analytics +# Usage: docker compose -f docker-compose.influxdb.yml up -d + +networks: + smartcity-shared: + external: true + traefik-public: + external: true + +volumes: + influxdb_data: + external: false + name: digital-twin_influxdb_data + +services: + influxdb: + image: influxdb:2.7-alpine + container_name: smart-city-influxdb + networks: + - smartcity-shared + - traefik-public + ports: + - "8086:8086" + environment: + - DOCKER_INFLUXDB_INIT_MODE=setup + - DOCKER_INFLUXDB_INIT_USERNAME=admin + - DOCKER_INFLUXDB_INIT_PASSWORD=admin1234 + - DOCKER_INFLUXDB_INIT_ORG=digitribe + - DOCKER_INFLUXDB_INIT_BUCKET=iot_data + - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-admin-token + volumes: + - influxdb_data:/var/lib/influxdb2 + restart: unless-stopped + healthcheck: + test: ["CMD", "influx", "ping"] + interval: 30s + timeout: 10s + retries: 5 diff --git a/grafana/provisioning/dashboards/dashboards.yml b/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 00000000..e42c7f17 --- /dev/null +++ b/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,13 @@ +apiVersion: 1 + +providers: + - name: 'Smart City Dashboards' + orgId: 1 + folder: 'Smart City' + folderUid: 'smart-city' + type: file + disableDeletion: false + updateIntervalSeconds: 30 + allowUiUpdates: true + options: + path: /etc/grafana/provisioning/dashboards diff --git a/grafana/provisioning/dashboards/smart-city-dashboards.json b/grafana/provisioning/dashboards/smart-city-dashboards.json new file mode 100644 index 00000000..c186a49b --- /dev/null +++ b/grafana/provisioning/dashboards/smart-city-dashboards.json @@ -0,0 +1,16 @@ +{ + "annotations": {"list": []}, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "schemaVersion": 36, + "style": "dark", + "tags": ["smart-city"], + "templating": {"list": []}, + "time": {"from": "now-24h", "to": "now"}, + "title": "Smart City Dashboards", + "timezone": "Americas/Martinique", + "uid": "smart-city-dashboards" +} diff --git a/grafana/provisioning/dashboards/smart-city-overview.json b/grafana/provisioning/dashboards/smart-city-overview.json new file mode 100644 index 00000000..0a379c00 --- /dev/null +++ b/grafana/provisioning/dashboards/smart-city-overview.json @@ -0,0 +1,348 @@ +{ + "timezone": "Americas/Martinique", + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d" + ], + "nowDelay": "5s" + }, + "title": "Smart City Digital Twin - Overview", + "tags": [ + "smart-city", + "digital-twin", + "overview" + ], + "schemaVersion": 16, + "version": 1, + "refresh": "5s", + "panels": [ + { + "id": 1, + "title": "Total Vehicles", + "type": "stat", + "gridPos": { + "x": 0, + "y": 0, + "w": 4, + "h": 4 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -24h) |> filter(fn: (r) => r[\"_measurement\"] == \"traffic\") |> group(columns: [\"sensor_id\"]) |> sum()" + } + ], + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto" + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 150 + }, + { + "color": "red", + "value": 300 + } + ] + } + } + } + }, + { + "id": 2, + "title": "Average Air Quality Index", + "type": "gauge", + "gridPos": { + "x": 4, + "y": 0, + "w": 4, + "h": 4 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -24h) |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\") |> mean(column: \"air_quality_index\")" + } + ], + "options": { + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "fieldConfig": { + "defaults": { + "min": 1, + "max": 5, + "unit": "short", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 2 + }, + { + "color": "orange", + "value": 3 + }, + { + "color": "red", + "value": 4 + } + ] + } + } + } + }, + { + "id": 3, + "title": "Available Parking Spots", + "type": "stat", + "gridPos": { + "x": 8, + "y": 0, + "w": 4, + "h": 4 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -24h) |> filter(fn: (r) => r[\"_measurement\"] == \"parking\") |> sum(column: \"available_spots\")" + } + ], + "fieldConfig": { + "defaults": { + "unit": "short", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "green", + "value": 100 + } + ] + } + } + } + }, + { + "id": 4, + "title": "Average Noise Level", + "type": "gauge", + "gridPos": { + "x": 12, + "y": 0, + "w": 4, + "h": 4 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -24h) |> filter(fn: (r) => r[\"_measurement\"] == \"noise\") |> mean(column: \"noise_level_db\")" + } + ], + "options": { + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "fieldConfig": { + "defaults": { + "min": 30, + "max": 100, + "unit": "dB", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 60 + }, + { + "color": "orange", + "value": 75 + }, + { + "color": "red", + "value": 85 + } + ] + } + } + } + }, + { + "id": 5, + "title": "Traffic Over Time", + "type": "timeseries", + "gridPos": { + "x": 0, + "y": 4, + "w": 12, + "h": 6 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -24h) |> filter(fn: (r) => r[\"_measurement\"] == \"traffic\") |> aggregateWindow(every: 1m, fn: mean)" + } + ], + "options": { + "legend": { + "displayMode": "hidden" + }, + "tooltip": { + "mode": "single" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "gradientMode": "opacity" + } + } + } + }, + { + "id": 6, + "title": "Air Quality - PM2.5", + "type": "timeseries", + "gridPos": { + "x": 12, + "y": 4, + "w": 12, + "h": 6 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -24h) |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\") |> aggregateWindow(every: 1m, fn: mean)" + } + ], + "options": { + "legend": { + "displayMode": "hidden" + }, + "tooltip": { + "mode": "single" + } + }, + "fieldConfig": { + "defaults": { + "unit": "\u03bcg/m\u00b3", + "color": { + "mode": "palette-classic" + } + } + } + }, + { + "id": 7, + "title": "Parking Availability", + "type": "piechart", + "gridPos": { + "x": 0, + "y": 10, + "w": 8, + "h": 6 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -24h) |> filter(fn: (r) => r[\"_measurement\"] == \"parking\") |> group(columns: [\"sensor_name\"]) |> sum()" + } + ], + "options": { + "pieType": "donut", + "displayLabels": "name", + "legend": { + "displayMode": "table", + "placement": "right", + "values": [ + "value" + ] + } + } + }, + { + "id": 8, + "title": "Weather Conditions", + "type": "timeseries", + "gridPos": { + "x": 8, + "y": 10, + "w": 16, + "h": 6 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -24h) |> filter(fn: (r) => r[\"_measurement\"] == \"weather\") |> aggregateWindow(every: 5m, fn: mean)" + } + ], + "options": { + "legend": { + "displayMode": "hidden" + }, + "tooltip": { + "mode": "single" + } + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 5 + } + } + } + } + ], + "time": { + "from": "now-24h", + "to": "now" + } +} \ No newline at end of file diff --git a/grafana/provisioning/dashboards/twin-overview.json b/grafana/provisioning/dashboards/twin-overview.json new file mode 100644 index 00000000..0c30710b --- /dev/null +++ b/grafana/provisioning/dashboards/twin-overview.json @@ -0,0 +1,84 @@ +{ + "timezone": "Americas/Martinique", + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d" + ] + }, + "title": "TWIN Supply Chain - Overview", + "tags": [ + "twin", + "supply-chain" + ], + "panels": [ + { + "id": 1, + "title": "Active Shipments", + "type": "stat", + "gridPos": { + "x": 0, + "y": 0, + "w": 6, + "h": 4 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -1h) |> filter(fn: (r) => r[\"_measurement\"] == \"shipment\") |> count()" + } + ], + "fieldConfig": { + "defaults": { + "unit": "short" + } + } + }, + { + "id": 2, + "title": "Inventory Level", + "type": "stat", + "gridPos": { + "x": 6, + "y": 0, + "w": 6, + "h": 4 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -1h) |> filter(fn: (r) => r[\"_measurement\"] == \"inventory\") |> mean()" + } + ], + "fieldConfig": { + "defaults": { + "unit": "short" + } + } + }, + { + "id": 3, + "title": "Pending Orders", + "type": "stat", + "gridPos": { + "x": 12, + "y": 0, + "w": 6, + "h": 4 + }, + "targets": [ + { + "query": "from(bucket: \"iot_data\") |> range(start: -1h) |> filter(fn: (r) => r[\"_measurement\"] == \"order\") |> count()" + } + ], + "fieldConfig": { + "defaults": { + "unit": "short" + } + } + } + ] +} \ No newline at end of file diff --git a/grafana/provisioning/dashboards/twin-supply-chain.json b/grafana/provisioning/dashboards/twin-supply-chain.json new file mode 100644 index 00000000..b376b5f2 --- /dev/null +++ b/grafana/provisioning/dashboards/twin-supply-chain.json @@ -0,0 +1,16 @@ +{ + "annotations": {"list": []}, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "schemaVersion": 36, + "style": "dark", + "tags": ["twin", "supply-chain"], + "templating": {"list": []}, + "time": {"from": "now-24h", "to": "now"}, + "title": "TWIN Supply Chain", + "timezone": "Americas/Martinique", + "uid": "twin-supply-chain" +} diff --git a/grafana/provisioning/datasources/datasources.yml b/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 00000000..ec71f1a2 --- /dev/null +++ b/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,51 @@ +# Grafana datasources - Smart City Digital Twin Martinique +# Each datasource is editable and uses the container DNS name in smartcity-shared network +apiVersion: 1 + +datasources: + # ── InfluxDB v2 (time-series IoT data) ────────────────────────────────────── + - name: InfluxDB-v2 + type: influxdb + access: proxy + url: http://smart-city-influxdb:8086 + isDefault: true + editable: true + jsonData: + version: Flux + organization: digitribe + defaultBucket: iot_data + secureJsonData: + token: my-super-secret-admin-token + + # ── FIWARE Orion-LD (NGSI-LD context broker) ──────────────────────────────── + # Requires grafana-simple-json-datasource plugin + - name: FIWARE Orion + type: grafana-simple-json-datasource + access: proxy + url: http://fiware-gis-quickstart-orion-1:1026 + editable: true + jsonData: + queryURLTemplate: "/ngsi-ld/v1/entities?type={{type}}" + method: GET + + # ── GeoServer WMS (spatial data) ──────────────────────────────────────────── + # GeoServer is an external service reachable via its container name + - name: GeoServer WMS + type: grafana-simple-json-datasource + access: proxy + url: http://docker-geoserver-1:8080/geoserver + editable: true + jsonData: + queryURLTemplate: "/geoserver/wfs?service=WFS&version=2.0&request=GetFeature&typeName={{type}}" + method: GET + + # ── FROST-Server (SensorThings API) ────────────────────────────────────────── + # Requires grafana-simple-json-datasource plugin + - name: FROST-Server + type: grafana-simple-json-datasource + access: proxy + url: http://frost-api-8090:8090/FROST-Server/v1.1 + editable: true + jsonData: + queryURLTemplate: "/Things?$top=1" + method: GET diff --git a/pulsar/docker-compose.yml b/pulsar/docker-compose.yml new file mode 100644 index 00000000..a113b0db --- /dev/null +++ b/pulsar/docker-compose.yml @@ -0,0 +1,43 @@ +# Apache Pulsar Standalone - Smart City Digital Twin Martinique +# HTTP Admin UI: https://pulsar.digitribe.fr (via Traefik) +# HTTP API: http://smart-city-pulsar:8080/admin/v2 +# Binary: pulsar://smart-city-pulsar:6650 +services: + pulsar: + image: apachepulsar/pulsar:3.2.0 + container_name: smart-city-pulsar + restart: unless-stopped + user: "10000:0" + ports: + - "6650:6650" + - "8080:8080" + environment: + PULSAR_MEM: "-Xms512m -Xmx512m -XX:MaxDirectMemorySize=512m" + PULSAR_STANDALONE_USE_ZOOKEEPER: "true" + volumes: + - pulsar-data:/pulsar/data + networks: + - traefik-public + - smartcity-shared + command: ["/pulsar/bin/pulsar", "standalone", "-nfw"] + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8080/admin/v2/clusters || exit 1"] + interval: 30s + timeout: 10s + retries: 10 + start_period: 60s + labels: + - "traefik.enable=true" + - "traefik.http.routers.pulsar.rule=Host(`pulsar.digitribe.fr`)" + - "traefik.http.routers.pulsar.entrypoints=websecure" + - "traefik.http.routers.pulsar.tls=true" + - "traefik.http.services.pulsar.loadbalancer.server.port=8080" + +networks: + traefik-public: + external: true + smartcity-shared: + external: true + +volumes: + pulsar-data: