diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6beeaea --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +# License Information + +This repository contains multiple components, each with its own licensing terms. Please refer to the LICENSE file in each subdirectory for specific licensing information. + +## Subdirectory Licenses + +- **dashboard-deployments/** - Licensed under the Elastic License 2.0. See [dashboard-deployments/LICENSE](./dashboard-deployments/LICENSE) for full terms. +- **Other components** - Each subdirectory may have its own specific license. Check individual subdirectories for their respective LICENSE files. + +## General Guidelines + +Before using any component from this repository: + +1. Navigate to the specific subdirectory you plan to use +2. Read the LICENSE file in that subdirectory +3. Ensure compliance with the specific license terms + +For questions about licensing, please contact the FixedIT team. diff --git a/README.md b/README.md index 2aa93bc..cef2787 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ -# fixedit-data-agent-dashboards +# FixedIT Data Agent Examples -This repository contains an InfluxDB + Grafana stack with provisioned dashboards for Axis camera monitoring with the FixedIT Data Agent edge application. +This repository provides resources for the [FixedIT Data Agent ACAP](https://fixedit.ai/products-data-agent/) for the Axis network cameras and other Axis devices. + +**📊 Server-side dashboards** - The [dashboard-deployments](./dashboard-deployments) directory contains visualization dashboards that work with the FixedIT Data Agent. Some work directly with the bundled configurations (just spin them up and start visualizing), while others can be used as-is or customized for your needs. Advanced users often combine edge device customization with dashboard modifications to visualize new data types. + +The dashboard stack in the image below is the system monitoring example for the bundled configuration in the FixedIT Data Agent, for more details see the [README](./dashboard-deployments/system-monitoring-influxdb2-flux-grafana/README.md) in the dashboard-deployments directory. + +![Grafana Dashboard Overview](./dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/laptop-with-grafana-for-monitoring.png) + +**🛠️ Edge device customization** - Project implementation examples that show how to extend and customize the FixedIT Data Agent by uploading custom configuration files and scripts. This makes it easy to create tailored edge applications for Axis devices without starting from scratch using the AXIS ACAP SDK. + +[Comming soon...] diff --git a/dashboard-deployments/LICENSE b/dashboard-deployments/LICENSE new file mode 100644 index 0000000..9abc7bf --- /dev/null +++ b/dashboard-deployments/LICENSE @@ -0,0 +1,55 @@ +Elastic License 2.0 + +URL: https://www.elastic.co/licensing/elastic-license + +Acceptance + +By using the software, you agree to all of the terms and conditions below. + +Copyright License + +The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below. + +Limitations + +You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software. + +You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key. + +You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor's trademarks is subject to applicable law. + +Patents + +The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +Notices + +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. + +If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software. + +No Other Rights + +These terms do not imply any licenses other than those expressly granted in these terms. + +Termination + +If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently. + +No Liability + +As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim. + +Definitions + +The licensor is the entity offering these terms, and the software is the software the licensor makes available under these terms, including any portion of it. + +you refers to the individual or entity agreeing to these terms. + +your company is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +your licenses are all the licenses granted to you for the software under these terms. + +use means anything you do with the software requiring one of your licenses. + +trademark means trademarks, service marks, and similar rights. \ No newline at end of file diff --git a/dashboard-deployments/README.md b/dashboard-deployments/README.md new file mode 100644 index 0000000..99fae2a --- /dev/null +++ b/dashboard-deployments/README.md @@ -0,0 +1,28 @@ +# Dashboard examples for the FixedIt Data Agent + +This directory contains example dashboard deployments for the FixedIT Data Agent. Depending on what the Data Agent is used for, different stacks of e.g. InfluxDB and Grafana are used together with provisioned dashboards. This makes it easy to try a setup to start visualizing the data from your Axis devices running the FixedIT Data Agent. + +## [System monitoring with InfluxDB (v2) and Grafana](./system-monitoring-influxdb2-flux-grafana) + +This example shows how to set up advanced dashboards for Axis device system monitoring. These dashboards works with the bundled configuration files in the FixedIT Data Agent. The dashboards are built using InfluxDB 2.x as a database and Flux as the query language. + +## Open source licenses + +The dashboards in this repository are open source for your convenience and are licensed under the Elastic License 2.0. This means that in most cases you can use them for both commercial and non-commercial purposes, but there are some exceptions. + +- By default, you are not allowed to use the dashboards to provide a service to third parties. If you want to do that, you need to contact the FixedIT team to get a license. +- When redistributing the dashboards, you need to include our license notice in the redistributed files, source us as the original authors and include any copyright notices. + +For full license details, see the [LICENSE](./LICENSE) file. + +### Can I use the dashboards internally in my own business? + +Yes, you are allowed to use the dashboards internally in your own business (including commercial purposes). + +### Can I setup these dashboards and sell access to them to our customers? + +You will need to contact the FixedIT team to get our approval, which we in most cases are happy to give. + +### Can I use the dashboards internally to monitor a service we sell to customers? + +Yes, you are allowed to use the dashboards internally in your own business, also for monitoring a service you sell to customers. diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-device-details.png b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-device-details.png new file mode 100644 index 0000000..5027725 Binary files /dev/null and b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-device-details.png differ diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-folder-cameras.png b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-folder-cameras.png new file mode 100644 index 0000000..c644d6b Binary files /dev/null and b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-folder-cameras.png differ diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-overview-of-devices.png b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-overview-of-devices.png new file mode 100644 index 0000000..355a7dc Binary files /dev/null and b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-overview-of-devices.png differ diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-system-overview.png b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-system-overview.png new file mode 100644 index 0000000..e98fb1e Binary files /dev/null and b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboard-system-overview.png differ diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboards-tab.png b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboards-tab.png new file mode 100644 index 0000000..9b041ad Binary files /dev/null and b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/dashboards-tab.png differ diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/laptop-with-grafana-for-monitoring.png b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/laptop-with-grafana-for-monitoring.png new file mode 100644 index 0000000..131787e Binary files /dev/null and b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/.images/laptop-with-grafana-for-monitoring.png differ diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/README.md b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/README.md new file mode 100644 index 0000000..77d231a --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/README.md @@ -0,0 +1,188 @@ +# InfluxDB and Grafana Monitoring Stack + +This directory contains a complete system monitoring stack designed to work with the **FixedIT Data Agent** running on Axis devices. The stack includes InfluxDB for time-series data storage and Grafana with pre-built dashboards for visualizing system metrics and device health from your Axis device fleet. + +![Grafana Monitoring Overview](.images/laptop-with-grafana-for-monitoring.png) + +## What This Stack Provides + +This monitoring solution gives you comprehensive visibility into your Axis devices through three specialized dashboards: + +- **System Overview**: Fleet-wide metrics including connected devices, recently lost devices, and performance outliers +- **Overview of Devices**: Individual device information with detailed metrics, IP addresses, and system health +- **Device Details**: Deep-dive into specific device performance including CPU, RAM, network, and geo-location data + +The stack is designed for DevOps and IT professionals who want to deploy monitoring infrastructure for devices running the FixedIT Data Agent, without needing to create dashboards from scratch. + +## The Database and Query Language + +This monitoring stack uses **InfluxDB 2** with the **Flux query language** for all data queries and dashboard visualizations. + +**Important Note**: InfluxDB 3 has moved away from Flux to SQL-based queries. However, we chose to use InfluxDB 2 with Flux for this stack because: + +- InfluxDB 2 with Flux is still the most widely adopted version in the community +- Most existing time-series monitoring setups use Flux +- The Flux query language is well-suited for time-series data analysis + +If you're already using InfluxDB 3 in your environment, you would need to adapt the dashboard queries from Flux to SQL syntax. + +## Prerequisites + +- Docker and Docker Compose installed on your server +- Network connectivity between your Axis devices (running FixedIT Data Agent) and the server hosting this stack +- Basic familiarity with InfluxDB and Grafana (helpful but not required) + +## Quick Start + +Choose one of the deployment options below: + +### Option A: Simple Demo (Recommended for Testing) + +This is the quickest way to test the monitoring stack on your local machine. Everything is pre-configured with hardcoded values. + +1. **Start the stack:** + + ```bash + docker compose up -d + ``` + +2. **Verify it's running:** + - **InfluxDB**: [http://127.0.0.1:8086](http://127.0.0.1:8086) - login with username `test` and password `testtest` + - **Grafana**: [http://127.0.0.1:3000](http://127.0.0.1:3000) - login with username `admin` and password `test` + +⚠️ **Note**: This uses hardcoded credentials and ports. Only use for local testing. + +### Option B: Production-Like Example + +This provides a more secure example with environment variables and additional monitoring features. It's still an **example to build upon**, not a complete production solution. + +1. **Generate secure environment variables:** + + ```bash + ./helper_scripts/generate-env.sh + source env.sh + ``` + +2. **Start the enhanced stack:** + ```bash + docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d + ``` + +This example includes: + +- Environment variables for all sensitive data +- Configurable ports for multiple instances +- Health checks for both services (needs some more work) + +⚠️ **Note**: This is still an example configuration. For actual production use, you'll need additional security hardening, backup strategies, and monitoring. + +### Port Configuration + +The production-like setup uses configurable ports to allow running multiple instances on the same server. The demo setup uses hardcoded ports (8086 for InfluxDB, 3000 for Grafana) for simplicity. You can change the production ports by editing the `env.sh` file after generation with the `helper_scripts/generate-env.sh` script or by manually setting the environment variables before launching the stack. + +### Deployment Organization + +This dashboard stack can be deployed multiple times to the same server. This is useful if you want to monitor multiple sites and have them completely isolated from each other. To enable this, you need to use the production override file (`docker-compose.prod.yml`) with the generated environment variables. This will make sure that the ports are not the same and that a project specific prefix will be used for the container names. + +The intended deployment pattern is to use **separate folders for different deployments**: + +- Copy the content of this directory to separate folders (e.g., `site-1-dashboard/`, `site-2-dashboard/`, etc.) +- Each folder contains the same code and has its own `env.sh` file with unique credentials and ports +- Data volumes mount to consistent folder names within each deployment directory +- This makes it easy to see which code version was used for each deployment and keeps credentials isolated. + +## Configure Your FixedIT Data Agent + +After deploying the stack, configure your Axis devices to send data to it: + +**For Simple Demo (Option A):** + +- **InfluxDBHost**: `http://YOUR_SERVER_IP:8086` +- **InfluxDBToken**: `KLoG_Z0NsDIbVzS7zVn_VwhgxUgvwaGWE1wwO9SsGfNEeMaopLMsAA2aAGbCshpetVdu86Ig3-WTKugv6Srg6w==` (demo admin token) +- **InfluxDBOrganization**: `Whisperer` +- **InfluxDBBucket**: `Cameras` + +_Note: The demo uses the admin token for simplicity. For any real testing beyond local evaluation, create dedicated tokens as described below._ + +**For Production-Like Example (Option B):** + +- **InfluxDBHost**: `http://YOUR_SERVER_IP:${INFLUXDB_PORT}` (check your env.sh for port) +- **InfluxDBToken**: Create a dedicated token in InfluxDB (see security note below) +- **InfluxDBOrganization**: `Whisperer` (hardcoded in docker-compose.yml) +- **InfluxDBBucket**: `Cameras` (hardcoded in docker-compose.yml) + +⚠️ **Security Best Practice**: Do NOT use `${INFLUXDB_ADMIN_TOKEN}` for your cameras. Instead, log into InfluxDB and create dedicated tokens with write-only permissions to the `Cameras` bucket. Ideally create one token per camera, or at minimum a shared token with restricted permissions. + +**To create a camera token in InfluxDB:** + +1. Log into InfluxDB UI with admin credentials +2. Go to "Data" → "API Tokens" → "Generate API Token" +3. Choose "Custom API Token" +4. Grant only "Write" permission to the `Cameras` bucket +5. Use this token for your FixedIT Data Agent configuration + +**⚠️ Grafana Security Note**: Grafana also uses the admin token by default (configured via environment variables in docker-compose files). For better security, you should: + +1. Create a separate token for Grafana with only "Read" permission to the `Cameras` bucket +2. Log into Grafana → Configuration → Data Sources → InfluxDB +3. Update the token field with your new read-only token +4. Test the connection to ensure it works + +**Summary of recommended tokens for secure setup:** + +- **Admin token**: Keep for InfluxDB administration only +- **Camera tokens**: Write-only access to `Cameras` bucket (one per camera ideally) +- **Grafana token**: Read-only access to `Cameras` bucket + +Once configured, metrics will appear in the Grafana dashboards within a few minutes. + +## Grafana Dashboards + +Open the Grafana UI to access the pre-built dashboards. The URL and credentials depend on your deployment option: + +- **Option A (Simple Demo)**: [http://127.0.0.1:3000](http://127.0.0.1:3000) - login with username `admin` and password `test` +- **Option B (Production-Like)**: Check your `env.sh` file for the port and admin password + +### Accessing the Dashboards + +After logging into Grafana, navigate to the dashboards by clicking on the Grafana icon and selecting "Dashboards": + +![Dashboards tab in Grafana](.images/dashboards-tab.png) + +The dashboards are organized in a "Cameras" folder for easy organization: + +![Dashboard folder containing camera dashboards](.images/dashboard-folder-cameras.png) + +### Pre-built Dashboard Overview + +This stack includes three comprehensive dashboards designed specifically for monitoring Axis devices running the FixedIT Data Agent: + +#### 1. System Overview Dashboard + +Provides fleet-wide visibility including connected device counts, recently lost devices, and performance outliers. This dashboard is designed to give you a high-level view of your entire device ecosystem without overwhelming detail on individual devices. + +![System Overview Dashboard](.images/dashboard-system-overview.png) + +#### 2. Overview of Devices Dashboard + +Shows detailed information for all connected devices including last report times, AXIS OS versions, IP addresses, and individual device performance metrics. Perfect for monitoring the health and status of your device fleet. + +![Overview of Devices Dashboard](.images/dashboard-overview-of-devices.png) + +#### 3. Device Details Dashboard + +Provides deep-dive analytics for individual devices, including detailed system metrics, geographic tagging information, and network performance data. Access this dashboard by clicking on any device identifier from the other dashboards. + +![Device Details Dashboard](.images/dashboard-device-details.png) + +### Integration with FixedIT Data Agent + +These dashboards are designed to work seamlessly with the default configuration of the FixedIT Data Agent. The agent automatically collects and sends system metrics to InfluxDB, which are then visualized through these Grafana dashboards. No additional configuration is needed beyond setting up the InfluxDB connection details in your FixedIT Data Agent settings. + +## Known Issues and Limitations + +**A note on efficiency**: Some of the dashboard visualizations are rather compute-intensive and may perform slowly when used with many cameras, a large amount of data or insufficient server resources. We will continue to optimize the dashboards and the data model to improve performance. + +## Helper Scripts + +The directory [helper_scripts](./helper_scripts/) contains scripts that can assist with deployment, see the [README](./helper_scripts/README.md) for more details. diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/docker-compose.prod.yml b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/docker-compose.prod.yml new file mode 100644 index 0000000..32fd416 --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/docker-compose.prod.yml @@ -0,0 +1,38 @@ +# This override file: +# - Adds healthchecks to InfluxDB and Grafana +# - Overrides production secrets for InfluxDB and Grafana +# - Supports multiple deployments on same host using COMPOSE_PROJECT_NAME +# +# This stack is only intended as an example. + +version: "3.8" + +services: + influxdb: + container_name: influxdb-${COMPOSE_PROJECT_NAME:-default} + environment: + - DOCKER_INFLUXDB_INIT_USERNAME=admin + - DOCKER_INFLUXDB_INIT_PASSWORD=${INFLUXDB_PASSWORD} + - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=${INFLUXDB_ADMIN_TOKEN} + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8086/health"] + interval: 30s + timeout: 5s + retries: 3 + labels: + - "com.ouroboros.enable=true" # Explicitly allow monitoring + restart: unless-stopped + + grafana: + container_name: grafana-${COMPOSE_PROJECT_NAME:-default} + environment: + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} + - INFLUXDB_TOKEN=${INFLUXDB_ADMIN_TOKEN} + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] + interval: 30s + timeout: 5s + retries: 3 + labels: + - "com.ouroboros.enable=true" # Explicitly allow monitoring + restart: unless-stopped diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/docker-compose.yml b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/docker-compose.yml new file mode 100644 index 0000000..a33eeb6 --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/docker-compose.yml @@ -0,0 +1,46 @@ +# This is a simple example of a stack with InfluxDB and Grafana. +# This file hard codes credentials for simple use. It is not intended for production use. +# For production use, use this file together with the docker-compose.prod.yml override file +# which will add more security and robustness. + +version: "3.8" + +x-common-vars: &common-vars + INFLUXDB_ORG: &org Whisperer + INFLUXDB_BUCKET: &bucket Cameras + +services: + influxdb: + image: influxdb:2 + ports: + - "${INFLUXDB_PORT:-8086}:8086" + environment: + DOCKER_INFLUXDB_INIT_MODE: setup + DOCKER_INFLUXDB_INIT_USERNAME: test + DOCKER_INFLUXDB_INIT_PASSWORD: testtest + DOCKER_INFLUXDB_INIT_ORG: *org + DOCKER_INFLUXDB_INIT_BUCKET: *bucket + DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: KLoG_Z0NsDIbVzS7zVn_VwhgxUgvwaGWE1wwO9SsGfNEeMaopLMsAA2aAGbCshpetVdu86Ig3-WTKugv6Srg6w== + volumes: + - ./influxdb-data:/var/lib/influxdb2 + + grafana: + image: grafana/grafana + ports: + - "${GRAFANA_PORT:-3000}:3000" + # To avoid permission issues to the mounted dir we run as root... + # Should be changed. See: + # https://grafana.com/docs/grafana/latest/setup-grafana/installation/run-grafana-docker-image/#migrate-to-v51-or-later + user: "0" + environment: + GF_SECURITY_ADMIN_PASSWORD: test + INFLUXDB_TOKEN: KLoG_Z0NsDIbVzS7zVn_VwhgxUgvwaGWE1wwO9SsGfNEeMaopLMsAA2aAGbCshpetVdu86Ig3-WTKugv6Srg6w== + INFLUXDB_ORG: *org + INFLUXDB_BUCKET: *bucket + volumes: + - ./grafana-data:/var/lib/grafana + # The provisioning folder contains the data source info. + # This automatically sets up InfluxDB as a data source. + - ./provisioning:/etc/grafana/provisioning + depends_on: + - influxdb diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/helper_scripts/README.md b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/helper_scripts/README.md new file mode 100644 index 0000000..93d3196 --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/helper_scripts/README.md @@ -0,0 +1,32 @@ +# Helper scripts + +## [install-docker-aws-ec2.sh](./install-docker-aws-ec2.sh) + +This script can be used to install docker with docker compose on an AWS EC2 instance with Amazon Linux 2. This makes it easy to deploy the example dashboard to an AWS EC2 instance. + +Just copy the script to the EC2 instance and run it with `./install-docker-aws-ec2.sh`. + +## [generate-env.sh](./generate-env.sh) + +This script generates an `env.sh` file with secure random values for all environment variables required by the production docker-compose setup (`docker-compose.prod.yml`). + +The script generates: + +- **Unique project name**: `monitoring-{suffix}` for container isolation where `{suffix}` is a random 6-character hex string (e.g., `a1b2c3`) +- **Random port numbers**: For InfluxDB and Grafana (to avoid conflicts) +- **Secure passwords**: For InfluxDB and Grafana +- **Secure admin token**: For InfluxDB +- **Placeholder for Slack webhook URL**: (needs manual update) + +Usage: + +```bash +./helper_scripts/generate-env.sh +``` + +This will create an `env.sh` file in the project root. To use it: + +```bash +source env.sh +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d +``` diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/helper_scripts/generate-env.sh b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/helper_scripts/generate-env.sh new file mode 100755 index 0000000..c2f2988 --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/helper_scripts/generate-env.sh @@ -0,0 +1,95 @@ +#!/bin/bash +set -euo pipefail + +# Script to generate environment variables for prod docker-compose setup +# This script creates an env.sh file with secure random values for all required variables + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$SCRIPT_DIR/../env.sh" + +# Check if openssl is available +if ! command -v openssl &> /dev/null; then + echo "❌ Error: openssl is required but not installed." + echo "Please install openssl and try again." + exit 1 +fi + +# Check if env.sh already exists to prevent accidental overwrite +if [[ -f "$ENV_FILE" ]]; then + echo "❌ Error: $ENV_FILE already exists." + echo "To prevent accidental loss of secrets, please delete it manually first." + echo "Run: rm $ENV_FILE" + exit 1 +fi + +echo "Generating production environment variables..." + +# Generate secure passwords (~32 characters) +# 24 bytes -> base64 (32 chars) -> remove "=+/" -> ~32 chars +INFLUXDB_PASSWORD=$(openssl rand -base64 24 | tr -d "=+/") +GRAFANA_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d "=+/") + +# Generate secure admin token (32 bytes = 64 hex characters) +INFLUXDB_ADMIN_TOKEN=$(openssl rand -hex 32) + +# Generate random deployment suffix (6 alphanumeric characters). +# This allows multiple deployments to run in parallel on the same machine. +DEPLOYMENT_SUFFIX=$(openssl rand -hex 3) + +# Generate random ports within safe ranges +# InfluxDB: 10000-19999 (10k ports) +# Grafana: 20000-29999 (10k ports) +INFLUXDB_PORT=$((10000 + RANDOM % 10000)) +GRAFANA_PORT=$((20000 + RANDOM % 10000)) + +# Create the env.sh file with secure permissions. +OLD_UMASK=$(umask) +umask 077 +cat > "$ENV_FILE" << EOF +#!/bin/bash +# Generated environment variables for production docker-compose setup +# Generated on: $(date) +# +# To use these variables, source this file: +# source env.sh +# +# Then run docker-compose with production overrides: +# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d + +# Deployment Configuration +export COMPOSE_PROJECT_NAME="monitoring-$DEPLOYMENT_SUFFIX" + +# Port Configuration +export INFLUXDB_PORT="$INFLUXDB_PORT" +export GRAFANA_PORT="$GRAFANA_PORT" + +# InfluxDB Configuration +export INFLUXDB_PASSWORD="$INFLUXDB_PASSWORD" +export INFLUXDB_ADMIN_TOKEN="$INFLUXDB_ADMIN_TOKEN" + +# Grafana Configuration +export GRAFANA_ADMIN_PASSWORD="$GRAFANA_ADMIN_PASSWORD" + +# Additional configuration can be added here as needed + +EOF + +# Restore original umask. This is mostly important if someone would +# source the script instead of running it. +umask "$OLD_UMASK" + +# Print a summary of the generated values and some instructions. +echo "✅ Environment file generated: $ENV_FILE" +echo "" +echo "📋 Generated values:" +echo " DEPLOYMENT_SUFFIX: $DEPLOYMENT_SUFFIX" +echo " COMPOSE_PROJECT_NAME: monitoring-$DEPLOYMENT_SUFFIX" +echo " INFLUXDB_PORT: $INFLUXDB_PORT" +echo " GRAFANA_PORT: $GRAFANA_PORT" +echo " INFLUXDB_PASSWORD: $INFLUXDB_PASSWORD" +echo " INFLUXDB_ADMIN_TOKEN: $INFLUXDB_ADMIN_TOKEN" +echo " GRAFANA_ADMIN_PASSWORD: $GRAFANA_ADMIN_PASSWORD" +echo "" +echo "⚠️ IMPORTANT:" +echo " 1. Keep this file secure and don't commit it to version control" +echo " 2. To use: source env.sh && docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d" \ No newline at end of file diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/helper_scripts/install-docker-aws-ec2.sh b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/helper_scripts/install-docker-aws-ec2.sh new file mode 100755 index 0000000..a4a0383 --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/helper_scripts/install-docker-aws-ec2.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -euo pipefail + +# Install Docker +sudo yum update -y +sudo amazon-linux-extras enable docker +sudo yum install -y docker +sudo systemctl start docker +sudo systemctl enable docker +sudo usermod -aG docker $USER + +# Install Docker Compose as a plugin +DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker} +mkdir -p "$DOCKER_CONFIG/cli-plugins" + +curl -SL https://github.com/docker/compose/releases/download/v2.37.3/docker-compose-linux-x86_64 \ + -o "$DOCKER_CONFIG/cli-plugins/docker-compose" + +chmod +x "$DOCKER_CONFIG/cli-plugins/docker-compose" \ No newline at end of file diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/camera_dashboards.yml b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/camera_dashboards.yml new file mode 100644 index 0000000..62fa088 --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/camera_dashboards.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: "Camera monitoring dashboards" + folder: "Cameras" + type: file + disableDeletion: false + editable: true + options: + # Look for dashboards in this folder (json files) + path: /etc/grafana/provisioning/dashboards diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/device_details.json b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/device_details.json new file mode 100644 index 0000000..479e460 --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/device_details.json @@ -0,0 +1,1105 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 4, + "links": [], + "panels": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "/.*/", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": "// Get the latest timestamp where product_full_name was reported for the selected camera\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n // Filter on measurement to get a consistent schema\n |> filter(fn: (r) => r[\"_measurement\"] == \"cpu\")\n // Create a stream for each device\n |> group(columns: [\"${unique_identifier}\"])\n // Sort and get the latest\n |> sort(columns: [\"_time\"], desc: true)\n |> first()\n |> keep(columns: [\"${unique_identifier}\", \"product_full_name\"])", + "refId": "A" + } + ], + "title": "Device Model Name", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 11, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": "from(bucket: \"${bucket}\")\n // Time range filter...\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n\n // Generic measurement just to access the tags...\n |> filter(fn: (r) => r[\"_measurement\"] == \"cpu\")\n\n // Filter based on drop-downs\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n\n // Sort and take the latest received tags for each device\n |> group(columns: [\"${unique_identifier}\"])\n |> sort(columns: [\"_time\"], desc: true)\n |> limit(n:1)\n\n // Keep things we want only\n |> keep(columns: [\"_time\", \"${unique_identifier}\", \"area\", \"geography\", \"region\", \"site\", \"type\", \"product_full_name\"])\n\n // Flatten\n |> group()\n", + "refId": "A" + } + ], + "title": "Geo Tags", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "_time": true, + "product_full_name": true, + "type": true + }, + "includeByName": {}, + "indexByName": { + "_time": 7, + "area": 6, + "geography": 1, + "host": 0, + "product_full_name": 2, + "region": 3, + "site": 4, + "type": 5 + }, + "renameByName": { + "area": "Area", + "geography": "Geography", + "host": "Device", + "product_full_name": "", + "region": "Region", + "site": "Site" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 77, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 3, + "spanNulls": false + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 10, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.78, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n\n // Generic measurement just to access the tags. To reduce number of points,\n // we also filter to one single core and field.\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"cpu\" and\n r[\"cpu\"] == \"cpu-total\" and\n r[\"_field\"] == \"usage_idle\"\n )\n\n // Filter for matching devices\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n\n // Aggregate into time series windows to reduce number of returned data points\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n\n // Keep timestamp, device and type\n |> keep(columns: [\"_time\", \"${unique_identifier}\", \"type\"])\n\n // Rename and remap to Grafana-friendly _time, _field, _value,\n // that gets rid of the \"type \" prefix in the visualization.\n |> map(fn: (r) => ({\n _time: r._time,\n _field: r[\"${unique_identifier}\"],\n _value: r[\"type\"]\n }))\n\n // Group per device (now _field)\n |> group(columns: [\"_field\"])\n\n // Sort by time\n |> sort(columns: [\"_time\"])\n", + "refId": "B" + } + ], + "title": "Device Type Tag Over Time", + "type": "state-timeline" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 4, + "panels": [], + "title": "Device Metrics", + "type": "row" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 3600 + }, + { + "color": "#EAB839", + "value": 86400 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": " from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"system\")\n |> filter(fn: (r) => r[\"_field\"] == \"uptime\")\n |> group(columns: [\"${unique_identifier}\"]) // flatten tag sets for host\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", + "refId": "A" + } + ], + "title": "Device Uptime", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 3600000, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 50000000 + }, + { + "color": "#EAB839", + "value": 100000000 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": " from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"mem\")\n |> filter(fn: (r) => r[\"_field\"] == \"available\")\n |> group(columns: [\"${unique_identifier}\"]) // flatten tag sets for host\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", + "refId": "A" + } + ], + "title": "Free RAM", + "type": "timeseries" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 3600000, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": " from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"cpu\")\n |> filter(fn: (r) => r[\"_field\"] == \"usage_idle\")\n |> group(columns: [\"${unique_identifier}\", \"cpu\"]) // flatten tag sets for host\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n ", + "refId": "A" + } + ], + "title": "Free CPU", + "type": "timeseries" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 3600000, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Total.*/" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 11 + }, + { + "id": "custom.showPoints", + "value": "never" + }, + { + "id": "color", + "value": { + "fixedColor": "#ffffff", + "mode": "shades" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": "free = \nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"disk\")\n |> filter(fn: (r) => r[\"_field\"] == \"free\")\n |> filter(fn: (r) => r[\"path\"] == \"/usr/local\")\n |> group(columns: [\"${unique_identifier}\"])\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"${unique_identifier}\"])\n |> rename(columns: {_value: \"Free\"})\n\ntotal =\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"disk\")\n |> filter(fn: (r) => r[\"_field\"] == \"total\")\n |> filter(fn: (r) => r[\"path\"] == \"/usr/local\")\n |> group(columns: [\"${unique_identifier}\"])\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"${unique_identifier}\"])\n |> rename(columns: {_value: \"Total\"})\n\njoin(\n tables: {free: free, total: total},\n on: [\"_time\", \"${unique_identifier}\"]\n)\n", + "refId": "A" + } + ], + "title": "Free Flash Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 3600000, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Total.*/" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 11 + }, + { + "id": "custom.showPoints", + "value": "never" + }, + { + "id": "color", + "value": { + "fixedColor": "#ffffff", + "mode": "shades" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 30 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": "free = \nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"disk\")\n |> filter(fn: (r) => r[\"_field\"] == \"free\")\n |> filter(fn: (r) => r[\"path\"] == \"/var/spool/storage/SD_DISK\")\n |> group(columns: [\"${unique_identifier}\"])\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"${unique_identifier}\"])\n |> rename(columns: {_value: \"Free\"})\n\ntotal =\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"disk\")\n |> filter(fn: (r) => r[\"_field\"] == \"total\")\n |> filter(fn: (r) => r[\"path\"] == \"/var/spool/storage/SD_DISK\")\n |> group(columns: [\"${unique_identifier}\"])\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"${unique_identifier}\"])\n |> rename(columns: {_value: \"Total\"})\n\njoin(\n tables: {free: free, total: total},\n on: [\"_time\", \"${unique_identifier}\"]\n)\n", + "refId": "A" + } + ], + "title": "Free SD Card Storage", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 6, + "panels": [], + "title": "Network Info", + "type": "row" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "/.*/", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": " // Get latest local and external IP per device in one query\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n // Select both relevant measurements\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"local_ip_addresses\" or\n r[\"_measurement\"] == \"external_ip\"\n )\n // Group by device and measurement\n |> group(columns: [\"${unique_identifier}\", \"_measurement\"])\n // Get the latest entry for each\n |> last()\n // Reshape for clarity\n |> pivot(\n rowKey: [\"${unique_identifier}\"],\n columnKey: [\"_measurement\"],\n valueColumn: \"_value\"\n )\n // Optional: Rename columns for clarity\n |> rename(columns: {\n local_ip_addresses: \"Local IPs\",\n external_ip: \"External IP\"\n })\n |> keep(columns: [\"${unique_identifier}\", \"Local IPs\", \"External IP\"])", + "refId": "A" + } + ], + "title": "Device IP", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 17, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 42 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"internet_connectivity_dns\")\n |> filter(fn: (r) => r[\"result\"] == \"success\")\n |> filter(fn: (r) => r[\"_field\"] == \"query_time_ms\")\n |> group(columns: [\"${unique_identifier}\"]) // flatten tag sets for host\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", + "refId": "A" + } + ], + "title": "DNS Response Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 64, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 3600000, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 4, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 52 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"internet_connectivity_dns\")\n // Map success to 1, others to 0\n |> map(fn: (r) => ({\n r with _value: if r[\"result\"] == \"success\" then 1 else 0\n }))\n |> group(columns: [\"${unique_identifier}\"])\n // Calculate the mean for each block to reduce number of data points\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n", + "refId": "A" + } + ], + "title": "DNS Connection OK", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [ + { + "allowCustomValue": false, + "current": { + "text": [], + "value": [] + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\nschema.tagValues(\n bucket: \"${bucket}\",\n tag: \"host\",\n)", + "label": "Device", + "multi": true, + "name": "device", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\nschema.tagValues(\n bucket: \"${bucket}\",\n tag: \"host\",\n)" + }, + "refresh": 1, + "regex": "", + "sort": 5, + "type": "query" + }, + { + "current": { + "text": "host", + "value": "host" + }, + "description": "", + "hide": 2, + "name": "unique_identifier", + "query": "host", + "skipUrlSync": true, + "type": "constant" + }, + { + "allowCustomValue": false, + "current": { + "text": "InfluxDB", + "value": "InfluxDB" + }, + "hide": 2, + "name": "datasource", + "options": [], + "query": "influxdb", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "allowCustomValue": false, + "definition": "buckets()", + "hide": 2, + "name": "bucket", + "options": [], + "query": { + "query": "buckets()" + }, + "refresh": 1, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Device Details", + "uid": "bekds38txl1j4c", + "version": 35 +} diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/overview_of_devices.json b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/overview_of_devices.json new file mode 100644 index 0000000..dffc8cb --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/overview_of_devices.json @@ -0,0 +1,1194 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 5, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 15, + "panels": [], + "title": "Overview", + "type": "row" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "internet latency" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "host" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Device detail link", + "url": "/d/${device_details_uid:raw}/device-details?var-device=${__value.text}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Product Name" + }, + "properties": [ + { + "id": "custom.width", + "value": 305 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Device" + }, + "properties": [ + { + "id": "custom.width", + "value": 184 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "AXIS OS" + }, + "properties": [ + { + "id": "custom.width", + "value": 124 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "External IP" + }, + "properties": [ + { + "id": "custom.width", + "value": 155 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Local IPs" + }, + "properties": [ + { + "id": "custom.width", + "value": 477 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 20, + "x": 0, + "y": 1 + }, + "id": 17, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "hide": false, + "query": "// Get cameras with reported data in the selected interval\r\nfrom(bucket: \"${bucket}\")\r\n // Filter on time\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n // Filter using variable selectors (supports All and multi-select)\r\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\r\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\r\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\r\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\r\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\r\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\r\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\r\n // We cant flatten if the values are of different types, so we just\r\n // filter for an arbitrary value that we know exists...\r\n |> filter(fn: (r) =>\r\n r[\"_measurement\"] == \"cpu\" and\r\n r[\"cpu\"] == \"cpu-total\" and\r\n r[\"_field\"] == \"usage_idle\"\r\n )\r\n // Group by device to merge to one time series per device\r\n |> group(columns: [\"${unique_identifier}\"])\r\n // Get the last data point for each device\r\n |> last()\r\n // Ungroup to prepare a flat output\r\n |> group()\r\n // Keep only timestamp and device identifier\r\n |> keep(columns: [\"_time\", \"${unique_identifier}\"])\r\n // Rename _time to seen\r\n |> rename(columns: {_time: \"seen\"})", + "refId": "Get last time stamp" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "// Get latest external IP per camera in the selected interval\nfrom(bucket: \"${bucket}\")\n // Filter on time\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter using variable selectors (supports All and multi-select)\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n // Filter on external IP measurement\n |> filter(fn: (r) => r[\"_measurement\"] == \"external_ip\")\n // Group by device to isolate each host\n |> group(columns: [\"${unique_identifier}\"])\n // Get the last reported external IP\n |> last()\n // Ungroup to flatten result set\n |> group()\n // Rename the value column and keep only relevant fields\n |> rename(columns: {_value: \"external ip\"})\n |> keep(columns: [\"${unique_identifier}\", \"external ip\"])\n", + "refId": "Get external IPs" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "// Get latest local IP per camera in the selected interval\nfrom(bucket: \"${bucket}\")\n // Filter on time\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter using variable selectors (supports All and multi-select)\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n // Filter on local IP measurement\n |> filter(fn: (r) => r[\"_measurement\"] == \"local_ip_addresses\")\n // Group by device to isolate each host\n |> group(columns: [\"${unique_identifier}\"])\n // Get the last reported local IP\n |> last()\n // Ungroup to flatten result set\n |> group()\n // Rename the value column and keep only relevant fields\n |> rename(columns: {_value: \"local ip\"})\n |> keep(columns: [\"${unique_identifier}\", \"local ip\"])\n", + "refId": "Get internal IPs" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "// Get latest internet latency per camera in the selected interval\nfrom(bucket: \"${bucket}\")\n // Filter on time\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter using variable selectors (supports All and multi-select)\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n // Filter on internet connectivity measurement and field\n |> filter(fn: (r) => r[\"_measurement\"] == \"internet_connectivity\")\n |> filter(fn: (r) => r[\"_field\"] == \"response_time\")\n // Group by device to isolate each host\n |> group(columns: [\"${unique_identifier}\"])\n // Get the last reported response time\n |> last()\n // Ungroup to flatten result set\n |> group()\n // Rename the value column and keep only relevant fields\n |> rename(columns: {_value: \"internet latency\"})\n |> keep(columns: [\"${unique_identifier}\", \"internet latency\"])\n", + "refId": "Internet response time" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "// Get latest local IP per camera in the selected interval\nfrom(bucket: \"${bucket}\")\n // Filter on time\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter using variable selectors (supports All and multi-select)\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n // Filter on a single meaurement to make the schema aligned\n |> filter(fn: (r) => r[\"_measurement\"] == \"cpu\")\n // Group by device to isolate each host\n |> group(columns: [\"${unique_identifier}\"])\n // Get the last report\n |> last()\n // Ungroup to flatten result set\n |> group()\n // Keep only host and device model\n |> keep(columns: [\"${unique_identifier}\", \"product_full_name\"])\n |> rename(columns: {\n product_full_name: \"product full name\"\n })", + "refId": "Get camera model name" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"cpu\")\n |> group(columns: [\"${unique_identifier}\"])\n |> last()\n |> group()\n |> keep(columns: [\"${unique_identifier}\", \"firmware_version\"])\n |> rename(columns: {\n \"${unique_identifier}\": \"host\",\n firmware_version: \"firmware version\"\n })\n", + "refId": "A" + } + ], + "title": "Last Report", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "host", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": { + "external ip": 3, + "firmware version": 2, + "host": 0, + "local ip": 4, + "product full name": 1, + "seen": 5 + }, + "renameByName": { + "external ip": "External IP", + "firmware version": "AXIS OS", + "host": "Device", + "local ip": "Local IPs", + "product full name": "Product Name", + "seen": "Last Seen" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "description": "AXIS OS Changes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 20, + "x": 0, + "y": 10 + }, + "id": 18, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n\n // Generic measurement just to access the tags. To reduce number of points,\n // we also filter to one single core and field.\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"cpu\" and\n r[\"cpu\"] == \"cpu-total\" and\n r[\"_field\"] == \"usage_idle\" and\n\n // Only show the ones that are reporting firmware_version\n exists r[\"firmware_version\"] and\n\n // Applied filters\n (r[\"area\"] =~ /${area:regex}/) and\n (r[\"geography\"] =~ /${geography:regex}/) and\n (r[\"region\"] =~ /${region:regex}/) and\n (r[\"site\"] =~ /${site:regex}/) and\n (r[\"type\"] =~ /${type:regex}/) and\n (r[\"${unique_identifier}\"] =~ /${device:regex}/) and\n (r[\"product_full_name\"] =~ /${model:regex}/)\n )\n\n // Aggregate into time series windows to reduce number of returned data points\n |> aggregateWindow(every: 1h, fn: last, createEmpty: false)\n\n // Keep timestamp, device and firmware_version\n |> keep(columns: [\"_time\", \"${unique_identifier}\", \"firmware_version\"])\n\n // Rename and remap to Grafana-friendly _time, _field, _value,\n // that gets rid of the \"firmware_version \" prefix in the visualization.\n |> map(fn: (r) => ({\n _time: r._time,\n _field: r[\"${unique_identifier}\"],\n _value: r[\"firmware_version\"]\n }))\n\n // Group per device (now in _field)\n |> group(columns: [\"_field\"])\n\n // Sort by time\n |> sort(columns: [\"_time\"])", + "refId": "A" + } + ], + "title": "AXIS OS Changes", + "type": "state-timeline" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "cpu" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "region 2" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "site 2" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "host" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Device detail link", + "url": "/d/${device_details_uid:raw}/device-details?var-device=${__value.text}" + } + ] + }, + { + "id": "custom.width", + "value": 192 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 20, + "x": 0, + "y": 19 + }, + "id": 12, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "frameIndex": 13, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Trend #cpu" + } + ] + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": "from(bucket: \"${bucket}\")\n // Selected time span\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter based on drop-downs\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n // Filter out the fields we are interested in\n |> filter(fn: (r) => r[\"_measurement\"] == \"cpu\")\n |> filter(fn: (r) => r[\"cpu\"] == \"cpu-total\")\n |> filter(fn: (r) => r[\"_field\"] == \"usage_idle\")\n // One time series for each device\n |> group(columns: [\"${unique_identifier}\"])\n // Invert to CPU load instead of idle\n |> map(fn: (r) => ({ r with _value: 100.0 - r._value }))\n // Aggregate in blocks to reduce number of data points\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", + "refId": "cpu" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "from(bucket: \"${bucket}\")\n // Selected time span\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter based on drop-downs\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n // Filter out the fields we are interested in\n |> filter(fn: (r) => r[\"_measurement\"] == \"mem\")\n |> filter(fn: (r) => r[\"_field\"] == \"used_percent\")\n // One time series for each device\n |> group(columns: [\"${unique_identifier}\"])\n // Aggregate in blocks to reduce number of data points\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", + "refId": "ram" + } + ], + "title": "CPU and RAM trends", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "cpu": { + "timeField": "Time" + }, + "ram": { + "timeField": "Time" + } + } + }, + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Trend #cpu": "CPU Usage", + "Trend #ram": "RAM Usage", + "host": "Device" + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 3, + "panels": [], + "repeat": "device", + "title": "${device}", + "type": "row" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red" + }, + { + "color": "dark-green", + "value": 604800 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 0, + "y": 29 + }, + "id": 5, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "groupBy": [ + { + "params": ["$__interval"], + "type": "time" + }, + { + "params": ["null"], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "query": "from(bucket: \"${bucket}\")\n // Filter on time\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter on the right device. Since this visual will be repeated, the\n // device variable will always contain the right value and only one,\n // so we do not need to do regexp here..\n |> filter(fn: (r) => r[\"${unique_identifier}\"] == \"${device}\")\n // Filter on the field we want\n |> filter(fn: (r) => r[\"_measurement\"] == \"system\")\n |> filter(fn: (r) => r[\"_field\"] == \"uptime\")\n // Group by device to merge multiple tagsets\n |> group(columns: [\"${unique_identifier}\"])\n // Get the last sample\n |> last()", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": ["value"], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "title": "Uptime", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "fieldMinMax": false, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "#EAB839", + "value": 80 + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 3, + "y": 29 + }, + "id": 1, + "maxPerRow": 12, + "options": { + "displayMode": "gradient", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": ["last"], + "fields": "", + "limit": 1, + "values": true + }, + "showUnfilled": true, + "sizing": "auto", + "text": {}, + "valueMode": "color" + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "from(bucket: \"${bucket}\")\n // Filter on time\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter on the right device. Since this visual will be repeated,\n // the device variable will always contain the right value and only one,\n // so we do not need to do regexp here..\n |> filter(fn: (r) => r[\"${unique_identifier}\"] == \"${device}\")\n // Filter on the measurement and field we want\n |> filter(fn: (r) => r[\"_measurement\"] == \"cpu\")\n |> filter(fn: (r) => r[\"cpu\"] == \"cpu-total\")\n |> filter(fn: (r) => r[\"_field\"] == \"usage_active\")\n // Group by device to merge multiple tagsets\n |> group(columns: [\"${unique_identifier}\"])\n // Get the last sample\n |> last()\n", + "refId": "A" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"system\")\n |> filter(fn: (r) => r[\"_field\"] == \"n_cpus\")\n |> filter(fn: (r) => r[\"host\"] == \"${device}\")\n|> last()", + "refId": "B" + } + ], + "title": "CPU", + "type": "bargauge" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "fieldMinMax": false, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "yellow" + }, + { + "color": "orange", + "value": 80 + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 6, + "y": 29 + }, + "id": 2, + "maxPerRow": 12, + "options": { + "displayMode": "gradient", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 25, + "minVizHeight": 12, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "limit": 1, + "values": true + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": "from(bucket: \"${bucket}\")\n // Filter on time\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter on the right device. Since this visual will be repeated,\n // the device variable will always contain the right value and only one,\n // so we do not need to do regexp here..\n |> filter(fn: (r) => r[\"${unique_identifier}\"] == \"${device}\")\n // Filter on the measurement and field we want\n |> filter(fn: (r) => r[\"_measurement\"] == \"mem\")\n |> filter(fn: (r) => r[\"_field\"] == \"used_percent\")\n // Group by device to merge multiple tagsets\n |> group(columns: [\"${unique_identifier}\"])\n // Get the last sample\n |> last()\n", + "refId": "A" + } + ], + "title": "RAM", + "type": "bargauge" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "fieldMinMax": false, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "blue" + }, + { + "color": "orange", + "value": 80 + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 9, + "y": 29 + }, + "id": 6, + "options": { + "displayMode": "gradient", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 25, + "minVizHeight": 12, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "limit": 1, + "values": true + }, + "showUnfilled": true, + "sizing": "auto", + "text": { + "titleSize": 1 + }, + "valueMode": "color" + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": "from(bucket: \"${bucket}\")\n // Filter on time\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter on the right device. Since this visual will be repeated,\n // the device variable will always contain the right value and only one,\n // so we do not need to do regexp here..\n |> filter(fn: (r) => r[\"${unique_identifier}\"] == \"${device}\")\n // Filter on the measurement, field, and specific path we want\n |> filter(fn: (r) => r[\"_measurement\"] == \"disk\")\n |> filter(fn: (r) => r[\"_field\"] == \"used_percent\")\n |> filter(fn: (r) => r[\"path\"] == \"/usr/local\")\n // Group by device to merge multiple tagsets\n |> group(columns: [\"${unique_identifier}\"])\n // Get the last sample\n |> last()\n", + "refId": "A" + } + ], + "title": "Storage", + "type": "bargauge" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "fieldMinMax": false, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "blue" + }, + { + "color": "orange", + "value": 80 + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 12, + "y": 29 + }, + "id": 7, + "options": { + "displayMode": "gradient", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 25, + "minVizHeight": 12, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "limit": 1, + "values": true + }, + "showUnfilled": true, + "sizing": "auto", + "text": { + "titleSize": 1 + }, + "valueMode": "color" + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "query": "from(bucket: \"${bucket}\")\n // Filter on time\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n // Filter on the right device. Since this visual will be repeated,\n // the device variable will always contain the right value and only one,\n // so we do not need to do regexp here..\n |> filter(fn: (r) => r[\"${unique_identifier}\"] == \"${device}\")\n // Filter on the measurement, field, and specific path we want\n |> filter(fn: (r) => r[\"_measurement\"] == \"disk\")\n |> filter(fn: (r) => r[\"_field\"] == \"used_percent\")\n |> filter(fn: (r) => r[\"path\"] == \"/var/spool/storage/SD_DISK\")\n // Group by device to merge multiple tagsets\n |> group(columns: [\"${unique_identifier}\"])\n // Get the last sample\n |> last()\n", + "refId": "A" + } + ], + "title": "SD Card", + "type": "bargauge" + } + ], + "preload": false, + "refresh": "15m", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [ + { + "allowCustomValue": true, + "current": { + "text": "InfluxDB", + "value": "InfluxDB" + }, + "description": "", + "hide": 2, + "label": "Data Source", + "name": "datasource", + "options": [], + "query": "influxdb", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "buckets()", + "hide": 2, + "label": "Database", + "name": "bucket", + "options": [], + "query": { + "query": "buckets()" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"${unique_identifier}\")", + "description": "All devices that are known in the schema", + "includeAll": true, + "label": "Device", + "multi": true, + "name": "device", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"${unique_identifier}\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"geography\")", + "includeAll": true, + "label": "Geography", + "multi": true, + "name": "geography", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"geography\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"region\")", + "description": "", + "includeAll": true, + "label": "Region", + "multi": true, + "name": "region", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"region\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"area\")", + "description": "", + "includeAll": true, + "label": "Area", + "multi": true, + "name": "area", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"area\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"site\")", + "description": "", + "includeAll": true, + "label": "Site", + "multi": true, + "name": "site", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"site\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"type\")", + "includeAll": true, + "label": "Type", + "multi": true, + "name": "type", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"type\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "host", + "value": "host" + }, + "description": "The name of the field for the unique identifier like the serial number", + "hide": 2, + "name": "unique_identifier", + "query": "host", + "skipUrlSync": true, + "type": "constant" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"product_full_name\")", + "description": "Model of the camera", + "includeAll": true, + "label": "Model Name", + "multi": true, + "name": "model", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"product_full_name\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "bekds38txl1j4c", + "value": "bekds38txl1j4c" + }, + "description": "UID of the device details dashboard", + "hide": 2, + "name": "device_details_uid", + "query": "bekds38txl1j4c", + "skipUrlSync": true, + "type": "constant" + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Overview of Devices", + "uid": "ceh7yyrnarj7kd", + "version": 29 +} diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/system_overview.json b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/system_overview.json new file mode 100644 index 0000000..4409c68 --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/dashboards/system_overview.json @@ -0,0 +1,1150 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 7, + "links": [], + "panels": [ + { + "datasource": { + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"cpu\")\n |> filter(fn: (r) => r[\"cpu\"] == \"cpu-total\")\n |> keep(columns: [\"host\"])\n |> group()\n |> distinct(column: \"host\")\n |> count(column: \"_value\")\n |> rename(columns: {_value: \"total\"})\n |> yield(name: \"all\")\n\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n |> filter(fn: (r) => r[\"_measurement\"] == \"cpu\")\n |> filter(fn: (r) => r[\"cpu\"] == \"cpu-total\")\n |> keep(columns: [\"host\"])\n |> group()\n |> distinct(column: \"host\")\n |> count(column: \"_value\")\n |> rename(columns: {_value: \"selected\"})\n |> yield(name: \"filtered\")", + "refId": "A" + } + ], + "title": "Device Count", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "description": "Known devices that does not have any data during this time span.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "seen" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "known" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "in_interval" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "seen_earlier" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "dummy" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "host" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Device detail link", + "url": "/d/${device_details_uid:raw}/device-details?var-device=${__value.text}&from=now-90d" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 4, + "y": 0 + }, + "id": 12, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "frameIndex": 1, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Trend #cpu" + } + ] + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "import \"array\"\n\n// Get cameras with reported data in the selected interval\nreal_data = from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"cpu\" and\n r[\"cpu\"] == \"cpu-total\" and\n r[\"_field\"] == \"usage_idle\" and\n r[\"area\"] =~ /${area:regex}/ and\n r[\"geography\"] =~ /${geography:regex}/ and\n r[\"region\"] =~ /${region:regex}/ and\n r[\"site\"] =~ /${site:regex}/ and\n r[\"type\"] =~ /${type:regex}/ and\n r[\"${unique_identifier}\"] =~ /${device:regex}/ and\n r[\"product_full_name\"] =~ /${model:regex}/\n )\n |> group(columns: [\"host\"])\n |> last()\n |> group()\n |> keep(columns: [\"_time\", \"host\"])\n |> rename(columns: {_time: \"seen\"})\n |> map(fn: (r) => ({ r with in_interval: true })) // Add extra field for easier identification\n\n// Dummy fallback row (ensures structure exists even if real data is empty)\nfake_row = array.from(rows: [\n {\n host: \"dummy_device\",\n seen: time(v: 0),\n in_interval: true,\n dummy: 1\n }\n])\n\n// Combine real data with dummy row\nunion(tables: [real_data, fake_row])", + "refId": "in-interval" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "import \"influxdata/influxdb/schema\"\n\n// Step 1: Get all known hosts\nhosts = schema.tagValues(\n bucket: \"${bucket}\",\n tag: \"host\"\n)\n |> rename(columns: {_value: \"host\"})\n |> map(fn: (r) => ({ r with known: true }))\n\n// Step 2: Get real data to apply tag-based filters\nfiltered_hosts = from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"cpu\" and\n r[\"cpu\"] == \"cpu-total\" and\n r[\"_field\"] == \"usage_idle\" and\n (exists r[\"area\"] and r[\"area\"] =~ /${area:regex}/) and\n (exists r[\"geography\"] and r[\"geography\"] =~ /${geography:regex}/) and\n (exists r[\"region\"] and r[\"region\"] =~ /${region:regex}/) and\n (exists r[\"site\"] and r[\"site\"] =~ /${site:regex}/) and\n (exists r[\"type\"] and r[\"type\"] =~ /${type:regex}/) and\n (exists r[\"${unique_identifier}\"] and r[\"${unique_identifier}\"] =~ /${device:regex}/) and\n (exists r[\"product_full_name\"] and r[\"product_full_name\"] =~ /${model:regex}/)\n )\n |> keep(columns: [\"host\"])\n |> group()\n |> distinct(column: \"host\")\n\n// Step 3: Join to retain only filtered hosts from schema list\njoin(\n tables: {all: hosts, filtered: filtered_hosts},\n on: [\"host\"]\n)\n", + "refId": "all_devices" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "// Get last timestamp from a host (check the last 3 months back)\nfrom(bucket: \"${bucket}\")\n |> range(start: -90d)\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"cpu\" and\n r[\"cpu\"] == \"cpu-total\" and\n r[\"_field\"] == \"usage_idle\" and\n r[\"area\"] =~ /${area:regex}/ and\n r[\"geography\"] =~ /${geography:regex}/ and\n r[\"region\"] =~ /${region:regex}/ and\n r[\"site\"] =~ /${site:regex}/ and\n r[\"type\"] =~ /${type:regex}/ and\n r[\"${unique_identifier}\"] =~ /${device:regex}/ and\n r[\"product_full_name\"] =~ /${model:regex}/\n )\n |> group(columns: [\"host\"])\n |> last()\n |> group()\n |> keep(columns: [\"_time\", \"host\"])\n |> rename(columns: {_time: \"last seen\"})\n |> map(fn: (r) => ({ r with seen_earlier: true })) // Add extra field for easier identification", + "refId": "last-seen" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": true, + "query": "// NOTE: This does not work as expected...\nfrom(bucket: \"${bucket}\")\n |> range(start: -90d)\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"cpu\" and\n r[\"cpu\"] == \"cpu-total\" and\n r[\"_field\"] == \"usage_idle\"\n )\n |> group(columns: [\"host\"])\n |> last()\n |> group()\n |> keep(columns: [\"host\", \"area\", \"geography\", \"region\", \"site\", \"type\", \"${unique_identifier}\", \"product_full_name\"])\n |> map(fn: (r) => ({\n host: r.host,\n filter_applies:\n (\n (r[\"area\"] =~ /${area:regex}/) and\n (r[\"geography\"] =~ /${geography:regex}/) and\n (r[\"region\"] =~ /${region:regex}/) and\n (r[\"site\"] =~ /${site:regex}/) and\n (r[\"type\"] =~ /${type:regex}/) and\n (r[\"${unique_identifier}\"] =~ /${device:regex}/) and\n (r[\"product_full_name\"] =~ /${model:regex}/)\n )\n }))\n", + "refId": "filter applies" + } + ], + "title": "Missing Devices", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "host", + "mode": "outer" + } + }, + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "isNull", + "options": {} + }, + "fieldName": "in_interval" + } + ], + "match": "any", + "type": "include" + } + }, + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "equal", + "options": { + "value": "dummy_device" + } + }, + "fieldName": "host" + }, + { + "config": { + "id": "isNull", + "options": {} + }, + "fieldName": "filter_applies" + } + ], + "match": "any", + "type": "exclude" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "host": "Device", + "last seen": "Last Seen" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "description": "New devices that did not exist before the start of the selected time span.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "seen" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "known" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "in_interval" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "seen_earlier" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "_time" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "dummy" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "host" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Device details link", + "url": "/d/${device_details_uid:raw}/device-details?var-device=${__value.text}" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 12, + "y": 0 + }, + "id": 17, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "frameIndex": 1, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Trend #cpu" + } + ] + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "// Get cameras with reported data in the selected interval\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => (\n r[\"_measurement\"] == \"cpu\" and r[\"cpu\"] == \"cpu-total\" and r[\"_field\"] == \"usage_idle\"\n ))\n |> group(columns: [\"host\"])\n |> first()\n |> group()\n |> keep(columns: [\"_time\", \"host\"])\n |> rename(columns: {_time: \"first seen\"})\n |> map(fn: (r) => ({ r with in_interval: true })) // Add extra field for easier identification", + "refId": "in-interval" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "import \"array\"\nimport \"experimental\"\n\n// Make a query the last year before the selected interval starts,\n// that way we can find devices that was now known before the start of this\n// interval.\nbefore_data = from(bucket: \"${bucket}\")\n |> range(start: experimental.subDuration(from: v.timeRangeStart, d: 365d), stop: v.timeRangeStart) // Before the interval starts\n |> filter(fn: (r) => (\n r[\"_measurement\"] == \"cpu\" and r[\"cpu\"] == \"cpu-total\" and r[\"_field\"] == \"usage_idle\"\n ))\n |> group(columns: [\"host\"])\n |> last()\n |> group()\n |> keep(columns: [\"_time\", \"host\"])\n |> map(fn: (r) => ({ r with seen_earlier: true })) // Add extra field for easier identification\n\n// Dummy fallback row to ensure structure\nbefore_dummy =\n array.from(rows: [{\n host: \"dummy_device\",\n _time: time(v: 0),\n seen_earlier: true,\n dummy: 1\n }])\n\n// Combine real before data with fallback\nunion(tables: [before_data, before_dummy])", + "refId": "before-interval" + } + ], + "title": "New Devices", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "host", + "mode": "outer" + } + }, + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "isNull", + "options": {} + }, + "fieldName": "seen_earlier" + } + ], + "match": "any", + "type": "include" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "first seen": "First Seen", + "host": "Device" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "description": "Top k:\n* Highest load1\n* Highest load15\n* Shortest uptime\n* Highest memory used\n* Most flash storage (built in) used\n* Most SD card storage used", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "uptime" + }, + "properties": [ + { + "id": "unit", + "value": "s" + }, + { + "id": "decimals", + "value": 1 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "memory used" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "flash used" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "sd card used" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "host" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Device Detail Link", + "url": "/d/${device_details_uid:raw}/device-details?var-device=${__value.text}" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 20, + "x": 0, + "y": 11 + }, + "id": 13, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "host" + } + ] + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "metric_to_use = \"load1\"\n\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n\n // 1. Apply metadata filters first (only if fields exist)\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n\n // 2. Then filter for the metric of interest\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"system\" and\n r[\"_field\"] == metric_to_use\n )\n\n // 3. Aggregate, format, and limit\n |> group(columns: [\"host\"])\n |> mean()\n |> group()\n |> rename(columns: {_value: metric_to_use})\n |> keep(columns: [\"host\", metric_to_use])\n |> sort(columns: [metric_to_use], desc: true)\n |> limit(n: ${outlier_count})\n", + "refId": "load1" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "metric_to_use = \"load15\"\n\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n\n // 1. Metadata filters (only if fields exist)\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n\n // 2. Metric filtering\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"system\" and\n r[\"_field\"] == metric_to_use\n )\n\n // 3. Aggregation and formatting\n |> group(columns: [\"host\"])\n |> mean()\n |> group()\n |> rename(columns: {_value: metric_to_use})\n |> keep(columns: [\"host\", metric_to_use])\n\n // 4. Sort and limit\n |> sort(columns: [metric_to_use], desc: true)\n |> limit(n: ${outlier_count})\n", + "refId": "load15" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "metric_to_use = \"uptime\"\n\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n\n // 1. Metadata filters (only apply if fields exist)\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n\n // 2. Metric filter for uptime\n |> filter(fn: (r) => r[\"_measurement\"] == \"system\" and r[\"_field\"] == metric_to_use)\n\n // 3. Aggregate and format\n |> group(columns: [\"host\"])\n |> last() // Last reported uptime during the selected time span\n |> group()\n |> rename(columns: {_value: metric_to_use})\n |> keep(columns: [\"host\", metric_to_use])\n |> sort(columns: [metric_to_use], desc: false) // Ascending = lowest uptime\n |> limit(n: ${outlier_count})\n", + "refId": "uptime" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n\n // 1. Metadata filters (safe if tag missing)\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n\n // 2. Metric filter\n |> filter(fn: (r) => r[\"_measurement\"] == \"mem\" and r[\"_field\"] == \"used_percent\")\n\n // 3. Aggregation and formatting\n |> group(columns: [\"host\"])\n |> mean()\n |> group()\n |> rename(columns: {_value: \"memory used\"})\n |> keep(columns: [\"host\", \"memory used\"])\n\n // 4. Sort by memory used (descending = highest usage)\n |> sort(columns: [\"memory used\"], desc: true)\n |> limit(n: ${outlier_count})\n", + "refId": "memory" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "metric_name = \"flash used\"\ndisk_path = \"/usr/local\"\n\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n\n // 1. Metadata filters\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n\n // 2. Metric and path filter\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"disk\" and\n r[\"_field\"] == \"used_percent\" and\n r[\"path\"] == disk_path\n )\n\n // 3. Aggregate and format\n |> group(columns: [\"host\"])\n |> mean() // Mean over the selected time span\n |> group()\n |> rename(columns: {_value: metric_name})\n |> keep(columns: [\"host\", metric_name])\n\n // 4. Sort and limit\n |> sort(columns: [metric_name], desc: true)\n |> limit(n: ${outlier_count})\n", + "refId": "flash" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "hide": false, + "query": "metric_name = \"sd card used\"\ndisk_path = \"/var/spool/storage/SD_DISK\"\n\nfrom(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n\n // 1. Metadata filters\n |> filter(fn: (r) => r[\"area\"] =~ /${area:regex}/)\n |> filter(fn: (r) => r[\"geography\"] =~ /${geography:regex}/)\n |> filter(fn: (r) => r[\"region\"] =~ /${region:regex}/)\n |> filter(fn: (r) => r[\"site\"] =~ /${site:regex}/)\n |> filter(fn: (r) => r[\"type\"] =~ /${type:regex}/)\n |> filter(fn: (r) => r[\"${unique_identifier}\"] =~ /${device:regex}/)\n |> filter(fn: (r) => r[\"product_full_name\"] =~ /${model:regex}/)\n\n // 2. Metric and path filter\n |> filter(fn: (r) =>\n r[\"_measurement\"] == \"disk\" and\n r[\"_field\"] == \"used_percent\" and\n r[\"path\"] == disk_path\n )\n\n // 3. Aggregate and format\n |> group(columns: [\"host\"])\n |> mean() // Mean over the selected time span\n |> group()\n |> rename(columns: {_value: metric_name})\n |> keep(columns: [\"host\", metric_name])\n\n // 4. Sort and limit\n |> sort(columns: [metric_name], desc: true)\n |> limit(n: ${outlier_count})\n", + "refId": "SD card" + } + ], + "title": "Outliers", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "host", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "flash used": "Flash Used", + "host": "Device", + "load1": "Load 1 minute", + "load15": "Load 15 minutes", + "memory used": "RAM Used", + "sd card used": "SD Card Used", + "uptime": "Uptime" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 20, + "x": 0, + "y": 19 + }, + "id": 18, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xField": "type", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "query": "cpu = from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) =>\n r._measurement == \"cpu\" and\n r._field == \"usage_idle\" and\n r.cpu == \"cpu-total\" and\n exists r.type and\n r.type =~ /${type:regex}/ and\n r[\"area\"] =~ /${area:regex}/ and\n r[\"geography\"] =~ /${geography:regex}/ and\n r[\"region\"] =~ /${region:regex}/ and\n r[\"site\"] =~ /${site:regex}/ and\n r[\"product_full_name\"] =~ /${model:regex}/ and\n r[\"${unique_identifier}\"] =~ /${device:regex}/\n )\n |> map(fn: (r) => ({ r with _value: 100.0 - r._value }))\n |> group(columns: [\"type\"])\n |> mean()\n |> map(fn: (r) => ({ r with metric: \"CPU Usage\" }))\n\nram = from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) =>\n r._measurement == \"mem\" and\n r._field == \"used_percent\" and\n exists r.type and\n r.type =~ /${type:regex}/ and\n r[\"area\"] =~ /${area:regex}/ and\n r[\"geography\"] =~ /${geography:regex}/ and\n r[\"region\"] =~ /${region:regex}/ and\n r[\"site\"] =~ /${site:regex}/ and\n r[\"product_full_name\"] =~ /${model:regex}/ and\n r[\"${unique_identifier}\"] =~ /${device:regex}/\n )\n |> group(columns: [\"type\"])\n |> mean()\n |> map(fn: (r) => ({ r with metric: \"RAM Usage\" }))\n\ndisk_usr_local = from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) =>\n r._measurement == \"disk\" and\n r._field == \"used_percent\" and\n r.path == \"/usr/local\" and\n exists r.type and\n r.type =~ /${type:regex}/ and\n r[\"area\"] =~ /${area:regex}/ and\n r[\"geography\"] =~ /${geography:regex}/ and\n r[\"region\"] =~ /${region:regex}/ and\n r[\"site\"] =~ /${site:regex}/ and\n r[\"product_full_name\"] =~ /${model:regex}/ and\n r[\"${unique_identifier}\"] =~ /${device:regex}/\n )\n |> group(columns: [\"type\"])\n |> mean()\n |> map(fn: (r) => ({ r with metric: \"Disk /usr/local\" }))\n\ndisk_sd = from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) =>\n r._measurement == \"disk\" and\n r._field == \"used_percent\" and\n r.path == \"/var/volatile/spool/storage/SD_DISK\" and\n exists r.type and\n r.type =~ /${type:regex}/ and\n r[\"area\"] =~ /${area:regex}/ and\n r[\"geography\"] =~ /${geography:regex}/ and\n r[\"region\"] =~ /${region:regex}/ and\n r[\"site\"] =~ /${site:regex}/ and\n r[\"product_full_name\"] =~ /${model:regex}/ and\n r[\"${unique_identifier}\"] =~ /${device:regex}/\n )\n |> group(columns: [\"type\"])\n |> mean()\n |> map(fn: (r) => ({ r with metric: \"Disk /SD_DISK\" }))\n\nunion(tables: [cpu, ram, disk_usr_local, disk_sd])\n |> group()\n |> pivot(rowKey: [\"type\"], columnKey: [\"metric\"], valueColumn: \"_value\")\n", + "refId": "A" + } + ], + "title": "Average CPU / RAM / Disk Usage per Type", + "type": "barchart" + } + ], + "preload": false, + "refresh": "15m", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [ + { + "allowCustomValue": false, + "current": { + "text": "InfluxDB", + "value": "InfluxDB" + }, + "description": "", + "hide": 2, + "label": "Data Source", + "name": "datasource", + "options": [], + "query": "influxdb", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "allowCustomValue": false, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "buckets()", + "hide": 2, + "label": "Database", + "name": "bucket", + "options": [], + "query": { + "query": "buckets()" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"geography\")", + "includeAll": true, + "label": "Geography", + "multi": true, + "name": "geography", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"geography\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"region\")", + "description": "", + "includeAll": true, + "label": "Region", + "multi": true, + "name": "region", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"region\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"area\")", + "description": "", + "includeAll": true, + "label": "Area", + "multi": true, + "name": "area", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"area\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"site\")", + "description": "", + "includeAll": true, + "label": "Site", + "multi": true, + "name": "site", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"site\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"type\")", + "includeAll": true, + "label": "Type", + "multi": true, + "name": "type", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"type\")" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"${unique_identifier}\")", + "description": "", + "includeAll": true, + "label": "Device", + "multi": true, + "name": "device", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"${unique_identifier}\")" + }, + "refresh": 1, + "regex": "", + "sort": 1, + "type": "query" + }, + { + "current": { + "text": "host", + "value": "host" + }, + "description": "The name of the field for the unique identifier like the serial number", + "hide": 2, + "name": "unique_identifier", + "query": "host", + "skipUrlSync": true, + "type": "constant" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": "All", + "value": ["$__all"] + }, + "datasource": { + "type": "influxdb", + "uid": "${datasource}" + }, + "definition": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"product_full_name\")", + "includeAll": true, + "label": "Model Name", + "multi": true, + "name": "model", + "options": [], + "query": { + "query": "import \"influxdata/influxdb/schema\"\n\nschema.tagValues(bucket: \"${bucket}\", tag: \"product_full_name\")" + }, + "refresh": 1, + "regex": "", + "sort": 1, + "type": "query" + }, + { + "current": { + "text": "5", + "value": "5" + }, + "label": "K-Outliers", + "name": "outlier_count", + "options": [ + { + "selected": false, + "text": "1", + "value": "1" + }, + { + "selected": false, + "text": "2", + "value": "2" + }, + { + "selected": false, + "text": "3", + "value": "3" + }, + { + "selected": false, + "text": "4", + "value": "4" + }, + { + "selected": true, + "text": "5", + "value": "5" + }, + { + "selected": false, + "text": "10", + "value": "10" + }, + { + "selected": false, + "text": "50", + "value": "50" + } + ], + "query": "1,2,3,4,5,10,50", + "type": "custom" + }, + { + "current": { + "text": "bekds38txl1j4c", + "value": "bekds38txl1j4c" + }, + "description": "UID of the device details dashboard", + "hide": 2, + "name": "device_details_uid", + "query": "bekds38txl1j4c", + "skipUrlSync": true, + "type": "constant" + } + ] + }, + "time": { + "from": "now-7d", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "System Overview", + "uid": "dsdsadfg3t3t3", + "version": 27 +} diff --git a/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/datasources/datasource.yaml b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/datasources/datasource.yaml new file mode 100644 index 0000000..0b7c79b --- /dev/null +++ b/dashboard-deployments/system-monitoring-influxdb2-flux-grafana/provisioning/datasources/datasource.yaml @@ -0,0 +1,15 @@ +apiVersion: 1 + +datasources: + - name: InfluxDB + type: influxdb + access: proxy + url: http://influxdb:8086 + jsonData: + version: Flux + organization: ${INFLUXDB_ORG} + defaultBucket: ${INFLUXDB_BUCKET} + timeInterval: "15s" + secureJsonData: + token: ${INFLUXDB_TOKEN} + isDefault: true