From e35fcc46d454b868bf3e709b4d34e018f7df4b0e Mon Sep 17 00:00:00 2001 From: NejcKle Date: Mon, 14 Jul 2025 11:17:38 +0200 Subject: [PATCH 01/28] feat: add system diagram --- .../ajdovscina-architecture.drawio | 317 ++++++++++++++++++ docs/architecture/ajdovscina-architecture.png | Bin 0 -> 266561 bytes 2 files changed, 317 insertions(+) create mode 100644 docs/architecture/ajdovscina-architecture.drawio create mode 100644 docs/architecture/ajdovscina-architecture.png diff --git a/docs/architecture/ajdovscina-architecture.drawio b/docs/architecture/ajdovscina-architecture.drawio new file mode 100644 index 0000000..bee8f72 --- /dev/null +++ b/docs/architecture/ajdovscina-architecture.drawio @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/architecture/ajdovscina-architecture.png b/docs/architecture/ajdovscina-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..45c7e5fff0a18d5e0d4267324f4c096c49aff480 GIT binary patch literal 266561 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

$2?^WAp8*hZz@cx` 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ё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 W*6MB=d#Wzp$P!CsqOFp literal 0 HcmV?d00001 From 573ae231c96f620b15b1fd77ac13d93a04810e5c Mon Sep 17 00:00:00 2001 From: NejcKle Date: Mon, 14 Jul 2025 14:57:25 +0200 Subject: [PATCH 02/28] feat: add docker compose with system blocks --- Makefile | 9 ++ alertmanager/README.md | 52 +++++++++ alertmanager/alertmanager.yml | 38 +++++++ docker-compose-dev.yml | 205 ++++++++++++++++++++++++++++++++++ init/postgres.sql | 114 +++++++++++++++++++ prometheus/prometheus-dev.yml | 39 +++++++ 6 files changed, 457 insertions(+) create mode 100644 Makefile create mode 100644 alertmanager/README.md create mode 100644 alertmanager/alertmanager.yml create mode 100644 docker-compose-dev.yml create mode 100644 init/postgres.sql create mode 100644 prometheus/prometheus-dev.yml diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f4d9c50 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +# these will speed up builds, for docker-compose >= 1.25 +export COMPOSE_DOCKER_CLI_BUILD=1 +export DOCKER_BUILDKIT=1 + +dev-up: + docker compose -f docker-compose-dev.yml -p ajdovscina-dev up --build -d + +dev-down: + docker compose -f docker-compose-dev.yml -p ajdovscina-dev down --remove-orphans diff --git a/alertmanager/README.md b/alertmanager/README.md new file mode 100644 index 0000000..bafb098 --- /dev/null +++ b/alertmanager/README.md @@ -0,0 +1,52 @@ +# Alertmanager + +The `Alertmanager` is +[part of Prometheus](https://prometheus.io/docs/alerting/latest/alertmanager/), used to handle +alerts sent by Prometheus server or other clients. + +In our case, the `Alertmanager` is configured to send alerts to a dedicated `Slack` channel. It can +be configured to send alerts to multiple `Slack` channels or other endpoints. + +## Slack integration + +A handy guide on `Slack` integration provided by Grafana is available +[on this link](https://grafana.com/blog/2020/02/25/step-by-step-guide-to-setting-up-prometheus-alertmanager-with-slack-pagerduty-and-gmail/). + +Configuration of `Slack webhooks` is described in the official `Slack` +[documentation](https://api.slack.com/messaging/webhooks). + +The rules we use to trigger alerts are defined in the `alertmanager.yml` file. + +Prometheus also needs to be configured properly, the following configuration in the `prometheus.yml` +file is required: + +```yaml +alerting: + # Alert manager instance + alertmanagers: + - static_configs: + - targets: + - alert-manager:9093 +``` + +Furthermore, the `rules.yml` file contains the rules that trigger alerts: + +```yaml +# Prometheus rules for alerting +# Alerts trigger when the conditions defined in the rules are met + +groups: + - name: InstanceDown + rules: + - alert: InstanceDown + expr: up == 0 + for: 1m + # Annotation - additional informational labels to store more information + annotations: + title: "Instance {{ $labels.instance }} down" + description: + "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." + # Labels - additional labels to be attached to the alert + labels: + severity: "critical" +``` diff --git a/alertmanager/alertmanager.yml b/alertmanager/alertmanager.yml new file mode 100644 index 0000000..ff75f6c --- /dev/null +++ b/alertmanager/alertmanager.yml @@ -0,0 +1,38 @@ +# Alertmanager configuration +# Defines the notification routes and the external systems they communicate with + +global: + resolve_timeout: 1m + slack_api_url: 'https://hooks.slack.com/services/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' # TODO: Hide this in a secret + +route: + receiver: 'slack-notifications' + +receivers: +- name: 'slack-notifications' + slack_configs: + - channel: '#telespor-alerts' + send_resolved: true + icon_url: https://avatars3.githubusercontent.com/u/3380462 + title: |- + [{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }} + {{- if gt (len .CommonLabels) (len .GroupLabels) -}} + {{" "}}( + {{- with .CommonLabels.Remove .GroupLabels.Names }} + {{- range $index, $label := .SortedPairs -}} + {{ if $index }}, {{ end }} + {{- $label.Name }}="{{ $label.Value -}}" + {{- end }} + {{- end -}} + ) + {{- end }} + text: >- + {{ range .Alerts -}} + *Alert:* {{ .Annotations.title }}{{ if .Labels.severity }} - `{{ .Labels.severity }}`{{ end }} + + *Description:* {{ .Annotations.description }} + + *Details:* + {{ range .Labels.SortedPairs }} β€’ *{{ .Name }}:* `{{ .Value }}` + {{ end }} + {{ end }} diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000..4bb7a03 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,205 @@ +services: + + # ingest-service: + # container_name: ajdovscina-ingest-service + # build: + # context: . + # dockerfile: ./dockerfiles/Dockerfile.updated-udp-receiver + # # flask requires SIGINT to stop gracefully + # # (default stop signal from Compose is SIGTERM) + # environment: + # - ENV=development + # - LOCALHOST=false + # - PYTHONUNBUFFERED=1 + # logging: + # driver: "local" + # options: + # max-size: "50M" + # max-file: "3" + # stop_signal: SIGINT + # labels: + # - "autoheal=true" + # healthcheck: + # test: curl --fail http://localhost:5000/health-check || exit 1 + # interval: 60s + # timeout: 30s + # retries: 3 + # start_period: 30s + # depends_on: + # kafka: + # condition: service_healthy + # telespor-cloud-mysqldb: + # condition: service_healthy + # ports: + # - 40005:5000 + # expose: + # - 40005 + + geospatial-data: + container_name: ajdovscina-geospatial-data + image: timescale/timescaledb-ha:pg17-all-amd64 + restart: always + environment: + POSTGRES_DB: "ajdovscina" + POSTGRES_USER: "ajdovscina-geospatial-user" + POSTGRES_PASSWORD: "password" + ports: + - "13328:5432" + volumes: + - ajdovscina-geospatial-db:/var/lib/postgresql/data:rw + - ./init/postgres.sql:/docker-entrypoint-initdb.d/1.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ajdovscina-geospatial-user -d ajdovscina"] + timeout: 2s + retries: 2 + + geospatial-data-exporter: + container_name: ajdovscina-geospatial-exporter + image: quay.io/prometheuscommunity/postgres-exporter:v0.17.0 + environment: + - DATA_SOURCE_NAME=postgresql://ajdovscina-geospatial-user:password@geospatial-data:5432/ajdovscina?sslmode=disable + ports: + - 9204:9187 + links: + - geospatial-data + depends_on: + - geospatial-data + + grafana: + container_name: ajdovscina-grafana + image: grafana/grafana-enterprise:10.2.2 + restart: always + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=password + - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/dashboard.json + ports: + - "13201:3000" + volumes: + - grafana-volume:/var/lib/grafana + + prometheus: + container_name: ajdovscina-prometheus + image: prom/prometheus:v3.1.0 + restart: always + volumes: + - ./prometheus:/etc/prometheus + - prometheus-volume:/prometheus + command: + - "--config.file=/etc/prometheus/prometheus-dev.yml" + - "--log.level=debug" + ports: + - 19290:9090 + + node-exporter: + container_name: ajdovscinanode-exporter + image: prom/node-exporter:v1.8.2 + restart: always + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.rootfs=/rootfs' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + ports: + - "19200:9100" + + autoheal: + container_name: ajdovscina-autoheal + image: willfarrell/autoheal:latest + tty: true + restart: always + environment: + - AUTOHEAL_INTERVAL=60 + - AUTOHEAL_START_PERIOD=120 + - AUTOHEAL_DEFAULT_STOP_TIMEOUT=10 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + alert-manager: + image: prom/alertmanager:v0.28.0 + volumes: + - "./alertmanager:/config" # Mount local alertmanager config to container + - alertmanager:/data + ports: + - 19193:9093 + command: + - "--config.file=/config/alertmanager.yml" # Start alertmanager with config + - "--log.level=debug" + + zookeeper: + image: confluentinc/cp-zookeeper:7.8.0 + ports: + - "2281:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + healthcheck: + test: nc -z localhost 2181 || exit -1 + interval: 10s + timeout: 5s + retries: 3 + labels: + - "autoheal=true" + volumes: + - zookeeper-data:/var/lib/zookeeper/data + - zookeeper-log:/var/lib/zookeeper/log + + kafka: + image: confluentinc/cp-kafka:7.8.0 + container_name: ajdovscina-kafka + environment: + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9993,OUTSIDE://localhost:9992 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_LISTENERS: INSIDE://0.0.0.0:9993,OUTSIDE://0.0.0.0:9992 + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + ports: + - "9992:9992" + expose: + - "9993" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - kafka-data:/var/lib/kafka/data + depends_on: + - zookeeper + healthcheck: + test: kafka-topics --bootstrap-server localhost:9992 --list + interval: 5s + timeout: 10s + retries: 5 + labels: + - "autoheal=true" + + kafka-exporter: + image: danielqsj/kafka-exporter:v1.8.0 + command: ["--kafka.server=kafka:9993"] + depends_on: + kafka: + condition: service_healthy + ports: + - 9328:9308 + + postgres-exporter: + image: quay.io/prometheuscommunity/postgres-exporter:v0.16.0 + container_name: ajdovscina-postgres-exporter + environment: + - DATA_SOURCE_NAME=postgresql://username:password@172.17.0.1:5432/postgres?sslmode=disable + ports: + - 9287:9187 + + +volumes: + grafana-volume: + + prometheus-volume: + alertmanager: + + ajdovscina-geospatial-db: + + kafka-data: + zookeeper-data: + zookeeper-log: diff --git a/init/postgres.sql b/init/postgres.sql new file mode 100644 index 0000000..64445ab --- /dev/null +++ b/init/postgres.sql @@ -0,0 +1,114 @@ +CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE; +CREATE EXTENSION IF NOT EXISTS postgis; + +CREATE TABLE IF NOT EXISTS sensors ( + id SERIAL PRIMARY KEY, + sensor_uid UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(), -- renamed to avoid confusion with PK + sensor_name VARCHAR(255) NOT NULL, -- Added for better identification + sensor_type VARCHAR(50) NOT NULL, -- e.g. 'fire', 'water' + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- This is generated programmatically first time sensor data is received + +-- CREATE TABLE IF NOT EXISTS water_level_data ( +-- id SERIAL PRIMARY KEY, +-- sensor_id INTEGER NOT NULL, +-- water_level DOUBLE PRECISION NOT NULL +-- ); + +-- CREATE TABLE IF NOT EXISTS fire_sensor_data ( +-- id SERIAL PRIMARY KEY , +-- sensor_id INTEGER NOT NULL, +-- temperature FLOAT NOT NULL, +-- smoke_level FLOAT NOT NULL +-- ); + +CREATE TABLE IF NOT EXISTS applications ( + id SERIAL PRIMARY KEY, + description TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS locations ( + id SERIAL PRIMARY KEY, + description TEXT NOT NULL, + name TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS metadata ( + id SERIAL, + key TEXT NOT NULL, + value TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + sensor_id INTEGER, + application_id INTEGER, + location_id INTEGER +); + + +-- Create hypertables + +CREATE TABLE IF NOT EXISTS positions ( + id INT NOT NULL, + sensor_id INTEGER NOT NULL, + latitude DOUBLE PRECISION NOT NULL, + longitude DOUBLE PRECISION NOT NULL, + altitude FLOAT, + position_time TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + geom GEOGRAPHY(Point, 4326), -- Geospatial column for geographic operations + PRIMARY KEY (id, sensor_id, position_time) +); + +CREATE TABLE IF NOT EXISTS readings ( + id INT NOT NULL, + sensor_id INTEGER NOT NULL, + device_type VARCHAR(50) NOT NULL, + timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, sensor_id, device_type, timestamp) +); + +-- 2.1. Create a geospatial index to improve spatial query performance +CREATE INDEX IF NOT EXISTS idx_positions_geom ON positions USING GIST (geom); +-- 2.2. Add unique constraint on deviceId and date to support ON CONFLICT operations +-- ALTER TABLE positions DROP CONSTRAINT IF EXISTS unique_device_date; +-- ALTER TABLE positions ADD CONSTRAINT unique_device_date UNIQUE (sensor_id, position_time); + +SELECT create_hypertable('positions', 'position_time', 'sensor_id', if_not_exists => TRUE, number_partitions => 10); +SELECT create_hypertable('readings', 'timestamp', 'sensor_id', if_not_exists => TRUE, number_partitions => 10); + +-- 3.2. Create additional indexes to optimize queries +CREATE INDEX IF NOT EXISTS idx_positions_id ON positions (id); +CREATE INDEX IF NOT EXISTS idx_readings_timestamp ON readings (timestamp); + +-- ----------------------------------------------------------------------------------- +-- 4. Establishing Referential Integrity +-- ----------------------------------------------------------------------------------- + +-- 4.5. Add a foreign key from `metadata.sensor_id` to `sensors.id` +ALTER TABLE metadata +ADD CONSTRAINT fk_metadata_sensors +FOREIGN KEY (sensor_id) +REFERENCES sensors(id) +ON DELETE CASCADE; + +-- 4.6. Add a foreign key from `metadata.application_id` to `application.id` +ALTER TABLE metadata +ADD CONSTRAINT fk_metadata_application +FOREIGN KEY (application_id) +REFERENCES applications(id) +ON DELETE CASCADE; + +-- 4.7. Add a foreign key from `metadata.location_id` to `location.id` +ALTER TABLE metadata +ADD CONSTRAINT fk_metadata_location +FOREIGN KEY (location_id) +REFERENCES locations(id) +ON DELETE CASCADE; + +-- Warning: This affects all transactions on the PostgreSQL instance. +-- Required for high-volume FK operations. Requires PostgreSQL restart. +ALTER SYSTEM SET max_locks_per_transaction = 512; diff --git a/prometheus/prometheus-dev.yml b/prometheus/prometheus-dev.yml new file mode 100644 index 0000000..ffd18e8 --- /dev/null +++ b/prometheus/prometheus-dev.yml @@ -0,0 +1,39 @@ +global: + scrape_interval: 15s + scrape_timeout: 10s + evaluation_interval: 30s + +# Specify rule file +rule_files: + - rules.yml + +alerting: +# Alert manager instance + alertmanagers: + - static_configs: + - targets: + - alert-manager:9093 + +scrape_configs: + +# Monitor the node-exporter +- job_name: node-exporter + static_configs: + - targets: + - telespor-cloud-node-exporter:9100 + +# Monitor mysqldb metrics +- job_name: geospatial-exporter + # Override the global default and scrape targets from this job every 5 seconds. + scrape_interval: 5s + + static_configs: + - targets: + - geospatial-db-exporter:9187 + +- job_name: kafka-exporter + scrape_interval: 5s + + static_configs: + - targets: + - kafka-exporter:9308 From 64e46a6a6c40eb599bdbb546dccaab52df13014a Mon Sep 17 00:00:00 2001 From: NejcKle Date: Tue, 15 Jul 2025 09:01:58 +0200 Subject: [PATCH 03/28] feat: add NodeRED to docker compose --- docker-compose-dev.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 4bb7a03..db1afd4 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -35,6 +35,15 @@ services: # expose: # - 40005 + nodered: + image: nodered/node-red:3.1.15-minimal + container_name: nodered + ports: + - "11880:1880" # Exposes Node-RED UI on http://localhost:1880 + volumes: + - nodered-data:/data # Persist flows and configuration + restart: unless-stopped + geospatial-data: container_name: ajdovscina-geospatial-data image: timescale/timescaledb-ha:pg17-all-amd64 @@ -200,6 +209,8 @@ volumes: ajdovscina-geospatial-db: + nodered-data: + kafka-data: zookeeper-data: zookeeper-log: From 76606c6cf5a74aaaae00390018e0501602470114 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Thu, 17 Jul 2025 15:39:53 +0200 Subject: [PATCH 04/28] feat: add ingest service --- ingest_service/Makefile | 21 +++ ingest_service/main.py | 226 ++++++++++++++++++++++++++++++++ ingest_service/mock_send_ttn.py | 92 +++++++++++++ ingest_service/requirements.txt | 4 + ingest_service/ttn_example.json | 89 +++++++++++++ 5 files changed, 432 insertions(+) create mode 100644 ingest_service/Makefile create mode 100644 ingest_service/main.py create mode 100644 ingest_service/mock_send_ttn.py create mode 100644 ingest_service/requirements.txt create mode 100644 ingest_service/ttn_example.json diff --git a/ingest_service/Makefile b/ingest_service/Makefile new file mode 100644 index 0000000..d124a11 --- /dev/null +++ b/ingest_service/Makefile @@ -0,0 +1,21 @@ +SHELL := /bin/bash + +clean: + rm -r -f ./common + +install: + cp -r ../common/ ./ + pip install -r requirements.txt + +run: clean install + bash -c "trap 'echo \"Cleaning up...\"; make clean' EXIT; uvicorn main:app --host 0.0.0.0 --port 50005" + +run-local-dev: + python3.10 -m venv ../venv + source ../venv/bin/activate; \ + export ENV=development && export LOCALHOST=true && make run + +run-dev: + python3.10 -m venv ../venv + source ../venv/bin/activate; \ + export ENV=development && export LOCALHOST=false && make run diff --git a/ingest_service/main.py b/ingest_service/main.py new file mode 100644 index 0000000..c7ed262 --- /dev/null +++ b/ingest_service/main.py @@ -0,0 +1,226 @@ +import json +import traceback +from datetime import date, datetime +from typing import Optional + +from fastapi import FastAPI, Header, HTTPException, Request, Response, status +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from kafka import KafkaProducer +from pydantic import BaseModel, Field + + +def custom_value_serializer(value): + """Custom serializer to handle datetime objects.""" + + def default_serializer(obj): + if isinstance(obj, (date, datetime)): + return obj.isoformat() + raise TypeError(f"Type {type(obj)} not serializable") + + return json.dumps(value, default=default_serializer).encode("utf-8") + + +producer = KafkaProducer( + bootstrap_servers="localhost:9992", + key_serializer=str.encode, + value_serializer=custom_value_serializer, +) + +app = FastAPI() + + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """Custom exception handler for request validation errors.""" + print(f"Validation error on request: {request.url}") + print("Full validation error details:") + print(exc) + print("Errors:") + for err in exc.errors(): + print(err) + print("Traceback:") + traceback.print_exc() + + return JSONResponse( + status_code=422, content={"detail": exc.errors(), "body": exc.body} + ) + + +API_KEY = "your-secret-api-key" # Replace with your actual key + + +class Position(BaseModel): + """Data model for position information.""" + + latitude: float + longitude: float + altitude: Optional[float] = None + timestamp: Optional[int] = None + source: Optional[str] = None + + +class DataIn(BaseModel): + """Data model for incoming data.""" + + measurement: Optional[dict] = None + position: Optional[Position] = None + + +def add_metadata(data: DataIn, received_from: str): + """The data is routed based on the application name. The application name is added in this service, as it is not part of the data. + + Application data is added based on the endpoint the data is received from. + """ + if received_from: + data.metadata.application = received_from + + +"""Example request which works with route: +curl -X 'POST' http://localhost:50005 -H "Content-Type: application/json" -H "Authorization: Bearer your-secret-api-key" -d '{"measurement": {"water-level": 123, "height": 535, "note": "my-note-is-nice"}, "metadata": {"application": "myApp"}, "position": {"longitude": 14.1451314, "latitude": 97.838824, "altitude": 123.0, "timestamp": 123}}' +""" + + +@app.post("/") +async def ingest_data(data, request: Request, Authorization: str = Header(...)): + """Ingest data from any source. This is a generic endpoint that can be used to ingest data from various sources.""" + print(request) + print(await request.json()) + print(Authorization) + if not Authorization.startswith("Bearer "): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication scheme", + ) + token = Authorization.removeprefix("Bearer ").strip() + if token != API_KEY: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key" + ) + + # Print the data: + print(f"Data received: {data}") + # Process data here (e.g., store in DB) + return Response(status_code=200) + + +class ApplicationIds(BaseModel): + """Data model for TTN application IDs.""" + + application_id: str + + +class DeviceInfoTTN(BaseModel): + """Data model for TTN device information.""" + + device_id: str + application_ids: ApplicationIds + + +class LocationTTN(BaseModel): + """Data model for TTN location information.""" + + frm_payload: Optional[Position] = Field(alias="frm-payload") + + class Config: + """Configuration for Pydantic model.""" + + validate_by_name = True # Allows using `frm_payload` when creating the object + + +class UplinkMessageTTN(BaseModel): + """Data model for TTN uplink message.""" + + f_port: int + decoded_payload: dict + locations: Optional[LocationTTN] = None + + +class DataTTN(BaseModel): + """Data model for TTN webhook data.""" + + end_device_ids: DeviceInfoTTN + received_at: datetime + uplink_message: UplinkMessageTTN + + +"""The TTN message usually contains the following structure: + +{ + "end_device_ids": { + "device_id": "sp100089", + "application_ids": { + "application_id": "irnas-ranger-test" + }, + "dev_eui": "0016C001F0118501", + "join_eui": "0000000000000000", + "dev_addr": "260BB7AD" + }, + "received_at": "2025-07-16T11:09:19.243513998Z", + "uplink_message": { + "session_key_id": "AZT1Hy95A+QB1gq8y/xU3w==", + "f_port": 14, + "f_cnt": 1256661, + "frm_payload": "lAVCf1YBAA==", + "decoded_payload": { + "n_msg": 87679, + "percentage": 66 + }, + "locations": { + "frm-payload": { + "latitude": 46.5581818, + "longitude": 15.6164958, + "altitude": 335, + "source": "SOURCE_GPS" + } + } + } +} + +We are interested in the fields defined above. +""" + + +@app.post("/ttn") +async def ingest_data_ttn(data: DataTTN, Authorization: str = Header(...)): + """Ingest data from TTN webhook.""" + if not Authorization.startswith("Bearer "): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication scheme", + ) + token = Authorization.removeprefix("Bearer ").strip() + if token != API_KEY: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key" + ) + + data_to_insert = format_ttn_data_for_processing(data) + + producer.send("process_data", key="ttn_service", value=data_to_insert) + + # Process data here (e.g., store in DB) + return Response(status_code=200) + + +def format_ttn_data_for_processing(data: DataTTN): + """We format the data for further processing and storage.""" + # Extract version from decoded_payload if it exists + version = data.uplink_message.decoded_payload.get( + "version", "v0" + ) # Set default to "v0" if not present + + # Remove version from decoded_payload if it exists + if "version" in data.uplink_message.decoded_payload: + del data.uplink_message.decoded_payload["version"] + + out_data = { + "sensor_name": data.end_device_ids.device_id, + "application": data.end_device_ids.application_ids.application_id, + "measurement": data.uplink_message.decoded_payload, + "version": version, + "location": data.uplink_message.locations.frm_payload.model_dump(), + "received_at": data.received_at, + } + + return out_data diff --git a/ingest_service/mock_send_ttn.py b/ingest_service/mock_send_ttn.py new file mode 100644 index 0000000..c084bd8 --- /dev/null +++ b/ingest_service/mock_send_ttn.py @@ -0,0 +1,92 @@ +import json +import random +import time +from datetime import datetime + +import requests + +url = "http://localhost:50005/ttn" +token = "your-secret-api-key" + +headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + +def generate_random_decoded_payload(): + """Generate random decoded payload dict with 0 to 15 values. The keys can be random and the values as well.""" + num_values = random.randint(0, 15) + decoded_payload = {} + + possible_columns = [ + "temperature", + "humidity", + "pressure", + "light", + "motion", + "battery", + "voltage", + "current", + "distance", + "speed", + "acceleration", + "gyroscope", + "magnetometer", + "gps_latitude", + "gps_longitude", + "gps_altitude", + "gps_accuracy", + "gps_speed", + "gps_heading", + "gps_satellites", + "gps_timestamp", + "note", + ] + + for _ in range(num_values): + key = "".join( + random.choice(possible_columns) + ) # Randomly select a key from the list + value = random.randint(-100, 100) # Random integer value + decoded_payload[key] = value + + # Add also a random version from v0 to v10 + version = random.randint(0, 10) + decoded_payload["version"] = f"v{version}" + + return decoded_payload + + +device_ids = [f"sp0000{i:03d}" for i in range(1, 101)] # sp00001 to sp00010 +application_ids = [f"irnas-ranger-test{i}" for i in range(1, 10)] + +while True: + payload = generate_random_decoded_payload() + data = { + "end_device_ids": { + "device_id": random.choice(device_ids), + "application_ids": {"application_id": random.choice(application_ids)}, + "dev_eui": "0000000000000000", + "join_eui": "0000000000000000", + "dev_addr": "00000000", + }, + "received_at": datetime.now().isoformat(), + "uplink_message": { + "session_key_id": "AZT1Hy95A+QB1gq8y/xU3w==", + "f_port": 14, + "f_cnt": 1256661, + "frm_payload": "lAVCf1YBAA==", + "decoded_payload": payload, + "locations": { + "frm-payload": { + "latitude": 46.5581818, + "longitude": 15.6164958, + "altitude": 335, + "source": "SOURCE_GPS", + } + }, + }, + } + serialized_data = json.dumps(data) + response = requests.post(url, headers=headers, json=data) + print(f"Status code: {response.status_code}") + print(f"Response: {response.text}") + time.sleep(0.001) diff --git a/ingest_service/requirements.txt b/ingest_service/requirements.txt new file mode 100644 index 0000000..feb6772 --- /dev/null +++ b/ingest_service/requirements.txt @@ -0,0 +1,4 @@ +fastapi[all]==0.116.1 +psycopg2==2.9.10 +sqlalchemy==2.0.41 +requests==2.32.4 diff --git a/ingest_service/ttn_example.json b/ingest_service/ttn_example.json new file mode 100644 index 0000000..9f5de31 --- /dev/null +++ b/ingest_service/ttn_example.json @@ -0,0 +1,89 @@ +{ + "end_device_ids": { + "device_id": "sp100089", + "application_ids": { + "application_id": "irnas-ranger-test" + }, + "dev_eui": "0016C001F0118501", + "join_eui": "0000000000000000", + "dev_addr": "260BB7AD" + }, + "correlation_ids": [ + "gs:uplink:01K09ESCSX0Z453H21R0N2WH43" + ], + "received_at": "2025-07-16T11:09:19.243513998Z", + "uplink_message": { + "session_key_id": "AZT1Hy95A+QB1gq8y/xU3w==", + "f_port": 14, + "f_cnt": 1256661, + "frm_payload": "lAVCf1YBAA==", + "decoded_payload": { + "n_msg": 87679, + "percentage": 66 + }, + "rx_metadata": [ + { + "gateway_ids": { + "gateway_id": "eui-5030354168114750", + "eui": "5030354168114750" + }, + "time": "2025-07-16T11:08:07.250218Z", + "timestamp": 391259796, + "rssi": -77, + "channel_rssi": -77, + "snr": 11.25, + "location": { + "latitude": 46.55781635533368, + "longitude": 15.61647190301601, + "altitude": 280, + "source": "SOURCE_REGISTRY" + }, + "uplink_token": "CiIKIAoUZXVpLTUwMzAzNTQxNjgxMTQ3NTASCFAwNUFoEUdQEJTNyLoBGgsI35DewwYQpKDiECCg5JrHsaWeAQ==", + "channel_index": 1, + "received_at": "2025-07-16T11:09:19.009858908Z" + }, + { + "gateway_ids": { + "gateway_id": "nob2", + "eui": "58A0CBFFFE800BFE" + }, + "time": "2025-07-16T11:09:18.970252990Z", + "timestamp": 3952843548, + "rssi": -101, + "channel_rssi": -101, + "snr": 10.25, + "uplink_token": "ChIKEAoEbm9iMhIIWKDL//6AC/4QnLbu3A4aCwjfkN7DBhCAufAYIODK176F8AE=", + "received_at": "2025-07-16T11:09:19.023445494Z" + } + ], + "settings": { + "data_rate": { + "lora": { + "bandwidth": 125000, + "spreading_factor": 9, + "coding_rate": "4/5" + } + }, + "frequency": "868300000", + "timestamp": 391259796, + "time": "2025-07-16T11:08:07.250218Z" + }, + "received_at": "2025-07-16T11:09:19.037825402Z", + "consumed_airtime": "0.185344s", + "locations": { + "frm-payload": { + "latitude": 46.5581818, + "longitude": 15.6164958, + "altitude": 335, + "source": "SOURCE_GPS" + } + }, + "network_ids": { + "net_id": "000013", + "ns_id": "EC656E0000000181", + "tenant_id": "ttn", + "cluster_id": "eu1", + "cluster_address": "eu1.cloud.thethings.network" + } + } +} From 795d33fb94b5f306adc3aaa39977aa7e1304f800 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Thu, 17 Jul 2025 15:41:02 +0200 Subject: [PATCH 05/28] feat: update system architecture --- .gitignore | 4 +++ Makefile | 2 +- docker-compose-dev.yml | 4 +-- init/postgres.sql | 75 ++++++++++++++++++++++++++++-------------- 4 files changed, 57 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index a59e22d..1d7c3ab 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ share/python-wheels/ #envrc .envrc + +ingest_service/common/* +rest_api/common/* +processor_service/common/* diff --git a/Makefile b/Makefile index f4d9c50..35c4b32 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ export COMPOSE_DOCKER_CLI_BUILD=1 export DOCKER_BUILDKIT=1 dev-up: - docker compose -f docker-compose-dev.yml -p ajdovscina-dev up --build -d + docker compose -f docker-compose-dev.yml -p ajdovscina-dev up --build dev-down: docker compose -f docker-compose-dev.yml -p ajdovscina-dev down --remove-orphans diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index db1afd4..ed0bcc4 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -37,7 +37,7 @@ services: nodered: image: nodered/node-red:3.1.15-minimal - container_name: nodered + container_name: ajdovscina-nodered ports: - "11880:1880" # Exposes Node-RED UI on http://localhost:1880 volumes: @@ -101,7 +101,7 @@ services: - 19290:9090 node-exporter: - container_name: ajdovscinanode-exporter + container_name: ajdovscina-node-exporter image: prom/node-exporter:v1.8.2 restart: always volumes: diff --git a/init/postgres.sql b/init/postgres.sql index 64445ab..4ec8559 100644 --- a/init/postgres.sql +++ b/init/postgres.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS sensors ( id SERIAL PRIMARY KEY, sensor_uid UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(), -- renamed to avoid confusion with PK sensor_name VARCHAR(255) NOT NULL, -- Added for better identification - sensor_type VARCHAR(50) NOT NULL, -- e.g. 'fire', 'water' + application_id INTEGER NOT NULL, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); @@ -25,12 +25,6 @@ CREATE TABLE IF NOT EXISTS sensors ( -- smoke_level FLOAT NOT NULL -- ); -CREATE TABLE IF NOT EXISTS applications ( - id SERIAL PRIMARY KEY, - description TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -); - CREATE TABLE IF NOT EXISTS locations ( id SERIAL PRIMARY KEY, description TEXT NOT NULL, @@ -49,11 +43,29 @@ CREATE TABLE IF NOT EXISTS metadata ( location_id INTEGER ); +-- Create metatable holding information about application and sensor + +CREATE TABLE IF NOT EXISTS applications( + id SERIAL PRIMARY KEY, + app_name TEXT NOT NULL, + description TEXT, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS application_measurements_registry( + id SERIAL PRIMARY KEY, + application_id INTEGER NOT NULL, + measurement_table_name TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + -- Create hypertables CREATE TABLE IF NOT EXISTS positions ( - id INT NOT NULL, + id INTEGER NOT NULL, sensor_id INTEGER NOT NULL, latitude DOUBLE PRECISION NOT NULL, longitude DOUBLE PRECISION NOT NULL, @@ -63,26 +75,33 @@ CREATE TABLE IF NOT EXISTS positions ( PRIMARY KEY (id, sensor_id, position_time) ); -CREATE TABLE IF NOT EXISTS readings ( - id INT NOT NULL, - sensor_id INTEGER NOT NULL, - device_type VARCHAR(50) NOT NULL, - timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id, sensor_id, device_type, timestamp) -); +-- CREATE TABLE IF NOT EXISTS measurements ( +-- id INTEGER NOT NULL, +-- sensor_id INTEGER NOT NULL, +-- application_id INTEGER NOT NULL, +-- timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, +-- PRIMARY KEY (id, sensor_id, application_id, timestamp) +-- ); -- 2.1. Create a geospatial index to improve spatial query performance CREATE INDEX IF NOT EXISTS idx_positions_geom ON positions USING GIST (geom); -- 2.2. Add unique constraint on deviceId and date to support ON CONFLICT operations --- ALTER TABLE positions DROP CONSTRAINT IF EXISTS unique_device_date; --- ALTER TABLE positions ADD CONSTRAINT unique_device_date UNIQUE (sensor_id, position_time); SELECT create_hypertable('positions', 'position_time', 'sensor_id', if_not_exists => TRUE, number_partitions => 10); -SELECT create_hypertable('readings', 'timestamp', 'sensor_id', if_not_exists => TRUE, number_partitions => 10); +-- SELECT create_hypertable('measurements', 'timestamp', 'sensor_id', if_not_exists => TRUE, number_partitions => 10); -- 3.2. Create additional indexes to optimize queries CREATE INDEX IF NOT EXISTS idx_positions_id ON positions (id); -CREATE INDEX IF NOT EXISTS idx_readings_timestamp ON readings (timestamp); +-- CREATE INDEX IF NOT EXISTS idx_measurements_timestamp ON measurements (timestamp); + +ALTER TABLE applications DROP CONSTRAINT IF EXISTS unique_application_name; +ALTER TABLE applications ADD CONSTRAINT unique_application_name UNIQUE (app_name); + +ALTER TABLE application_measurements_registry DROP CONSTRAINT IF EXISTS unique_application_registry; +ALTER TABLE application_measurements_registry ADD CONSTRAINT unique_application_registry UNIQUE(application_id, measurement_table_name); + +ALTER TABLE sensors DROP CONSTRAINT IF EXISTS unique_sensor_name; +ALTER TABLE sensors ADD CONSTRAINT unique_sensor_name UNIQUE (sensor_name); -- ----------------------------------------------------------------------------------- -- 4. Establishing Referential Integrity @@ -95,18 +114,24 @@ FOREIGN KEY (sensor_id) REFERENCES sensors(id) ON DELETE CASCADE; --- 4.6. Add a foreign key from `metadata.application_id` to `application.id` +-- 4.7. Add a foreign key from `metadata.location_id` to `location.id` ALTER TABLE metadata -ADD CONSTRAINT fk_metadata_application +ADD CONSTRAINT fk_metadata_location +FOREIGN KEY (location_id) +REFERENCES locations(id) +ON DELETE CASCADE; + +ALTER TABLE application_measurements_registry +ADD CONSTRAINT fk_application_measurements_registry_applications FOREIGN KEY (application_id) REFERENCES applications(id) ON DELETE CASCADE; --- 4.7. Add a foreign key from `metadata.location_id` to `location.id` +-- 4.8. Add a foreign key from `metadata.application_id` to `applications.id` ALTER TABLE metadata -ADD CONSTRAINT fk_metadata_location -FOREIGN KEY (location_id) -REFERENCES locations(id) +ADD CONSTRAINT fk_metadata_applications +FOREIGN KEY (application_id) +REFERENCES applications(id) ON DELETE CASCADE; -- Warning: This affects all transactions on the PostgreSQL instance. From 4f67a84a856ab6e97649fbdfbd685a3999d0aff8 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Thu, 17 Jul 2025 15:42:47 +0200 Subject: [PATCH 06/28] misc: remove common from makefile --- ingest_service/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ingest_service/Makefile b/ingest_service/Makefile index d124a11..4d17af5 100644 --- a/ingest_service/Makefile +++ b/ingest_service/Makefile @@ -1,10 +1,10 @@ SHELL := /bin/bash clean: - rm -r -f ./common + # rm -r -f ./common install: - cp -r ../common/ ./ + # cp -r ../common/ ./ pip install -r requirements.txt run: clean install From a93bb402ea89941251febff14f3707118984b358 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Thu, 17 Jul 2025 15:44:11 +0200 Subject: [PATCH 07/28] feat: add processor service --- processor_service/Makefile | 21 ++ processor_service/main.py | 45 +++++ processor_service/requirements.txt | 1 + processor_service/src/generate_table.py | 247 ++++++++++++++++++++++++ 4 files changed, 314 insertions(+) create mode 100644 processor_service/Makefile create mode 100644 processor_service/main.py create mode 100644 processor_service/requirements.txt create mode 100644 processor_service/src/generate_table.py diff --git a/processor_service/Makefile b/processor_service/Makefile new file mode 100644 index 0000000..2549268 --- /dev/null +++ b/processor_service/Makefile @@ -0,0 +1,21 @@ +SHELL := /bin/bash + +clean: + # rm -r -f ./common + +install: + # cp -r ../common/ ./ + pip install -r requirements.txt + +run: clean install + bash -c "trap 'echo \"Cleaning up...\"; make clean' EXIT; python3 main.py" + +run-local-dev: + python3.10 -m venv ../venv + source ../venv/bin/activate; \ + export ENV=development && export LOCALHOST=true && make run + +run-dev: + python3.10 -m venv ../venv + source ../venv/bin/activate; \ + export ENV=development && export LOCALHOST=false && make run diff --git a/processor_service/main.py b/processor_service/main.py new file mode 100644 index 0000000..cfcca65 --- /dev/null +++ b/processor_service/main.py @@ -0,0 +1,45 @@ +import json +import logging + +from kafka import KafkaConsumer +from src.generate_table import insert_data + +consumer = KafkaConsumer( + "process_data", + bootstrap_servers="localhost:9992", + auto_offset_reset="earliest", + group_id="ajdovscina-consumer", + max_poll_interval_ms=900000, + # enable_auto_commit=True, +) + +logging.basicConfig( + format="%(asctime)s %(levelname)s:%(message)s", + level=logging.INFO, +) +logger = logging.getLogger(__name__) + +for message in consumer: + logger.info( + "%s:%d:%d: key=%s value=%s" + % ( + message.topic, + message.partition, + message.offset, + message.key, + message.value, + ) + ) + + if message.key == b"ttn_service": + byte_string = message.value + + data_to_insert = json.loads(byte_string.decode("utf-8")) + + print(f"Incoming data: {data_to_insert}") + insert_data(data_to_insert) + # try: + # except Exception as e: + # logger.error(f"Error inserting data: {e}") + + consumer.commit_async() diff --git a/processor_service/requirements.txt b/processor_service/requirements.txt new file mode 100644 index 0000000..7aedb58 --- /dev/null +++ b/processor_service/requirements.txt @@ -0,0 +1 @@ +kafka-python==2.0.2 diff --git a/processor_service/src/generate_table.py b/processor_service/src/generate_table.py new file mode 100644 index 0000000..bb446dc --- /dev/null +++ b/processor_service/src/generate_table.py @@ -0,0 +1,247 @@ +"""This module dynamically creates SQL tables based on the sensor type and present fields. + +It infers the data type based on the value of the field and generates a SQL CREATE TABLE statement. +""" + +from datetime import datetime + +import psycopg2 + +"""The flow is as follows: + +1. Application data is received from device +2. A new application is created if it doesn't yet exist +3. A new _ table is created if it doesn't yet exist +4. The pair is added to the application_measurements_registry +5. Measurements are added to the _ table +6. Measurements are added to the `measurements` table +""" + +GEOM_DB_CONFIG = { + "dbname": "ajdovscina", + "user": "ajdovscina-geospatial-user", + "password": "password", + "host": "localhost", + "port": 13328, +} +conn = psycopg2.connect(**GEOM_DB_CONFIG) + + +def drop_duplicates(fields: dict): + """Drop duplicates from data fields in case they are present.""" + seen = {} + for k, v in fields.items(): + k = k.lower() + if k not in seen: + seen[k] = v # keep first occurrence + return seen + + +def insert_data(data: dict): + """Insert data into SQL database.""" + try: + with conn.cursor() as cur: + # 0. Verify integrity of data - this should rather be done on the receiver and the sending party should be notified if data is not good + + # 2. + app_name = data.get("application") + stmt = create_application(app_name) + cur.execute(stmt) + print(f"App create statement: {stmt}") + app_id = cur.fetchone()[0] + print(f"App id: {app_id}") + + # 3. + version = data.get("version") + fields = data.get("measurement", {}) + + print(fields) + + # Drop duplicate keys from fields + fields = drop_duplicates(fields) + + if fields is None or len(fields) == 0: + return + + meas_table_name = f"{app_name}_{version}" + stmt, table_name = create_sensor_readings_table(meas_table_name, fields) + print(f"Create measurement table statement: {stmt}") + cur.execute(stmt) + + # 3.05 Get existing columns + cur.execute( + f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}'" + ) + existing_columns = {row[0].lower() for row in cur.fetchall()} + print(f"Existing columns in table {table_name}: {existing_columns}") + + # 3.1 Alter the table to add new columns + stmt = alter_measurement_table(meas_table_name, fields, existing_columns) + if stmt is not None: + cur.execute(stmt) + + # 4. + add_to_registry_stmt = add_table_to_registry(app_id, table_name) + print(f"Add to registry statement: {add_to_registry_stmt}") + cur.execute(add_to_registry_stmt) + + # 5. Add measurement to table + timestamp = data.get("received_at", datetime.now()) + sensor_name = data.get("sensor_name", "") + + # 5.1 First create the sensor if it doesn't exist + sensor_insert_query = insert_sensor() + cur.execute( + sensor_insert_query, + ( + sensor_name, + app_id, + ), + ) + sens_id = cur.fetchone()[0] + print(f"Sens id: {sens_id}") + + insert_query = insert_sensor_readings( + table_name, fields, timestamp, sens_id + ) + print(f"Insert query: {insert_query}") + + cur.execute(insert_query) + + conn.commit() + except Exception as e: + conn.rollback() + print(f"Failed to insert data: {e}") + + +def insert_sensor(): + """Creates a SQL INSERT INTO statement for the sensors table.""" + query = """ + INSERT INTO sensors (sensor_name, application_id) + VALUES (%s, %s) + ON CONFLICT (sensor_name) + DO UPDATE SET sensor_name = EXCLUDED.sensor_name + RETURNING id; + """ + return query + + +def insert_sensor_readings( + table_name: str, fields: dict, timestamp: datetime, sensor_id: int +): + """Creates a SQL INSERT INTO statement based on the name and field.""" + keys = fields.keys() + values = fields.values() + columns = "timestamp, sensor_id, " + columns += ", ".join(keys) + values_placeholders = f"'{timestamp}', {sensor_id}, " + values_placeholders += ", ".join([f"'{value}'" for value in values]) + + query = f'INSERT INTO "{table_name}" ({columns}) VALUES ({values_placeholders})' + + return query + + +def create_sensor_readings_table(application_name: str, fields: dict): + """Creates a SQL CREATE TABLE statement based on the sensor type and fields. + + Args: + application_name (str): The name of the application, used to create the table name. + fields (dict): A dictionary containing field names and their corresponding values. + + Returns: + str: A SQL CREATE TABLE statement. + """ + table_name = application_name.replace(" ", "_").lower() + columns = [] + columns.append("id SERIAL PRIMARY KEY") + columns.append("sensor_id INTEGER") + columns.append("timestamp TIMESTAMPTZ") + + print(f"Fields: {fields}") + + for field, value in fields.items(): + if isinstance(value, int): + col_type = "FLOAT" + elif isinstance(value, float): + col_type = "DOUBLE PRECISION" + elif isinstance(value, str): + col_type = "TEXT" + elif isinstance(value, datetime): + col_type = "TIMESTAMPTZ" + + # Check if column might already exist + column = f"{field} {col_type}".lower() + if column in columns: + continue + + columns.append(column) + + columns_str = ", ".join(columns) + create_table_statement = ( + f'CREATE TABLE IF NOT EXISTS "{table_name}" ({columns_str})' + ) + + return create_table_statement, table_name + + +def alter_measurement_table(table_name, fields, existing_columns): + """A table can already exist but not all fields are present. If not all fields are present alter the table.""" + print(f"Fields: {fields}") + columns = [] + + for field, value in fields.items(): + if isinstance(value, int): + col_type = "FLOAT" + elif isinstance(value, float): + col_type = "DOUBLE PRECISION" + elif isinstance(value, str): + col_type = "TEXT" + elif isinstance(value, datetime): + col_type = "TIMESTAMPTZ" + + if field.lower() not in existing_columns: + columns.append(f"ADD COLUMN {field} {col_type},") + + if len(columns) == 0: + return None + + columns[-1] = columns[-1].rstrip(",") # Remove trailing comma from the last column + + columns_str = "\n".join(columns) + statement = f'ALTER TABLE "{table_name}"\n{columns_str};' + + print(f"Statement {statement}") + + return statement + + +def create_application(application_name): + """Insert application to applications table if it doesn't exist.""" + stmt = f""" + INSERT INTO applications (app_name) + VALUES ('{application_name}') + ON CONFLICT (app_name) + DO UPDATE SET app_name = EXCLUDED.app_name + RETURNING id; + """ + return stmt + + +def add_table_to_registry(application_id, measurement_table_name): + """Adds the created table to the registry. + + Args: + application_id (str): The ID of the application. + measurement_table_name (str): The name of the table to be added. + + Returns: + str: A SQL INSERT statement to add the table to the registry. + """ + stmt = ( + f"INSERT INTO application_measurements_registry (application_id, measurement_table_name) " + f"VALUES ('{application_id}', '{measurement_table_name}') " + f"ON CONFLICT (application_id, measurement_table_name) DO NOTHING;" + ) + + return stmt From b4f4003d5814fff31340b9f8b45ada719e39aea3 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Thu, 17 Jul 2025 15:45:10 +0200 Subject: [PATCH 08/28] feat: add REST API --- rest_api/Makefile | 21 +++++ rest_api/main.py | 175 ++++++++++++++++++++++++++++++++++++++ rest_api/requirements.txt | 4 + 3 files changed, 200 insertions(+) create mode 100644 rest_api/Makefile create mode 100644 rest_api/main.py create mode 100644 rest_api/requirements.txt diff --git a/rest_api/Makefile b/rest_api/Makefile new file mode 100644 index 0000000..373091a --- /dev/null +++ b/rest_api/Makefile @@ -0,0 +1,21 @@ +SHELL := /bin/bash + +clean: + rm -r -f ./common + +install: + cp -r ../common/ ./ + pip install -r requirements.txt + +run: clean install + bash -c "trap 'echo \"Cleaning up...\"; make clean' EXIT; uvicorn main:app --host 0.0.0.0 --port 51005" + +run-local-dev: + python3.10 -m venv ../venv + source ../venv/bin/activate; \ + export ENV=development && export LOCALHOST=true && make run + +run-dev: + python3.10 -m venv ../venv + source ../venv/bin/activate; \ + export ENV=development && export LOCALHOST=false && make run diff --git a/rest_api/main.py b/rest_api/main.py new file mode 100644 index 0000000..e2656f4 --- /dev/null +++ b/rest_api/main.py @@ -0,0 +1,175 @@ +from typing import List, Optional + +import orjson +import psycopg2 +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from fastapi.responses import ORJSONResponse +from pydantic import BaseModel + +app = FastAPI(default_response_class=ORJSONResponse) + + +class Measurement(BaseModel): + """Data model for sensor measurements.""" + + table_name: str + timestamp: str + data: dict # dynamic columns from the database + + +class Sensor(BaseModel): + """Data model for sensor information.""" + + sensor_name: str + application_id: int + + +class Application(BaseModel): + """Data model for application information.""" + + app_name: str + description: Optional[str] = None + + +def get_db_connection(): + """Establish a connection to the PostgreSQL database.""" + return psycopg2.connect( + host="localhost", + port=13328, + user="ajdovscina-geospatial-user", + password="password", + dbname="ajdovscina", + ) + + +@app.get("/api/sensors", response_model=List[Sensor]) +def get_sensors(): + """Fetch all sensors from the database.""" + conn = get_db_connection() + cur = conn.cursor() + cur.execute(""" + SELECT id, sensor_uid, sensor_name, application_id, created_at, updated_at + FROM sensors + """) + rows = cur.fetchall() + sensors = [ + Sensor( + id=row[0], + sensor_uid=row[1], + sensor_name=row[2], + application_id=row[3], + created_at=str(row[4]), + updated_at=str(row[5]), + ) + for row in rows + ] + cur.close() + conn.close() + + encoded = jsonable_encoder({"sensors": sensors}) + pretty_json = ORJSONResponse( + content=orjson.loads(orjson.dumps(encoded, option=orjson.OPT_INDENT_2)), + ) + return pretty_json + + +@app.get("/api/applications", response_model=List[Application]) +def get_applications(): + """Fetch all applications from the database.""" + conn = get_db_connection() + cur = conn.cursor() + cur.execute(""" + SELECT id, app_name, description, created_at, updated_at + FROM applications + """) + rows = cur.fetchall() + + applications = [ + Application( + id=row[0], + app_name=row[1], + description=row[2], + created_at=str(row[3]), + updated_at=str(row[4]), + ) + for row in rows + ] + cur.close() + conn.close() + + encoded = jsonable_encoder({"applications": applications}) + pretty_json = ORJSONResponse( + content=orjson.loads(orjson.dumps(encoded, option=orjson.OPT_INDENT_2)), + ) + return pretty_json + + +@app.get("/api/measurements/sensor_name", response_model=List[Measurement]) +def get_measurements(sensor_name): + """Fetch measurements for a given sensor name.""" + conn = get_db_connection() + cur = conn.cursor() + + # Fetch application_id for the given sensor_name + query = "SELECT application_id, id FROM sensors WHERE sensor_name = %s" + # print(f"Executing query: {query} with sensor_name: {sensor_name}") + cur.execute(query, (sensor_name,)) + result = cur.fetchone() + # print(result) + if not result: + cur.close() + conn.close() + return ORJSONResponse(content={"error": "Sensor not found"}, status_code=404) + application_id = result[0] + sensor_id = result[1] + + # Fetch measurement table names for the application_id + query = """ + SELECT measurement_table_name + FROM application_measurements_registry + WHERE application_id = %s + """ + # print(f"Measurement table query: {query} with application_id: {application_id}") + cur.execute(query, (application_id,)) + tables = cur.fetchall() + if not tables: + cur.close() + conn.close() + return ORJSONResponse(content={"measurements": []}) + + measurements = [] + for table_row in tables: + table_name = table_row[0] + # Fetch all measurements for the sensor_id from each table + query = f""" + SELECT * + FROM "{table_name}" + WHERE sensor_id = %s + """ + # print(f"Measurement query: {query} with sensor_id: {sensor_id}") + cur.execute(query, (sensor_id,)) + rows = cur.fetchall() + if cur.description is not None: + colnames = [desc[0] for desc in cur.description] + for row in rows: + # Build a dict of column names to values, excluding sensor_id and timestamp + + data = {col: val for col, val in zip(colnames[3:], row[3:])} + measurements.append( + Measurement( + # sensor_id=row[1], + table_name=table_name, + timestamp=str(row[2]), + data=data, + ) + ) + + cur.close() + conn.close() + + encoded = jsonable_encoder({"measurements": measurements}) + pretty_json = ORJSONResponse( + content=orjson.loads(orjson.dumps(encoded, option=orjson.OPT_INDENT_2)), + ) + return pretty_json diff --git a/rest_api/requirements.txt b/rest_api/requirements.txt new file mode 100644 index 0000000..feb6772 --- /dev/null +++ b/rest_api/requirements.txt @@ -0,0 +1,4 @@ +fastapi[all]==0.116.1 +psycopg2==2.9.10 +sqlalchemy==2.0.41 +requests==2.32.4 From 28a534108cb79155ac07cfcd4265fc96cb8b5247 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Thu, 17 Jul 2025 15:45:36 +0200 Subject: [PATCH 09/28] feat: add Grafana dashboards --- .pre-commit-config.yaml | 2 - grafana/dashboards/kafka-exporter.json | 626 + grafana/dashboards/node-exporter.json | 23870 ++++++++++++++++++ grafana/dashboards/postgresql-exporter.json | 3628 +++ prometheus/prometheus-dev.yml | 4 +- 5 files changed, 28126 insertions(+), 4 deletions(-) create mode 100644 grafana/dashboards/kafka-exporter.json create mode 100644 grafana/dashboards/node-exporter.json create mode 100644 grafana/dashboards/postgresql-exporter.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f811bc6..1bd2142 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,8 +21,6 @@ repos: stages: [pre-commit] - id: detect-private-key stages: [pre-commit] - - id: check-added-large-files - stages: [pre-commit] # clang-format uses .clang-format to apply formatting - repo: https://github.com/pre-commit/mirrors-clang-format rev: v18.1.7 diff --git a/grafana/dashboards/kafka-exporter.json b/grafana/dashboards/kafka-exporter.json new file mode 100644 index 0000000..626cc7f --- /dev/null +++ b/grafana/dashboards/kafka-exporter.json @@ -0,0 +1,626 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.2.2" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "text", + "name": "Text", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Kafka resource usage and throughput", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 7589, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "gridPos": { + "h": 2, + "w": 20, + "x": 0, + "y": 0 + }, + "id": 33, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "(responses to devices) mysql processor: storing data in mysql database - directly in UDP receiver\n(data to database) consumer: storing data from mysql database to telespor postgres database", + "mode": "markdown" + }, + "pluginVersion": "10.2.2", + "title": "System description", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 0, + "y": 2 + }, + "id": 14, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 480 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "sum by(topic) (rate(kafka_topic_partition_current_offset{instance=\"$instance\", topic=\"process_data\"}[1m]))", + "format": "time_series", + "fullMetaSearch": false, + "includeNullMetadata": true, + "intervalFactor": 1, + "legendFormat": "{{topic}}", + "range": true, + "refId": "B", + "useBackend": false + } + ], + "title": "Message in per second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 10, + "y": 2 + }, + "id": 12, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 480 + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(kafka_consumergroup_lag{instance=\"$instance\", topic=\"process_data\"} and ignoring(offset) kafka_consumergroup_current_offset > 0) by (consumergroup, topic)", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{consumergroup}} (topic: {{topic}})", + "refId": "A" + } + ], + "title": "Lag by Consumer Group", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 0, + "y": 10 + }, + "id": 18, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 480 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "clamp_min(\n sum(\n delta(kafka_consumergroup_current_offset{instance=\"$instance\", topic=\"process_data\"}[1m])\n ) by (consumergroup, topic) / 1,\n0)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{consumergroup}} (topic: {{topic}})", + "range": true, + "refId": "A" + } + ], + "title": "Message consume per minute", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 10, + "y": 10 + }, + "id": 19, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 480 + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "sum by(consumergroup, topic) (kafka_topic_partition_current_offset{instance=\"$instance\", topic=\"process_data\"})", + "format": "time_series", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{consumergroup}} (topic: {{topic}})", + "refId": "A", + "useBackend": false + } + ], + "title": "Total messages processed", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 38, + "tags": [ + "Kafka" + ], + "templating": { + "list": [ + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Job", + "multi": false, + "name": "job", + "options": [], + "query": "label_values(kafka_consumergroup_current_offset, job)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Instance", + "multi": false, + "name": "instance", + "options": [], + "query": "label_values(kafka_consumergroup_current_offset{job=~\"$job\"}, instance)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "", + "hide": 0, + "includeAll": true, + "label": "Topic", + "multi": true, + "name": "topic", + "options": [], + "query": "label_values(kafka_topic_partition_current_offset{instance='$instance',topic!='__consumer_offsets',topic!='--kafka'}, topic)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Kafka Exporter Overview", + "uid": "jwPKIsniz", + "version": 3, + "weekStart": "" +} diff --git a/grafana/dashboards/node-exporter.json b/grafana/dashboards/node-exporter.json new file mode 100644 index 0000000..79b653b --- /dev/null +++ b/grafana/dashboards/node-exporter.json @@ -0,0 +1,23870 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "panel", + "id": "bargauge", + "name": "Bar gauge", + "version": "" + }, + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "9.4.3" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "$$hashKey": "object:1058", + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 1860, + "graphTooltip": 1, + "id": null, + "links": [ + { + "icon": "external link", + "tags": [], + "targetBlank": true, + "title": "GitHub", + "type": "link", + "url": "https://github.com/rfmoz/grafana-dashboards" + }, + { + "icon": "external link", + "tags": [], + "targetBlank": true, + "title": "Grafana", + "type": "link", + "url": "https://grafana.com/grafana/dashboards/1860" + } + ], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 261, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Quick CPU / Mem / Disk", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Resource pressure via PSI", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "links": [], + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "dark-yellow", + "value": 70 + }, + { + "color": "dark-red", + "value": 90 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 323, + "links": [], + "options": { + "displayMode": "basic", + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "text": {} + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "irate(node_pressure_cpu_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": true, + "intervalFactor": 1, + "legendFormat": "CPU", + "range": false, + "refId": "CPU some", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "irate(node_pressure_memory_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "instant": true, + "intervalFactor": 1, + "legendFormat": "Mem", + "range": false, + "refId": "Memory some", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "irate(node_pressure_io_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "instant": true, + "intervalFactor": 1, + "legendFormat": "I/O", + "range": false, + "refId": "I/O some", + "step": 240 + } + ], + "title": "Pressure", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Busy state of all CPU cores together", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 85 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 95 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 3, + "y": 1 + }, + "id": 20, + "links": [], + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "100 * (1 - avg(rate(node_cpu_seconds_total{mode=\"idle\", instance=\"$node\"}[$__rate_interval])))", + "hide": false, + "instant": true, + "intervalFactor": 1, + "legendFormat": "", + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Busy", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "System load over all CPU cores together", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 85 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 95 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 6, + "y": 1 + }, + "id": 155, + "links": [], + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "scalar(node_load1{instance=\"$node\",job=\"$job\"}) * 100 / count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu))", + "format": "time_series", + "hide": false, + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Sys Load", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Non available RAM memory", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 80 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 9, + "y": 1 + }, + "hideTimeOverride": false, + "id": 16, + "links": [], + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "((node_memory_MemTotal_bytes{instance=\"$node\", job=\"$job\"} - node_memory_MemFree_bytes{instance=\"$node\", job=\"$job\"}) / node_memory_MemTotal_bytes{instance=\"$node\", job=\"$job\"}) * 100", + "format": "time_series", + "hide": true, + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "(1 - (node_memory_MemAvailable_bytes{instance=\"$node\", job=\"$job\"} / node_memory_MemTotal_bytes{instance=\"$node\", job=\"$job\"})) * 100", + "format": "time_series", + "hide": false, + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "B", + "step": 240 + } + ], + "title": "RAM Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Used Swap", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 10 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 25 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 12, + "y": 1 + }, + "id": 21, + "links": [], + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "((node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"}) / (node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"})) * 100", + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "SWAP Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Used Root FS", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 80 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 15, + "y": 1 + }, + "id": 154, + "links": [], + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "100 - ((node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",mountpoint=\"/\",fstype!=\"rootfs\"} * 100) / node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",mountpoint=\"/\",fstype!=\"rootfs\"})", + "format": "time_series", + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Root FS Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Total number of CPU cores", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 18, + "y": 1 + }, + "id": 14, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu))", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "CPU Cores", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "System uptime", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 20, + "y": 1 + }, + "hideTimeOverride": true, + "id": 15, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "node_time_seconds{instance=\"$node\",job=\"$job\"} - node_boot_time_seconds{instance=\"$node\",job=\"$job\"}", + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Uptime", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Total RootFS", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 70 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 18, + "y": 3 + }, + "id": 23, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",mountpoint=\"/\",fstype!=\"rootfs\"}", + "format": "time_series", + "hide": false, + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "RootFS Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Total RAM", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 20, + "y": 3 + }, + "id": 75, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}", + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "RAM Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Total SWAP", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 22, + "y": 3 + }, + "id": 18, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"}", + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "SWAP Total", + "type": "stat" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 263, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Basic CPU / Mem / Net / Disk", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Basic CPU info", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Busy Iowait" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Idle" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy Iowait" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Idle" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy System" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy User" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy Other" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 77, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "width": 250 + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"system\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "Busy System", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"user\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Busy User", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"iowait\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Busy Iowait", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=~\".*irq\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Busy IRQs", + "range": true, + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode!='idle',mode!='user',mode!='system',mode!='iowait',mode!='irq',mode!='softirq'}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Busy Other", + "range": true, + "refId": "E", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"idle\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Idle", + "range": true, + "refId": "F", + "step": 240 + } + ], + "title": "CPU Basic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Basic memory usage", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "SWAP Used" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap Used" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM Cache + Buffer" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Available" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#DEDAF7", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 78, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "RAM Total", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"} - (node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} + node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} + node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "RAM Used", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} + node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} + node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "RAM Cache + Buffer", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "RAM Free", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "(node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SWAP Used", + "refId": "E", + "step": 240 + } + ], + "title": "Memory Basic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Basic network info per interface", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Recv_bytes_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_drop_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_errs_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CCA300", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_bytes_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_drop_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_errs_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CCA300", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_drop_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#967302", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_errs_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_bytes_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_drop_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#967302", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_errs_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 74, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "recv {{device}}", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "trans {{device}} ", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Basic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Disk space used of all filesystems mounted", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 13 + }, + "id": 152, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "100 - ((node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'} * 100) / node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}}", + "refId": "A", + "step": 240 + } + ], + "title": "Disk Space Used Basic", + "type": "timeseries" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 265, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "percentage", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 70, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Idle - Waiting for something to happen" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Iowait - Waiting for I/O to complete" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Irq - Servicing interrupts" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Nice - Niced processes executing in user mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Softirq - Servicing softirqs" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Steal - Time spent in other operating systems when running in a virtualized environment" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCE2DE", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "System - Processes executing in kernel mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "User - Normal processes executing in user mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#5195CE", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 21 + }, + "id": 3, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 250 + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"system\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "System - Processes executing in kernel mode", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"user\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "User - Normal processes executing in user mode", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"nice\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Nice - Niced processes executing in user mode", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"iowait\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Iowait - Waiting for I/O to complete", + "range": true, + "refId": "E", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"irq\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Irq - Servicing interrupts", + "range": true, + "refId": "F", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"softirq\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Softirq - Servicing softirqs", + "range": true, + "refId": "G", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"steal\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Steal - Time spent in other operating systems when running in a virtualized environment", + "range": true, + "refId": "H", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"idle\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Idle - Waiting for something to happen", + "range": true, + "refId": "J", + "step": 240 + } + ], + "title": "CPU", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap - Swap memory usage" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused - Free memory unassigned" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Hardware Corrupted - *./" + }, + "properties": [ + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 21 + }, + "id": 24, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Slab_bytes{instance=\"$node\",job=\"$job\"} - node_memory_PageTables_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapCached_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Apps - Memory used by user-space applications", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_PageTables_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "PageTables - Memory used to map between virtual and physical memory addresses", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_SwapCached_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SwapCache - Memory that keeps track of pages that have been fetched from swap but not yet been modified", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Slab_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Slab - Memory used by the kernel to cache data structures for its own use (caches like inode, dentry, etc)", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Cache - Parked file data (file content) cache", + "refId": "E", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Buffers - Block device (e.g. harddisk) cache", + "refId": "F", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Unused - Free memory unassigned", + "refId": "G", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "(node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Swap - Swap space used", + "refId": "H", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_HardwareCorrupted_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working", + "refId": "I", + "step": 240 + } + ], + "title": "Memory Stack", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bits out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "receive_packets_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "receive_packets_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "transmit_packets_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "transmit_packets_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 84, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 33 + }, + "id": 156, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'} - node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}}", + "refId": "A", + "step": 240 + } + ], + "title": "Disk Space Used", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "IO read (-) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 45 + }, + "id": 229, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])", + "intervalFactor": 4, + "legendFormat": "{{device}} - Reads completed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])", + "intervalFactor": 1, + "legendFormat": "{{device}} - Writes completed", + "refId": "B", + "step": 240 + } + ], + "title": "Disk IOps", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes read (-) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "io time" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*read*./" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "hidden" + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 45 + }, + "id": 42, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_read_bytes_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{device}} - Successfully read bytes", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_written_bytes_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{device}} - Successfully written bytes", + "refId": "B", + "step": 240 + } + ], + "title": "I/O Usage Read / Write", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "%util", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "io time" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "hidden" + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 57 + }, + "id": 127, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_io_time_seconds_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"} [$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}}", + "refId": "A", + "step": 240 + } + ], + "title": "I/O Utilization", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "percentage", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 70, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/^Guest - /" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#5195ce", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/^GuestNice - /" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#c15c17", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 57 + }, + "id": 319, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_guest_seconds_total{instance=\"$node\",job=\"$job\", mode=\"user\"}[1m])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[1m])))", + "hide": false, + "legendFormat": "Guest - Time spent running a virtual CPU for a guest operating system", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_guest_seconds_total{instance=\"$node\",job=\"$job\", mode=\"nice\"}[1m])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[1m])))", + "hide": false, + "legendFormat": "GuestNice - Time spent running a niced guest (virtual CPU for guest operating system)", + "range": true, + "refId": "B" + } + ], + "title": "CPU spent seconds in guests (VMs)", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "CPU / Memory / Net / Disk", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 266, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 54 + }, + "id": 136, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Inactive_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Inactive - Memory which has been less recently used. It is more eligible to be reclaimed for other purposes", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Active_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Active - Memory that has been used more recently and usually not reclaimed unless absolutely necessary", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Active / Inactive", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*CommitLimit - *./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 54 + }, + "id": 135, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Committed_AS_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Committed_AS - Amount of memory presently allocated on the system", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_CommitLimit_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "CommitLimit - Amount of memory currently available to be allocated on the system", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Committed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 64 + }, + "id": 191, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Inactive_file_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Inactive_file - File-backed memory on inactive LRU list", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Inactive_anon_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Inactive_anon - Anonymous and swap cache on inactive LRU list, including tmpfs (shmem)", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Active_file_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Active_file - File-backed memory on active LRU list", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Active_anon_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Active_anon - Anonymous and swap cache on active least-recently-used (LRU) list, including tmpfs", + "refId": "D", + "step": 240 + } + ], + "title": "Memory Active / Inactive Detail", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 64 + }, + "id": 130, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Writeback_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Writeback - Memory which is actively being written back to disk", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_WritebackTmp_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "WritebackTmp - Memory used by FUSE for temporary writeback buffers", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Dirty_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Dirty - Memory which is waiting to get written back to the disk", + "refId": "C", + "step": 240 + } + ], + "title": "Memory Writeback and Dirty", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 74 + }, + "id": 138, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Mapped_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Mapped - Used memory in mapped pages files which have been mapped, such as libraries", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Shmem_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Shmem - Used shared memory (shared between several processes, thus including RAM disks)", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_ShmemHugePages_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_ShmemPmdMapped_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "ShmemPmdMapped - Amount of shared (shmem/tmpfs) memory backed by huge pages", + "refId": "D", + "step": 240 + } + ], + "title": "Memory Shared and Mapped", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 74 + }, + "id": 131, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_SUnreclaim_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SUnreclaim - Part of Slab, that cannot be reclaimed on memory pressure", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SReclaimable - Part of Slab, that might be reclaimed, such as caches", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Slab", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 84 + }, + "id": 70, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_VmallocChunk_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "VmallocChunk - Largest contiguous block of vmalloc area which is free", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_VmallocTotal_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "VmallocTotal - Total size of vmalloc memory area", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_VmallocUsed_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "VmallocUsed - Amount of vmalloc area which is used", + "refId": "C", + "step": 240 + } + ], + "title": "Memory Vmalloc", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 84 + }, + "id": 159, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Bounce_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Bounce - Memory used for block device bounce buffers", + "refId": "A", + "step": 240 + } + ], + "title": "Memory Bounce", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Inactive *./" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 94 + }, + "id": 129, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_AnonHugePages_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "AnonHugePages - Memory in anonymous huge pages", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_AnonPages_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "AnonPages - Memory in user pages not backed by files", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Anonymous", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 94 + }, + "id": 160, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_KernelStack_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "KernelStack - Kernel memory stack. This is not reclaimable", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Percpu_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "PerCPU - Per CPU memory allocated dynamically by loadable modules", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Kernel / CPU", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "pages", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 104 + }, + "id": 140, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_HugePages_Free{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages_Free - Huge pages in the pool that are not yet allocated", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_HugePages_Rsvd{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages_Rsvd - Huge pages for which a commitment to allocate from the pool has been made, but no allocation has yet been made", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_HugePages_Surp{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages_Surp - Huge pages in the pool above the value in /proc/sys/vm/nr_hugepages", + "refId": "C", + "step": 240 + } + ], + "title": "Memory HugePages Counter", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 104 + }, + "id": 71, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_HugePages_Total{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages - Total size of the pool of huge pages", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Hugepagesize_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Hugepagesize - Huge Page size", + "refId": "B", + "step": 240 + } + ], + "title": "Memory HugePages Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 114 + }, + "id": 128, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_DirectMap1G_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "DirectMap1G - Amount of pages mapped as this size", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_DirectMap2M_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "DirectMap2M - Amount of pages mapped as this size", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_DirectMap4k_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "DirectMap4K - Amount of pages mapped as this size", + "refId": "C", + "step": 240 + } + ], + "title": "Memory DirectMap", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 114 + }, + "id": 137, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Unevictable_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Unevictable - Amount of unevictable memory that can't be swapped out for a variety of reasons", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Mlocked_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "MLocked - Size of pages locked to memory using the mlock() system call", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Unevictable and MLocked", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 124 + }, + "id": 132, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_NFS_Unstable_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "NFS Unstable - Memory in NFS pages sent to the server, but not yet committed to the storage", + "refId": "A", + "step": 240 + } + ], + "title": "Memory NFS", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Memory Meminfo", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 267, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "pages out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 41 + }, + "id": 176, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pgpgin{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pagesin - Page in operations", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pgpgout{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pagesout - Page out operations", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Pages In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "pages out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 41 + }, + "id": 22, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pswpin{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pswpin - Pages swapped in", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pswpout{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pswpout - Pages swapped out", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Pages Swap In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "faults", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Pgfault - Page major and minor fault operations" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 51 + }, + "id": 175, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pgfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pgfault - Page major and minor fault operations", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pgmajfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pgmajfault - Major page fault operations", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pgfault{instance=\"$node\",job=\"$job\"}[$__rate_interval]) - irate(node_vmstat_pgmajfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pgminfault - Minor page fault operations", + "refId": "C", + "step": 240 + } + ], + "title": "Memory Page Faults", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 51 + }, + "id": 307, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_oom_kill{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "oom killer invocations ", + "refId": "A", + "step": 240 + } + ], + "title": "OOM Killer", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Memory Vmstat", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 293, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "seconds", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Variation*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 260, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_estimated_error_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Estimated error in seconds", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_offset_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Time offset in between local system and reference clock", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_maxerror_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum error in seconds", + "refId": "C", + "step": 240 + } + ], + "title": "Time Synchronized Drift", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 291, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_loop_time_constant{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Phase-locked loop time adjust", + "refId": "A", + "step": 240 + } + ], + "title": "Time PLL Adjust", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Variation*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 34 + }, + "id": 168, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_sync_status{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Is clock synchronized to a reliable server (1 = yes, 0 = no)", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_frequency_adjustment_ratio{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Local clock frequency adjustment", + "refId": "B", + "step": 240 + } + ], + "title": "Time Synchronized Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "seconds", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 34 + }, + "id": 294, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_tick_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Seconds between clock ticks", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_tai_offset_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "International Atomic Time (TAI) offset", + "refId": "B", + "step": 240 + } + ], + "title": "Time Misc", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "System Timesync", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 312, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 73 + }, + "id": 62, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_procs_blocked{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Processes blocked waiting for I/O to complete", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_procs_running{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Processes in runnable state", + "refId": "B", + "step": 240 + } + ], + "title": "Processes Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Enable with --collector.processes argument on node-exporter", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 73 + }, + "id": 315, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_processes_state{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ state }}", + "refId": "A", + "step": 240 + } + ], + "title": "Processes State", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "forks / sec", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 83 + }, + "id": 148, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_forks_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Processes forks second", + "refId": "A", + "step": 240 + } + ], + "title": "Processes Forks", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max.*/" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 83 + }, + "id": 149, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(process_virtual_memory_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Processes virtual memory size in bytes", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "process_resident_memory_max_bytes{instance=\"$node\",job=\"$job\"}", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum amount of virtual memory available in bytes", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(process_virtual_memory_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Processes virtual memory size in bytes", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(process_virtual_memory_max_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum amount of virtual memory available in bytes", + "refId": "D", + "step": 240 + } + ], + "title": "Processes Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Enable with --collector.processes argument on node-exporter", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "PIDs limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 93 + }, + "id": 313, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_processes_pids{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Number of PIDs", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_processes_max_processes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "PIDs limit", + "refId": "B", + "step": 240 + } + ], + "title": "PIDs Number and Limit", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "seconds", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*waiting.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 93 + }, + "id": 305, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_schedstat_running_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }} - seconds spent running a process", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_schedstat_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }} - seconds spent by processing waiting for this CPU", + "refId": "B", + "step": 240 + } + ], + "title": "Process schedule stats Running / Waiting", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Enable with --collector.processes argument on node-exporter", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Threads limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 103 + }, + "id": 314, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_processes_threads{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Allocated threads", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_processes_max_threads{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Threads limit", + "refId": "B", + "step": 240 + } + ], + "title": "Threads Number and Limit", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "System Processes", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 269, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 26 + }, + "id": 8, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_context_switches_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Context switches", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_intr_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Interrupts", + "refId": "B", + "step": 240 + } + ], + "title": "Context Switches / Interrupts", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 26 + }, + "id": 7, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_load1{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Load 1m", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_load5{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Load 5m", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_load15{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Load 15m", + "refId": "C", + "step": 240 + } + ], + "title": "System Load", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "hertz" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 10 + }, + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": false, + "viz": false + } + }, + { + "id": "custom.fillBelowTo", + "value": "Min" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Min" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + }, + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": false, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 36 + }, + "id": 321, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_cpu_scaling_frequency_hertz{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg(node_cpu_scaling_frequency_max_hertz{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Max", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "avg(node_cpu_scaling_frequency_min_hertz{instance=\"$node\",job=\"$job\"})", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Min", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "CPU Frequency Scaling", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "https://docs.kernel.org/accounting/psi.html", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Memory some" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Memory full" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "I/O some" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "I/O full" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 36 + }, + "id": 322, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "rate(node_pressure_cpu_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "CPU some", + "range": true, + "refId": "CPU some", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "rate(node_pressure_memory_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Memory some", + "range": true, + "refId": "Memory some", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "rate(node_pressure_memory_stalled_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Memory full", + "range": true, + "refId": "Memory full", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "rate(node_pressure_io_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "I/O some", + "range": true, + "refId": "I/O some", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "rate(node_pressure_io_stalled_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "I/O full", + "range": true, + "refId": "I/O full", + "step": 240 + } + ], + "title": "Pressure Stall Information", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Enable with --collector.interrupts argument on node-exporter", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Critical*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 46 + }, + "id": 259, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_interrupts_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ type }} - {{ info }}", + "refId": "A", + "step": 240 + } + ], + "title": "Interrupts Detail", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 46 + }, + "id": 306, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_schedstat_timeslices_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "refId": "A", + "step": 240 + } + ], + "title": "Schedule timeslices executed by each cpu", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 56 + }, + "id": 151, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_entropy_available_bits{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Entropy available to random number generators", + "refId": "A", + "step": 240 + } + ], + "title": "Entropy", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "seconds", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 56 + }, + "id": 308, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(process_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Time spent", + "refId": "A", + "step": 240 + } + ], + "title": "CPU time spent in user and system contexts", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 66 + }, + "id": 64, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "process_max_fds{instance=\"$node\",job=\"$job\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum open file descriptors", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "process_open_fds{instance=\"$node\",job=\"$job\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Open file descriptors", + "refId": "B", + "step": 240 + } + ], + "title": "File Descriptors", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "System Misc", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 304, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "temperature", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Critical*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 59 + }, + "id": 158, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_hwmon_temp_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ chip_name }} {{ sensor }} temp", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_hwmon_temp_crit_alarm_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ chip_name }} {{ sensor }} Critical Alarm", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_hwmon_temp_crit_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ chip_name }} {{ sensor }} Critical", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_hwmon_temp_crit_hyst_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ chip_name }} {{ sensor }} Critical Historical", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_hwmon_temp_max_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ chip_name }} {{ sensor }} Max", + "refId": "E", + "step": 240 + } + ], + "title": "Hardware temperature monitor", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 59 + }, + "id": 300, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_cooling_device_cur_state{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Current {{ name }} in {{ type }}", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_cooling_device_max_state{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Max {{ name }} in {{ type }}", + "refId": "B", + "step": 240 + } + ], + "title": "Throttle cooling device", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 69 + }, + "id": 302, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_power_supply_online{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ power_supply }} online", + "refId": "A", + "step": 240 + } + ], + "title": "Power supply", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Hardware Misc", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 27 + }, + "id": 296, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 46 + }, + "id": 297, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_systemd_socket_accepted_connections_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ name }} Connections", + "refId": "A", + "step": 240 + } + ], + "title": "Systemd Sockets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Failed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FF9830", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#73BF69", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Deactivating" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FFCB7D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Activating" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C8F2C2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 46 + }, + "id": 298, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"activating\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Activating", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"active\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Active", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"deactivating\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Deactivating", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"failed\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Failed", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"inactive\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Inactive", + "refId": "E", + "step": 240 + } + ], + "title": "Systemd Units State", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Systemd", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 270, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The number (after merges) of I/O requests completed per second for the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "IO read (-) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 47 + }, + "id": 9, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "intervalFactor": 4, + "legendFormat": "{{device}} - Reads completed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "intervalFactor": 1, + "legendFormat": "{{device}} - Writes completed", + "refId": "B", + "step": 240 + } + ], + "title": "Disk IOps Completed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The number of bytes read from or written to the device per second", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes read (-) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 47 + }, + "id": 33, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_read_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "{{device}} - Read bytes", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_written_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Written bytes", + "refId": "B", + "step": 240 + } + ], + "title": "Disk R/W Data", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The average time for requests issued to the device to be served. This includes the time spent by the requests in queue and the time spent servicing them.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "time. read (-) / write (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 30, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 57 + }, + "id": 37, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_read_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]) / irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - Read wait time avg", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_write_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]) / irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Write wait time avg", + "refId": "B", + "step": 240 + } + ], + "title": "Disk Average Wait Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The average queue length of the requests that were issued to the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "aqu-sz", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 57 + }, + "id": 35, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_io_time_weighted_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}}", + "refId": "A", + "step": 240 + } + ], + "title": "Average Queue Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The number of read and write requests merged per second that were queued to the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "I/Os", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 67 + }, + "id": 133, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_reads_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "intervalFactor": 1, + "legendFormat": "{{device}} - Read merged", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_writes_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "intervalFactor": 1, + "legendFormat": "{{device}} - Write merged", + "refId": "B", + "step": 240 + } + ], + "title": "Disk R/W Merged", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Percentage of elapsed time during which I/O requests were issued to the device (bandwidth utilization for the device). Device saturation occurs when this value is close to 100% for devices serving requests serially. But for devices serving requests in parallel, such as RAID arrays and modern SSDs, this number does not reflect their performance limits.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "%util", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 30, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 67 + }, + "id": 36, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_io_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - IO", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_discard_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - discard", + "refId": "B", + "step": 240 + } + ], + "title": "Time Spent Doing I/Os", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The number of outstanding requests at the instant the sample was taken. Incremented as requests are given to appropriate struct request_queue and decremented as they finish.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Outstanding req.", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 77 + }, + "id": 34, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_disk_io_now{instance=\"$node\",job=\"$job\"}", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - IO now", + "refId": "A", + "step": 240 + } + ], + "title": "Instantaneous Queue Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "IOs", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 77 + }, + "id": 301, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_discards_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - Discards completed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_discards_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Discards merged", + "refId": "B", + "step": 240 + } + ], + "title": "Disk IOps Discards completed / merged", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Storage Disk", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 29 + }, + "id": 271, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 62 + }, + "id": 43, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Available", + "metric": "", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_free_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Free", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Size", + "refId": "C", + "step": 240 + } + ], + "title": "Filesystem space available", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "file nodes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 62 + }, + "id": 41, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_files_free{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Free file nodes", + "refId": "A", + "step": 240 + } + ], + "title": "File Nodes Free", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "files", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 72 + }, + "id": 28, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filefd_maximum{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Max open files", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filefd_allocated{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Open files", + "refId": "B", + "step": 240 + } + ], + "title": "File Descriptor", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "file Nodes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 72 + }, + "id": 219, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_files{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - File nodes total", + "refId": "A", + "step": 240 + } + ], + "title": "File Nodes Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "/ ReadOnly" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 82 + }, + "id": 44, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_readonly{instance=\"$node\",job=\"$job\",device!~'rootfs'}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - ReadOnly", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_device_error{instance=\"$node\",job=\"$job\",device!~'rootfs',fstype!~'tmpfs'}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Device error", + "refId": "B", + "step": 240 + } + ], + "title": "Filesystem in ReadOnly / Error", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Storage Filesystem", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 272, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "packets out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "receive_packets_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "receive_packets_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "transmit_packets_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "transmit_packets_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 47 + }, + "id": 60, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_packets_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_packets_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic by Packets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "packets out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 47 + }, + "id": 142, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_errs_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive errors", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_errs_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit errors", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "packets out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 57 + }, + "id": 143, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_drop_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive drop", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_drop_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit drop", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Drop", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "packets out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 57 + }, + "id": 141, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_compressed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive compressed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_compressed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit compressed", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Compressed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "packets out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 67 + }, + "id": 146, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_multicast_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive multicast", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Multicast", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "packets out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 67 + }, + "id": 144, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_fifo_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive fifo", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_fifo_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit fifo", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Fifo", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "packets out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 77 + }, + "id": 145, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_frame_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive frame", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Frame", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 77 + }, + "id": 231, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_carrier_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Statistic transmit_carrier", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Carrier", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 87 + }, + "id": 232, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_colls_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit colls", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Colls", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "entries", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NF conntrack limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 87 + }, + "id": 61, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_nf_conntrack_entries{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "NF conntrack entries", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_nf_conntrack_entries_limit{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "NF conntrack limit", + "refId": "B", + "step": 240 + } + ], + "title": "NF Conntrack", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Entries", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 97 + }, + "id": 230, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_arp_entries{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - ARP entries", + "refId": "A", + "step": 240 + } + ], + "title": "ARP Entries", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 97 + }, + "id": 288, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_network_mtu_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - Bytes", + "refId": "A", + "step": 240 + } + ], + "title": "MTU", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 107 + }, + "id": 280, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_network_speed_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - Speed", + "refId": "A", + "step": 240 + } + ], + "title": "Speed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "packets", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 107 + }, + "id": 289, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_network_transmit_queue_length{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - Interface transmit queue length", + "refId": "A", + "step": 240 + } + ], + "title": "Queue Length", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "packetes drop (-) / process (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Dropped.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 117 + }, + "id": 290, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_softnet_processed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{cpu}} - Processed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_softnet_dropped_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{cpu}} - Dropped", + "refId": "B", + "step": 240 + } + ], + "title": "Softnet Packets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 117 + }, + "id": 310, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_softnet_times_squeezed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{cpu}} - Squeezed", + "refId": "A", + "step": 240 + } + ], + "title": "Softnet Out of Quota", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 127 + }, + "id": 309, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_network_up{operstate=\"up\",instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{interface}} - Operational state UP", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_network_carrier{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{device}} - Physical link state", + "refId": "B" + } + ], + "title": "Network Operational Status", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Network Traffic", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 273, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 63, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_alloc{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_alloc - Allocated sockets", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_inuse - Tcp sockets currently in use", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_mem{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_mem - Used memory for tcp", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_orphan{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_orphan - Orphan sockets", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_tw{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_tw - Sockets waiting close", + "refId": "E", + "step": 240 + } + ], + "title": "Sockstat TCP", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 124, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_UDPLITE_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "UDPLITE_inuse - Udplite sockets currently in use", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_UDP_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "UDP_inuse - Udp sockets currently in use", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_UDP_mem{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "UDP_mem - Used memory for udp", + "refId": "C", + "step": 240 + } + ], + "title": "Sockstat UDP", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 58 + }, + "id": 125, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_FRAG_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "FRAG_inuse - Frag sockets currently in use", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_RAW_inuse{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "RAW_inuse - Raw sockets currently in use", + "refId": "C", + "step": 240 + } + ], + "title": "Sockstat FRAG / RAW", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 58 + }, + "id": 220, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_mem_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "mem_bytes - TCP sockets in that state", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_UDP_mem_bytes{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "mem_bytes - UDP sockets in that state", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_FRAG_memory{instance=\"$node\",job=\"$job\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "FRAG_memory - Used memory for frag", + "refId": "C" + } + ], + "title": "Sockstat Memory Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "sockets", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 68 + }, + "id": 126, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_sockets_used{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Sockets_used - Sockets currently in use", + "refId": "A", + "step": 240 + } + ], + "title": "Sockstat Used", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Network Sockstat", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 274, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "octets out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 49 + }, + "id": 221, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_IpExt_InOctets{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InOctets - Received octets", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_IpExt_OutOctets{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "OutOctets - Sent octets", + "refId": "B", + "step": 240 + } + ], + "title": "Netstat IP In / Out Octets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 49 + }, + "id": 81, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Ip_Forwarding{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Forwarding - IP forwarding", + "refId": "A", + "step": 240 + } + ], + "title": "Netstat IP Forwarding", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "messages out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 59 + }, + "id": 115, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Icmp_InMsgs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InMsgs - Messages which the entity received. Note that this counter includes all those counted by icmpInErrors", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Icmp_OutMsgs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "OutMsgs - Messages which this entity attempted to send. Note that this counter includes all those counted by icmpOutErrors", + "refId": "B", + "step": 240 + } + ], + "title": "ICMP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "messages out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 59 + }, + "id": 50, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Icmp_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InErrors - Messages which the entity received but determined as having ICMP-specific errors (bad ICMP checksums, bad length, etc.)", + "refId": "A", + "step": 240 + } + ], + "title": "ICMP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Snd.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 69 + }, + "id": 55, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Udp_InDatagrams{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InDatagrams - Datagrams received", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Udp_OutDatagrams{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "OutDatagrams - Datagrams sent", + "refId": "B", + "step": 240 + } + ], + "title": "UDP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 69 + }, + "id": 109, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Udp_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InErrors - UDP Datagrams that could not be delivered to an application", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Udp_NoPorts{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "NoPorts - UDP Datagrams received on a port with no listener", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_UdpLite_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "InErrors Lite - UDPLite Datagrams that could not be delivered to an application", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Udp_RcvbufErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "RcvbufErrors - UDP buffer errors received", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Udp_SndbufErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "SndbufErrors - UDP buffer errors send", + "refId": "E", + "step": 240 + } + ], + "title": "UDP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Snd.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 79 + }, + "id": 299, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Tcp_InSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "InSegs - Segments received, including those received in error. This count includes segments received on currently established connections", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Tcp_OutSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "OutSegs - Segments sent, including those on current connections but excluding those containing only retransmitted octets", + "refId": "B", + "step": 240 + } + ], + "title": "TCP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 79 + }, + "id": 104, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_TcpExt_ListenOverflows{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "ListenOverflows - Times the listen queue of a socket overflowed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_TcpExt_ListenDrops{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "ListenDrops - SYNs to LISTEN sockets ignored", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_TcpExt_TCPSynRetrans{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCPSynRetrans - SYN-SYN/ACK retransmits to break down retransmissions in SYN, fast/timeout retransmits", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Tcp_RetransSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "RetransSegs - Segments retransmitted - that is, the number of TCP segments transmitted containing one or more previously transmitted octets", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Tcp_InErrs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "InErrs - Segments received in error (e.g., bad TCP checksums)", + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Tcp_OutRsts{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "interval": "", + "legendFormat": "OutRsts - Segments sent with RST flag", + "refId": "F" + } + ], + "title": "TCP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "connections", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*MaxConn *./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 89 + }, + "id": 85, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_netstat_Tcp_CurrEstab{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "CurrEstab - TCP connections for which the current state is either ESTABLISHED or CLOSE- WAIT", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_netstat_Tcp_MaxConn{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "MaxConn - Limit on the total number of TCP connections the entity can support (Dynamic is \"-1\")", + "refId": "B", + "step": 240 + } + ], + "title": "TCP Connections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Sent.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 89 + }, + "id": 91, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_TcpExt_SyncookiesFailed{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "SyncookiesFailed - Invalid SYN cookies received", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_TcpExt_SyncookiesRecv{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "SyncookiesRecv - SYN cookies received", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_TcpExt_SyncookiesSent{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "SyncookiesSent - SYN cookies sent", + "refId": "C", + "step": 240 + } + ], + "title": "TCP SynCookie", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "connections", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 99 + }, + "id": 82, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Tcp_ActiveOpens{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "ActiveOpens - TCP connections that have made a direct transition to the SYN-SENT state from the CLOSED state", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Tcp_PassiveOpens{instance=\"$node\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "PassiveOpens - TCP connections that have made a direct transition to the SYN-RCVD state from the LISTEN state", + "refId": "B", + "step": 240 + } + ], + "title": "TCP Direct Transition", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Enable with --collector.tcpstat argument on node-exporter", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "connections", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 99 + }, + "id": 320, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_tcp_connection_states{state=\"established\",instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "established - TCP sockets in established state", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_tcp_connection_states{state=\"fin_wait2\",instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "fin_wait2 - TCP sockets in fin_wait2 state", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_tcp_connection_states{state=\"listen\",instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "listen - TCP sockets in listen state", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_tcp_connection_states{state=\"time_wait\",instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "time_wait - TCP sockets in time_wait state", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "TCP Stat", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Network Netstat", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 33 + }, + "id": 279, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "seconds", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 66 + }, + "id": 40, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_scrape_collector_duration_seconds{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{collector}} - Scrape duration", + "refId": "A", + "step": 240 + } + ], + "title": "Node Exporter Scrape Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*error.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + }, + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 66 + }, + "id": 157, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_scrape_collector_success{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{collector}} - Scrape success", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_textfile_scrape_error{instance=\"$node\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{collector}} - Scrape textfile error (1 = true)", + "refId": "B", + "step": 240 + } + ], + "title": "Node Exporter Scrape", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "000000001" + }, + "refId": "A" + } + ], + "title": "Node Exporter", + "type": "row" + } + ], + "refresh": "1m", + "revision": 1, + "schemaVersion": 38, + "style": "dark", + "tags": [ + "linux" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "default", + "value": "default" + }, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Job", + "multi": false, + "name": "job", + "options": [], + "query": { + "query": "label_values(node_uname_info, job)", + "refId": "Prometheus-job-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(node_uname_info{job=\"$job\"}, instance)", + "hide": 0, + "includeAll": false, + "label": "Host", + "multi": false, + "name": "node", + "options": [], + "query": { + "query": "label_values(node_uname_info{job=\"$job\"}, instance)", + "refId": "Prometheus-node-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", + "value": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+" + }, + "hide": 2, + "includeAll": false, + "multi": false, + "name": "diskdevices", + "options": [ + { + "selected": true, + "text": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", + "value": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+" + } + ], + "query": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Node Exporter Full", + "uid": "rYdddlPWk", + "version": 90, + "weekStart": "" +} diff --git a/grafana/dashboards/postgresql-exporter.json b/grafana/dashboards/postgresql-exporter.json new file mode 100644 index 0000000..c6dc85d --- /dev/null +++ b/grafana/dashboards/postgresql-exporter.json @@ -0,0 +1,3628 @@ +{ + "__inputs": [ + { + "name": "DS_OBSERVE-PROMETHEUS02", + "label": "prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "7.0.3" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "singlestat", + "name": "Singlestat", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": 12485, + "graphTooltip": 0, + "id": null, + "iteration": 1592399036513, + "links": [], + "panels": [ + { + "collapsed": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 18, + "panels": [], + "title": "Global Statistics", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Source: server_version_num", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 0, + "y": 1 + }, + "id": 11, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "max(pg_settings_server_version_num)", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "PostgreSQL Version", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Max Replication lag behind master in seconds\n\nOnly available on a standby system.\n\nSource: pg_last_xact_replay_timestamp\n\nUse: pg_stat_replication for Details.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "s", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 4, + "y": 1 + }, + "id": 84, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "max(pg_replication_lag{instance=\"$Instance\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Max Replication Lag", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "max" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Clients executing Statements.\n\nSource: pg_stat_activity", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 8, + "y": 1 + }, + "id": 23, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(pg_stat_activity_count{state=\"active\",instance=\"$Instance\"})", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Active clients", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Shared buffer hits vs reads from disc", + "fieldConfig": { + "defaults": { + "custom": {}, + "decimals": 2, + "mappings": [ + { + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "semi-dark-red", + "value": null + }, + { + "color": "semi-dark-yellow", + "value": 80 + }, + { + "color": "semi-dark-green", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 1 + }, + "id": 16, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "sum(pg_stat_database_blks_hit{instance=~\"$Instance\"})/(sum(pg_stat_database_blks_hit{instance=~\"$Instance\"})+sum(pg_stat_database_blks_read{instance=~\"$Instance\"}))*100", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Shared Buffer Hits", + "type": "gauge" + }, + { + "cacheTimeout": null, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Percentage of max_connections used", + "fieldConfig": { + "defaults": { + "custom": {}, + "decimals": 0, + "mappings": [ + { + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "semi-dark-green", + "value": null + }, + { + "color": "semi-dark-yellow", + "value": 0.75 + }, + { + "color": "semi-dark-red", + "value": 0.9 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 16, + "y": 1 + }, + "id": 9, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "sum(pg_stat_database_numbackends)/max(pg_settings_max_connections)", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Connections used", + "type": "gauge" + }, + { + "cacheTimeout": null, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Transaction committed vs rollbacked", + "fieldConfig": { + "defaults": { + "custom": {}, + "decimals": 2, + "mappings": [ + { + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "semi-dark-red", + "value": null + }, + { + "color": "#EAB839", + "value": 0.75 + }, + { + "color": "semi-dark-green", + "value": 0.9 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 20, + "y": 1 + }, + "id": 15, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "sum(pg_stat_database_xact_commit{instance=\"$Instance\"})/(sum(pg_stat_database_xact_commit{instance=\"$Instance\"}) + sum(pg_stat_database_xact_rollback{instance=\"$Instance\"}))", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Commit Ratio", + "type": "gauge" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "decimals": 1, + "description": "Source: pg_postmaster_start_time()", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "s", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 0, + "y": 4 + }, + "id": 57, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false, + "ymax": null, + "ymin": null + }, + "tableColumn": "{instance=\"db01vp-nbg6a.obs.noris.net:9187\", job=\"postgres-exporter\", server=\"213.95.132.73:5432\"}", + "targets": [ + { + "expr": "time()-(pg_postmaster_start_time_seconds{instance=\"$Instance\"})", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "PostgreSQL Uptime", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Transactions committed + roolback per minute\n\nSource: pg_stat_database,xact_commit + xact_rollback", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 4, + "y": 4 + }, + "id": 14, + "interval": "", + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum((rate(pg_stat_database_xact_commit{instance=\"$Instance\"}[$Interval])))+sum((rate(pg_stat_database_xact_rollback{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Transaction rate", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Statements executed per Minute.\n\nSource: pg_stat_statements.calls", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 8, + "y": 4 + }, + "id": 93, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum((rate(pg_stat_statements_calls{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Query rate", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "decimals": 2, + "description": "Size of all databases in $Instance.\n\nSource: pg_database_size()", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "bytes", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 0, + "y": 7 + }, + "id": 37, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(pg_database_size{instance=\"$Instance\"})", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Total database size", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Source: pg_stat_statements.total_time / pg_stat_statements.calls", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "s", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 8, + "y": 7 + }, + "id": 102, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum((delta(pg_stat_statements_total_time_seconds{instance=\"$Instance\"}[$Interval])))/sum((delta(pg_stat_statements_calls{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Average query runtime", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "decimals": 0, + "description": "Setting: shared_buffers in PostgreSQL.conf", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "bytes", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 12, + "y": 7 + }, + "id": 66, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false, + "ymax": null, + "ymin": null + }, + "tableColumn": "pg_settings_shared_buffers_bytes{instance=\"db01vp-nbg6a.obs.noris.net:9187\", job=\"postgres-exporter\", server=\"213.95.132.73:5432\"}", + "targets": [ + { + "expr": "pg_settings_shared_buffers_bytes{instance=\"$Instance\"}", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Shared Buffers", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Setting: max_connections in postgreSQL.conf", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 16, + "y": 7 + }, + "id": 75, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false, + "ymax": null, + "ymin": null + }, + "tableColumn": "pg_settings_max_connections{instance=\"db01vp-nbg6a.obs.noris.net:9187\", job=\"postgres-exporter\", server=\"213.95.132.73:5432\"}", + "targets": [ + { + "expr": "pg_settings_max_connections{instance=\"$Instance\"}", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Max Connections", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "decimals": 0, + "description": "View: pg_stat_activity", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 10 + }, + "hiddenSeries": false, + "id": 24, + "interval": "$Interval", + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [ + { + "targetBlank": true, + "title": "PostgreSQL Documentation", + "url": "https://www.postgresql.org/docs/current/monitoring-stats.html" + } + ], + "nullPointMode": "null as zero", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (state) (pg_stat_activity_count{instance=\"$Instance\"})", + "legendFormat": "{{state}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Connections by state (stacked)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "decimals": 0, + "description": "View: pg_stat_activity", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 10 + }, + "hiddenSeries": false, + "id": 28, + "interval": "$Interval", + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [ + { + "targetBlank": true, + "title": "PostgreSQL Documentation", + "url": "https://www.postgresql.org/docs/current/monitoring-stats.html" + } + ], + "nullPointMode": "null as zero", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (datname) (pg_stat_activity_count{instance=\"$Instance\"})", + "legendFormat": "{{datname}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Connections by database (stacked)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "decimals": 2, + "description": "1 Minute rate of transactions committed or rollback.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 19 + }, + "hiddenSeries": false, + "id": 12, + "interval": "", + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum ((rate(pg_stat_database_xact_commit[$Interval])))", + "interval": "", + "legendFormat": "committed", + "refId": "A" + }, + { + "expr": "sum ((rate(pg_stat_database_xact_rollback[$Interval])))", + "hide": false, + "interval": "", + "legendFormat": "rollback", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Transactions", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Source: pg_stat_database", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 19 + }, + "hiddenSeries": false, + "id": 27, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideZero": false, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum((rate(pg_stat_database_tup_inserted{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "Inserts", + "refId": "A" + }, + { + "expr": "sum((rate(pg_stat_database_tup_updated{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "Updates", + "refId": "B" + }, + { + "expr": "sum((rate(pg_stat_database_tup_deleted{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "Deletes", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Tuples inserts/updates/deletes", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "* blk_read_time: Time spent reading data file blocks by backends in this database, in milliseconds\n* blk_write_time: Time spent writing data file blocks by backends in this database, in milliseconds\n\ntrack_io_timings needs to be activated", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 28 + }, + "hiddenSeries": false, + "id": 26, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum ((rate(pg_stat_database_blk_read_time{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "blk_read_time", + "refId": "A" + }, + { + "expr": "sum ((rate(pg_stat_database_blk_write_time{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "blk_read_time", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "I/O Read/Write time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Source: pg_stat_database\n\n* tup_fetched: rows needed to satisfy queries\n* tup_returned: rows read/scanned", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 28 + }, + "hiddenSeries": false, + "id": 111, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideZero": false, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum((rate(pg_stat_database_tup_fetched{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "Fetched", + "refId": "A" + }, + { + "expr": "sum((rate(pg_stat_database_tup_returned{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "Returned", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Tuples fetched/returned", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Source: pg_locks", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 37 + }, + "hiddenSeries": false, + "id": 29, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [ + { + "title": "PostgreSQL Lock Modes", + "url": "https://www.postgresql.org/docs/12/explicit-locking.html#LOCKING-TABLES" + } + ], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (mode) (pg_locks_count{instance=\"$Instance\"})", + "legendFormat": "{{mode}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Locks by state", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Should be 0 \n\nSource: pg_stat_database\n\nWith log_lock_waits turned on, deadlocks will be logged to the PostgreSQL Logfiles.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 37 + }, + "hiddenSeries": false, + "id": 30, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [ + { + "title": "PostgreSQL Locking", + "url": "https://www.postgresql.org/docs/12/explicit-locking.html" + } + ], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (datname) ((rate(pg_stat_database_deadlocks{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "{{datname}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Deadlocks by database", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Should be 0. If temporary files are created, it can indicate insufficient work_mem. With log_temp_files the creation of temporary files are logged to the PostgreSQL Logfiles.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 42 + }, + "hiddenSeries": false, + "id": 31, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [ + { + "title": "PostgreSQL Resources", + "url": "https://www.postgresql.org/docs/current/runtime-config-resource.html" + } + ], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (datname) ((rate(pg_stat_database_temp_files{instance=\"$Instance\"}[$Interval])))", + "interval": "", + "legendFormat": "{{datname}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Temporary files by database", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Lag behind master in seconds.\n\nOnly available on a standby System.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 47 + }, + "hiddenSeries": false, + "id": 120, + "interval": "1m", + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "max(pg_replication_lag{instance=\"$Instance\"})", + "instant": false, + "intervalFactor": 1, + "legendFormat": "lag ", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Replication lag ", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": true, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 57 + }, + "id": 20, + "panels": [ + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 0, + "y": 2 + }, + "id": 46, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true, + "ymax": null, + "ymin": null + }, + "tableColumn": "pg_stat_activity_count{datname=\"grafana\", instance=\"db01vp-nbg6a.obs.noris.net:9187\", job=\"postgres-exporter\", server=\"213.95.132.73:5432\", state=\"active\"}", + "targets": [ + { + "expr": "pg_stat_activity_count{state=\"active\",instance=\"$Instance\",datname=~\"$Database\"}", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Active clients", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "decimals": 2, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "bytes", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 4, + "y": 2 + }, + "id": 43, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true, + "ymax": null, + "ymin": null + }, + "tableColumn": "pg_database_size{datname=\"grafana\", instance=\"db01vp-nbg6a.obs.noris.net:9187\", job=\"postgres-exporter\", server=\"213.95.132.73:5432\"}", + "targets": [ + { + "expr": "pg_database_size{instance=\"$Instance\",datname=\"$Database\"}", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Database size", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Shared buffer hits vs reads from disc", + "fieldConfig": { + "defaults": { + "custom": {}, + "decimals": 2, + "mappings": [ + { + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "semi-dark-red", + "value": null + }, + { + "color": "semi-dark-yellow", + "value": 0.8 + }, + { + "color": "semi-dark-green", + "value": 0.9 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 2 + }, + "id": 44, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "7.0.3", + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "targets": [ + { + "expr": "pg_stat_database_blks_hit{instance=~\"$Instance\",datname=~\"$Database\"}/(pg_stat_database_blks_hit{instance=~\"$Instance\",datname=~\"$Database\"}+pg_stat_database_blks_read{instance=~\"$Instance\",datname=~\"$Database\"})", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Shared Buffer Hits", + "type": "gauge" + }, + { + "cacheTimeout": null, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Transaction committed vs rollbacked", + "fieldConfig": { + "defaults": { + "custom": {}, + "decimals": 2, + "mappings": [ + { + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "semi-dark-red", + "value": null + }, + { + "color": "#EAB839", + "value": 0.75 + }, + { + "color": "semi-dark-green", + "value": 0.9 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 2 + }, + "id": 45, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "7.0.3", + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "targets": [ + { + "expr": "pg_stat_database_xact_commit{instance=\"$Instance\",datname=~\"$Database\"}/(pg_stat_database_xact_commit{instance=\"$Instance\",datname=~\"$Database\"} + pg_stat_database_xact_rollback{instance=\"$Instance\",datname=~\"$Database\"})", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Commit Ratio", + "type": "gauge" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Transactions committed + roolback per minute\n\nSource: pg_stat_database,xact_commit + xact_rollback", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 0, + "y": 5 + }, + "id": 122, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum((rate(pg_stat_database_xact_commit{instance=\"$Instance\",datname=\"$Database\"}[$Interval])))+sum((rate(pg_stat_database_xact_rollback{instance=\"$Instance\",datname=\"$Database\"}[$Interval])))", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Transaction rate", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "decimals": 0, + "description": "View: pg_stat_activity", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 8 + }, + "hiddenSeries": false, + "id": 47, + "interval": "$Interval", + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [ + { + "targetBlank": true, + "title": "PostgreSQL Documentation", + "url": "https://www.postgresql.org/docs/current/monitoring-stats.html" + } + ], + "nullPointMode": "null as zero", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (state) (pg_stat_activity_count{instance=\"$Instance\",datname=\"$Database\"})", + "legendFormat": "{{state}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Connections by state (stacked)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "decimals": 2, + "description": "1 Minute rate of transactions committed or rollback.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 8 + }, + "hiddenSeries": false, + "id": 48, + "interval": "", + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(pg_stat_database_xact_commit{instance=\"$Instance\",datname=\"$Database\"}[$Interval])", + "interval": "", + "legendFormat": "committed", + "refId": "A" + }, + { + "expr": "rate(pg_stat_database_xact_rollback{instance=\"$Instance\",datname=\"$Database\"}[$Interval])", + "hide": false, + "interval": "", + "legendFormat": "rollback", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Transactions", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Source: pg_stat_database", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 17 + }, + "hiddenSeries": false, + "id": 124, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideZero": false, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(pg_stat_database_tup_inserted{instance=\"$Instance\",datname=\"$Database\"}[$Interval])", + "interval": "", + "legendFormat": "Inserts", + "refId": "A" + }, + { + "expr": "rate(pg_stat_database_tup_updated{instance=\"$Instance\",datname=\"$Database\"}[$Interval])", + "interval": "", + "legendFormat": "Updates", + "refId": "B" + }, + { + "expr": "rate(pg_stat_database_tup_deleted{instance=\"$Instance\",datname=\"$Database\"}[$Interval])", + "interval": "", + "legendFormat": "Deletes", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Tuples inserts/updates/deletes", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Source: pg_stat_database\n\n* tup_fetched: rows needed to satisfy queries\n* tup_returned: rows read/scanned", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 17 + }, + "hiddenSeries": false, + "id": 126, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideZero": false, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(pg_stat_database_tup_fetched{instance=\"$Instance\",datname=\"$Database\"}[$Interval])", + "interval": "", + "legendFormat": "Fetched", + "refId": "A" + }, + { + "expr": "rate(pg_stat_database_tup_returned{instance=\"$Instance\",datname=\"$Database\"}[$Interval])", + "interval": "", + "legendFormat": "Returned", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Tuples fetched/returned", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Source: pg_locks", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 26 + }, + "hiddenSeries": false, + "id": 128, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [ + { + "title": "PostgreSQL Lock Modes", + "url": "https://www.postgresql.org/docs/12/explicit-locking.html#LOCKING-TABLES" + } + ], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (mode) (pg_locks_count{instance=\"$Instance\",datname=\"$Database\"})", + "legendFormat": "{{mode}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Locks by state", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Should be 0 \n\nSource: pg_stat_database\n\nWith log_lock_waits turned on, deadlocks will be logged to the PostgreSQL Logfiles.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 26 + }, + "hiddenSeries": false, + "id": 130, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [ + { + "title": "PostgreSQL Locking", + "url": "https://www.postgresql.org/docs/12/explicit-locking.html" + } + ], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(pg_stat_database_deadlocks{instance=\"$Instance\",datname=\"$Database\"}[$Interval])", + "interval": "", + "legendFormat": "deadlocks", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Deadlocks by database", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "description": "Should be 0. If temporary files are created, it can indicate insufficient work_mem. With log_temp_files the creation of temporary files are logged to the PostgreSQL Logfiles.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 31 + }, + "hiddenSeries": false, + "id": 132, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [ + { + "title": "PostgreSQL Resources", + "url": "https://www.postgresql.org/docs/current/runtime-config-resource.html" + } + ], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "Database": { + "selected": false, + "text": "grafana", + "value": "grafana" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(pg_stat_database_temp_files{instance=\"$Instance\",datname=\"$Database\"}[$Interval])", + "interval": "", + "legendFormat": "temp_files", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Temporary files by database", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "repeat": "Database", + "title": "Database: $Database", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 25, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": {}, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "definition": "label_values({job=\"postgres-exporter\"}, instance)", + "hide": 0, + "includeAll": false, + "label": null, + "multi": false, + "name": "Instance", + "options": [], + "query": "label_values({job=\"postgres-exporter\"}, instance)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": {}, + "datasource": "${DS_OBSERVE-PROMETHEUS02}", + "definition": "label_values(datname)", + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "Database", + "options": [], + "query": "label_values(datname)", + "refresh": 1, + "regex": "/^(?!template*|postgres).*$/", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": true, + "auto_count": 30, + "auto_min": "10s", + "current": { + "selected": false, + "text": "10m", + "value": "10m" + }, + "hide": 0, + "label": null, + "name": "Interval", + "options": [ + { + "selected": false, + "text": "auto", + "value": "$__auto_interval_Interval" + }, + { + "selected": false, + "text": "30sec", + "value": "30sec" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": true, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + } + ], + "query": "30sec,1m,10m,30m,1h,6h,12h,1d", + "queryValue": "", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "PostgreSQL Exporter", + "uid": "v5ciIbUZz", + "version": 2, + "description": "postgres exporter Dashboard for display important postgreSQL metric" +} diff --git a/prometheus/prometheus-dev.yml b/prometheus/prometheus-dev.yml index ffd18e8..bbaede5 100644 --- a/prometheus/prometheus-dev.yml +++ b/prometheus/prometheus-dev.yml @@ -20,7 +20,7 @@ scrape_configs: - job_name: node-exporter static_configs: - targets: - - telespor-cloud-node-exporter:9100 + - node-exporter:9100 # Monitor mysqldb metrics - job_name: geospatial-exporter @@ -29,7 +29,7 @@ scrape_configs: static_configs: - targets: - - geospatial-db-exporter:9187 + - geospatial-data-exporter:9187 - job_name: kafka-exporter scrape_interval: 5s From fc2511aa0eb2f277607cddcd7daa5c3fc10e7a39 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 18 Jul 2025 11:37:14 +0200 Subject: [PATCH 10/28] feat: restructure the ingest service --- ingest_service/README.md | 55 +++++ ingest_service/main.py | 215 +++--------------- ingest_service/mock_send_ttn.py | 92 -------- ingest_service/models/general.py | 32 +++ ingest_service/models/http.py | 22 ++ ingest_service/models/ttn.py | 45 ++++ ingest_service/util/authentication.py | 18 ++ ingest_service/util/formatter.py | 54 +++++ ingest_service/util/util.py | 13 ++ .../ingest_service/ttn_example_message.json | 6 +- processor_service/main.py | 6 +- rest_api/Makefile | 4 +- 12 files changed, 278 insertions(+), 284 deletions(-) create mode 100644 ingest_service/README.md delete mode 100644 ingest_service/mock_send_ttn.py create mode 100644 ingest_service/models/general.py create mode 100644 ingest_service/models/http.py create mode 100644 ingest_service/models/ttn.py create mode 100644 ingest_service/util/authentication.py create mode 100644 ingest_service/util/formatter.py create mode 100644 ingest_service/util/util.py rename ingest_service/ttn_example.json => mock/ingest_service/ttn_example_message.json (96%) diff --git a/ingest_service/README.md b/ingest_service/README.md new file mode 100644 index 0000000..56a61b7 --- /dev/null +++ b/ingest_service/README.md @@ -0,0 +1,55 @@ +# Ingest Service + +The Ingest Service exposes POST API endpoints for applications to send data to the platform. It is +designed to handle various types of data formats and can be extended to support additional formats +as needed. + +The service is written in FastAPI and uses Pydantic for data validation. Data is then forwarded to +the [processor_service](../processor_service/README.md) for further processing. + +## Usage + +To start the Ingest Service run `make run-local-dev` from this directory. This will create a Python +virtual environment, install the required dependencies, and start the service on +`http://localhost:50005`. + +You can then send data to the service using HTTP POST requests. + +Currently available endpoints include: + +- `/ttn`: For data in the format forwarded by the Things Network (TTN). +- `/http`: For data in a generic HTTP format. + +## Data Format + +The service expects data in a predefined format, which is validated using Pydantic models. The data +structure is defined in the `models` directory. + +Even though the structure is predefined, what is variable is the actual data being sent, which can +differ based on the device and application. + +The data should be part of the `measurement` dictionary, which can contain any number of key-value +pairs representing the actual data being sent. The keys in this dictionary are not fixed and can +vary based on the device and application sending the data. + +### TTN + +The data sent by any TTN formation generally follows the same structure, therefore a Pydantic +validator is in place to ensure that the data conforms to the expected format. See the model in +`ingest_service/models/ttn.py` for details. + +### HTTP + +The `/http` endpoint is meant to be used by any other application that wants to send data to the +Ingest Service in a generic HTTP format. + +The sensor data structure is flexible and can be defined by the application sending the data. The +only requirement is that the data should be part of the `measurement` dictionary. + +See the `ingest_service/models/http.py` for the Pydantic model that validates the data sent to this +endpoint. + +## Mock Data Generation + +The `mock/ingest_service` directory contains scripts to generate mock data that can be ingested by +the service. This is useful for testing and development purposes. diff --git a/ingest_service/main.py b/ingest_service/main.py index c7ed262..0cb6489 100644 --- a/ingest_service/main.py +++ b/ingest_service/main.py @@ -1,45 +1,37 @@ -import json import traceback -from datetime import date, datetime -from typing import Optional from fastapi import FastAPI, Header, HTTPException, Request, Response, status from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from kafka import KafkaProducer -from pydantic import BaseModel, Field - - -def custom_value_serializer(value): - """Custom serializer to handle datetime objects.""" - - def default_serializer(obj): - if isinstance(obj, (date, datetime)): - return obj.isoformat() - raise TypeError(f"Type {type(obj)} not serializable") - - return json.dumps(value, default=default_serializer).encode("utf-8") - +from models.http import DataHTTP + +# Import the Pydantic models for the API requests +from models.ttn import DataTTN +from util.authentication import verify_token +from util.formatter import ( + format_http_data_for_processing, + format_ttn_data_for_processing, +) +from util.util import custom_value_serializer +# Initialize the Kafka producer with a custom serializer producer = KafkaProducer( bootstrap_servers="localhost:9992", key_serializer=str.encode, value_serializer=custom_value_serializer, ) +# Prepare the app app = FastAPI() +# Add custom exception handler to print more information about the 422 error @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Custom exception handler for request validation errors.""" - print(f"Validation error on request: {request.url}") - print("Full validation error details:") - print(exc) - print("Errors:") for err in exc.errors(): print(err) - print("Traceback:") traceback.print_exc() return JSONResponse( @@ -47,180 +39,39 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE ) -API_KEY = "your-secret-api-key" # Replace with your actual key - - -class Position(BaseModel): - """Data model for position information.""" - - latitude: float - longitude: float - altitude: Optional[float] = None - timestamp: Optional[int] = None - source: Optional[str] = None - - -class DataIn(BaseModel): - """Data model for incoming data.""" - - measurement: Optional[dict] = None - position: Optional[Position] = None - - -def add_metadata(data: DataIn, received_from: str): - """The data is routed based on the application name. The application name is added in this service, as it is not part of the data. - - Application data is added based on the endpoint the data is received from. - """ - if received_from: - data.metadata.application = received_from - - -"""Example request which works with route: -curl -X 'POST' http://localhost:50005 -H "Content-Type: application/json" -H "Authorization: Bearer your-secret-api-key" -d '{"measurement": {"water-level": 123, "height": 535, "note": "my-note-is-nice"}, "metadata": {"application": "myApp"}, "position": {"longitude": 14.1451314, "latitude": 97.838824, "altitude": 123.0, "timestamp": 123}}' -""" - - -@app.post("/") -async def ingest_data(data, request: Request, Authorization: str = Header(...)): +@app.post("/http") +async def ingest_data(data: DataHTTP, Authorization: str = Header(...)): """Ingest data from any source. This is a generic endpoint that can be used to ingest data from various sources.""" - print(request) - print(await request.json()) - print(Authorization) - if not Authorization.startswith("Bearer "): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication scheme", - ) - token = Authorization.removeprefix("Bearer ").strip() - if token != API_KEY: + verify_token(Authorization) + + try: + data_to_insert = format_http_data_for_processing(data) + except Exception as e: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key" + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Failed to format data: {str(e)}", ) - # Print the data: - print(f"Data received: {data}") - # Process data here (e.g., store in DB) - return Response(status_code=200) - - -class ApplicationIds(BaseModel): - """Data model for TTN application IDs.""" - - application_id: str + producer.send("process_data", key="http_service", value=data_to_insert.model_dump()) - -class DeviceInfoTTN(BaseModel): - """Data model for TTN device information.""" - - device_id: str - application_ids: ApplicationIds - - -class LocationTTN(BaseModel): - """Data model for TTN location information.""" - - frm_payload: Optional[Position] = Field(alias="frm-payload") - - class Config: - """Configuration for Pydantic model.""" - - validate_by_name = True # Allows using `frm_payload` when creating the object - - -class UplinkMessageTTN(BaseModel): - """Data model for TTN uplink message.""" - - f_port: int - decoded_payload: dict - locations: Optional[LocationTTN] = None - - -class DataTTN(BaseModel): - """Data model for TTN webhook data.""" - - end_device_ids: DeviceInfoTTN - received_at: datetime - uplink_message: UplinkMessageTTN - - -"""The TTN message usually contains the following structure: - -{ - "end_device_ids": { - "device_id": "sp100089", - "application_ids": { - "application_id": "irnas-ranger-test" - }, - "dev_eui": "0016C001F0118501", - "join_eui": "0000000000000000", - "dev_addr": "260BB7AD" - }, - "received_at": "2025-07-16T11:09:19.243513998Z", - "uplink_message": { - "session_key_id": "AZT1Hy95A+QB1gq8y/xU3w==", - "f_port": 14, - "f_cnt": 1256661, - "frm_payload": "lAVCf1YBAA==", - "decoded_payload": { - "n_msg": 87679, - "percentage": 66 - }, - "locations": { - "frm-payload": { - "latitude": 46.5581818, - "longitude": 15.6164958, - "altitude": 335, - "source": "SOURCE_GPS" - } - } - } -} - -We are interested in the fields defined above. -""" + return Response(status_code=200) @app.post("/ttn") async def ingest_data_ttn(data: DataTTN, Authorization: str = Header(...)): """Ingest data from TTN webhook.""" - if not Authorization.startswith("Bearer "): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication scheme", - ) - token = Authorization.removeprefix("Bearer ").strip() - if token != API_KEY: + verify_token(Authorization) + + data_to_insert = None + try: + data_to_insert = format_ttn_data_for_processing(data) + except Exception as e: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key" + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Failed to format TTN data: {str(e)}", ) - data_to_insert = format_ttn_data_for_processing(data) - - producer.send("process_data", key="ttn_service", value=data_to_insert) + producer.send("process_data", key="ttn_service", value=data_to_insert.model_dump()) # Process data here (e.g., store in DB) return Response(status_code=200) - - -def format_ttn_data_for_processing(data: DataTTN): - """We format the data for further processing and storage.""" - # Extract version from decoded_payload if it exists - version = data.uplink_message.decoded_payload.get( - "version", "v0" - ) # Set default to "v0" if not present - - # Remove version from decoded_payload if it exists - if "version" in data.uplink_message.decoded_payload: - del data.uplink_message.decoded_payload["version"] - - out_data = { - "sensor_name": data.end_device_ids.device_id, - "application": data.end_device_ids.application_ids.application_id, - "measurement": data.uplink_message.decoded_payload, - "version": version, - "location": data.uplink_message.locations.frm_payload.model_dump(), - "received_at": data.received_at, - } - - return out_data diff --git a/ingest_service/mock_send_ttn.py b/ingest_service/mock_send_ttn.py deleted file mode 100644 index c084bd8..0000000 --- a/ingest_service/mock_send_ttn.py +++ /dev/null @@ -1,92 +0,0 @@ -import json -import random -import time -from datetime import datetime - -import requests - -url = "http://localhost:50005/ttn" -token = "your-secret-api-key" - -headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} - - -def generate_random_decoded_payload(): - """Generate random decoded payload dict with 0 to 15 values. The keys can be random and the values as well.""" - num_values = random.randint(0, 15) - decoded_payload = {} - - possible_columns = [ - "temperature", - "humidity", - "pressure", - "light", - "motion", - "battery", - "voltage", - "current", - "distance", - "speed", - "acceleration", - "gyroscope", - "magnetometer", - "gps_latitude", - "gps_longitude", - "gps_altitude", - "gps_accuracy", - "gps_speed", - "gps_heading", - "gps_satellites", - "gps_timestamp", - "note", - ] - - for _ in range(num_values): - key = "".join( - random.choice(possible_columns) - ) # Randomly select a key from the list - value = random.randint(-100, 100) # Random integer value - decoded_payload[key] = value - - # Add also a random version from v0 to v10 - version = random.randint(0, 10) - decoded_payload["version"] = f"v{version}" - - return decoded_payload - - -device_ids = [f"sp0000{i:03d}" for i in range(1, 101)] # sp00001 to sp00010 -application_ids = [f"irnas-ranger-test{i}" for i in range(1, 10)] - -while True: - payload = generate_random_decoded_payload() - data = { - "end_device_ids": { - "device_id": random.choice(device_ids), - "application_ids": {"application_id": random.choice(application_ids)}, - "dev_eui": "0000000000000000", - "join_eui": "0000000000000000", - "dev_addr": "00000000", - }, - "received_at": datetime.now().isoformat(), - "uplink_message": { - "session_key_id": "AZT1Hy95A+QB1gq8y/xU3w==", - "f_port": 14, - "f_cnt": 1256661, - "frm_payload": "lAVCf1YBAA==", - "decoded_payload": payload, - "locations": { - "frm-payload": { - "latitude": 46.5581818, - "longitude": 15.6164958, - "altitude": 335, - "source": "SOURCE_GPS", - } - }, - }, - } - serialized_data = json.dumps(data) - response = requests.post(url, headers=headers, json=data) - print(f"Status code: {response.status_code}") - print(f"Response: {response.text}") - time.sleep(0.001) diff --git a/ingest_service/models/general.py b/ingest_service/models/general.py new file mode 100644 index 0000000..b2f3e6b --- /dev/null +++ b/ingest_service/models/general.py @@ -0,0 +1,32 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel + + +class Position(BaseModel): + """Data model for position information.""" + + latitude: float + longitude: float + altitude: Optional[float] = None + timestamp: Optional[int] = None + source: Optional[str] = None + + +class DataOut(BaseModel): + """Data model for outgoing data.""" + + sensor_name: str + application: str + measurement: dict + version: str + location: Optional[Position] = None + received_at: datetime + + class Config: + """Configuration for Pydantic model.""" + + json_encoders = { + Position: lambda v: v.model_dump() if v else None, + } diff --git a/ingest_service/models/http.py b/ingest_service/models/http.py new file mode 100644 index 0000000..944fbbb --- /dev/null +++ b/ingest_service/models/http.py @@ -0,0 +1,22 @@ +from datetime import datetime +from typing import Optional + +from models.general import Position +from pydantic import BaseModel + + +class DataHTTP(BaseModel): + """Data model for incoming HTTP data.""" + + sensor_name: str + application: str + measurement: dict + location: Optional[Position] = None + received_at: datetime + + class Config: + """Configuration for Pydantic model.""" + + json_encoders = { + Position: lambda v: v.model_dump() if v else None, + } diff --git a/ingest_service/models/ttn.py b/ingest_service/models/ttn.py new file mode 100644 index 0000000..80b7d1d --- /dev/null +++ b/ingest_service/models/ttn.py @@ -0,0 +1,45 @@ +from datetime import datetime +from typing import Optional + +from models.general import Position +from pydantic import BaseModel, Field + + +class ApplicationIds(BaseModel): + """Data model for TTN application IDs.""" + + application_id: str + + +class DeviceInfoTTN(BaseModel): + """Data model for TTN device information.""" + + device_id: str + application_ids: ApplicationIds + + +class LocationTTN(BaseModel): + """Data model for TTN location information.""" + + frm_payload: Optional[Position] = Field(alias="frm-payload") + + class Config: + """Configuration for Pydantic model.""" + + validate_by_name = True # Allows using `frm_payload` when creating the object + + +class UplinkMessageTTN(BaseModel): + """Data model for TTN uplink message.""" + + f_port: int + decoded_payload: dict + locations: Optional[LocationTTN] = None + + +class DataTTN(BaseModel): + """Data model for TTN webhook data.""" + + end_device_ids: DeviceInfoTTN + uplink_message: UplinkMessageTTN + received_at: datetime diff --git a/ingest_service/util/authentication.py b/ingest_service/util/authentication.py new file mode 100644 index 0000000..965c0e3 --- /dev/null +++ b/ingest_service/util/authentication.py @@ -0,0 +1,18 @@ +from fastapi import Header, HTTPException, status + +# TODO: Move to constants +API_KEY = "your-secret-api-key" # Replace with your actual key + + +def verify_token(Authorization: str = Header(...)): + """Verify the API key from the Authorization header.""" + if not Authorization.startswith("Bearer "): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication scheme", + ) + token = Authorization.removeprefix("Bearer ").strip() + if token != API_KEY: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key" + ) diff --git a/ingest_service/util/formatter.py b/ingest_service/util/formatter.py new file mode 100644 index 0000000..81d64db --- /dev/null +++ b/ingest_service/util/formatter.py @@ -0,0 +1,54 @@ +from models.general import DataOut +from models.http import DataHTTP +from models.ttn import DataTTN + + +def format_ttn_data_for_processing(data: DataTTN) -> DataOut: + """We format the data for further processing and storage.""" + # Extract version from decoded_payload if it exists + version = data.uplink_message.decoded_payload.get( + "version", "v0" + ) # Set default to "v0" if not present + + # Remove version from decoded_payload if it exists + if "version" in data.uplink_message.decoded_payload: + del data.uplink_message.decoded_payload["version"] + + out_data = { + "sensor_name": data.end_device_ids.device_id, + "application": data.end_device_ids.application_ids.application_id, + "measurement": data.uplink_message.decoded_payload, + "version": version, + "location": data.uplink_message.locations.frm_payload.model_dump(), + "received_at": data.received_at, + } + + try: + return DataOut.model_validate(out_data) + except Exception as e: + raise e + + +def format_http_data_for_processing(data: DataHTTP) -> DataOut: + """We format the received data for further processing and storage.""" + version = data.measurement.get( + "version", "v0" + ) # Set default to "v0" if not present + + # Remove version from decoded_payload if it exists + if "version" in data.measurement: + del data.measurement["version"] + + out_data = { + "sensor_name": data.sensor_name, + "application": data.application, + "measurement": data.measurement, + "version": version, + "location": data.location.model_dump() if data.location else None, + "received_at": data.received_at.isoformat() if data.received_at else None, + } + + try: + return DataOut.model_validate(out_data) + except Exception as e: + raise e diff --git a/ingest_service/util/util.py b/ingest_service/util/util.py new file mode 100644 index 0000000..cc95387 --- /dev/null +++ b/ingest_service/util/util.py @@ -0,0 +1,13 @@ +import json +from datetime import date, datetime + + +def custom_value_serializer(value): + """Custom serializer to handle datetime objects.""" + + def default_serializer(obj): + if isinstance(obj, (date, datetime)): + return obj.isoformat() + raise TypeError(f"Type {type(obj)} not serializable") + + return json.dumps(value, default=default_serializer).encode("utf-8") diff --git a/ingest_service/ttn_example.json b/mock/ingest_service/ttn_example_message.json similarity index 96% rename from ingest_service/ttn_example.json rename to mock/ingest_service/ttn_example_message.json index 9f5de31..6937727 100644 --- a/ingest_service/ttn_example.json +++ b/mock/ingest_service/ttn_example_message.json @@ -1,12 +1,12 @@ { "end_device_ids": { - "device_id": "sp100089", + "device_id": "sp000000", "application_ids": { "application_id": "irnas-ranger-test" }, - "dev_eui": "0016C001F0118501", + "dev_eui": "0000000000000000", "join_eui": "0000000000000000", - "dev_addr": "260BB7AD" + "dev_addr": "00000000" }, "correlation_ids": [ "gs:uplink:01K09ESCSX0Z453H21R0N2WH43" diff --git a/processor_service/main.py b/processor_service/main.py index cfcca65..59e9c1b 100644 --- a/processor_service/main.py +++ b/processor_service/main.py @@ -31,15 +31,11 @@ ) ) - if message.key == b"ttn_service": + if message.key == b"ttn_service" or message.key == b"http_service": byte_string = message.value data_to_insert = json.loads(byte_string.decode("utf-8")) - print(f"Incoming data: {data_to_insert}") insert_data(data_to_insert) - # try: - # except Exception as e: - # logger.error(f"Error inserting data: {e}") consumer.commit_async() diff --git a/rest_api/Makefile b/rest_api/Makefile index 373091a..9a02ec0 100644 --- a/rest_api/Makefile +++ b/rest_api/Makefile @@ -1,10 +1,10 @@ SHELL := /bin/bash clean: - rm -r -f ./common + # rm -r -f ./common install: - cp -r ../common/ ./ + # cp -r ../common/ ./ pip install -r requirements.txt run: clean install From 568649a7548802220760b820f0b04c7186687e29 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 18 Jul 2025 11:37:48 +0200 Subject: [PATCH 11/28] feat: add mock ingest data --- mock/ingest_service/README.md | 36 +++++++ mock/ingest_service/send.py | 188 ++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 mock/ingest_service/README.md create mode 100644 mock/ingest_service/send.py diff --git a/mock/ingest_service/README.md b/mock/ingest_service/README.md new file mode 100644 index 0000000..782c064 --- /dev/null +++ b/mock/ingest_service/README.md @@ -0,0 +1,36 @@ +# Mock data to ingest service + +This directory contains scripts used to generate mock data which is consumed by the ingest service. + +There are two examples available: + +## send.py + +This script mimics the data format forwarded by the Things Network (TTN) to the ingest service. It +generates a specified amount of mock devices with a specified number of mock applications and sends +the specified number of messages. Each message contains a random combination of the +device/application pair. + +To run it use + +```bash +python send.py --devices 10 --applications 5 --messages 100 --type ttn +``` + +The script generates random data for any other type of devices and sends it to the ingest service +via HTTP POST requests. + +To run it use + +```bash +python send.py --devices 10 --applications 5 --messages 100 --type http +``` + +The arguments specified in the command line are: + +- `--devices`: Number of devices to generate. +- `--applications`: Number of applications to generate. +- `--messages`: Number of messages to send. If set to 0 it will send messages indefinitely. +- `--url`: URL of the ingest service to send the data to. Defaults to + `http://localhost:50005/`. +- `--type`: Type of the device, either `ttn` or `http`. diff --git a/mock/ingest_service/send.py b/mock/ingest_service/send.py new file mode 100644 index 0000000..8eed257 --- /dev/null +++ b/mock/ingest_service/send.py @@ -0,0 +1,188 @@ +"""This module is used to send mock data to the locally deployed ingestion service. + +It mocks the data format from TTN (The Things Network) and sends it to the ingestion service running on localhost:50005. +""" + +import argparse +import json +import random +import time +from datetime import datetime + +import requests + +AUTH_TOKEN = "your-secret-api-key" + +headers = {"Authorization": f"Bearer {AUTH_TOKEN}", "Content-Type": "application/json"} + +device_ids = [f"mock-dev-{i:03d}" for i in range(1, 101)] +application_ids = [f"mock-app-{i}" for i in range(1, 10)] + + +def generate_http_data(sensor_name, application): + """Generate mock HTTP data for a given sensor and application.""" + return { + "sensor_name": sensor_name, + "application": application, + "measurement": generate_random_decoded_payload(), + "location": { + "latitude": random.uniform(-90, 90), + "longitude": random.uniform(-180, 180), + "altitude": random.randint(0, 5000), + }, + "received_at": datetime.now().isoformat(), + } + + +def generate_ttn_data(device_id, application_id): + """Generate mock TTN data for a given device and application.""" + return { + "end_device_ids": { + "device_id": device_id, + "application_ids": {"application_id": application_id}, + "dev_eui": "0000000000000000", + "join_eui": "0000000000000000", + "dev_addr": "00000000", + }, + "received_at": datetime.now().isoformat(), + "uplink_message": { + "session_key_id": "", + "f_port": 14, + "f_cnt": 1256661, + "frm_payload": "lAVCf1YBAA==", + "decoded_payload": generate_random_decoded_payload(), + "locations": { + "frm-payload": { + "latitude": 46.5581818, + "longitude": 15.6164958, + "altitude": 335, + "source": "SOURCE_GPS", + } + }, + }, + } + + +def generate_random_decoded_payload(): + """Generate random decoded payload dict with 0 to 15 values. The keys can be random and the values as well.""" + num_values = random.randint(0, 15) + decoded_payload = {} + + possible_columns = [ + "temperature", + "humidity", + "pressure", + "light", + "motion", + "battery", + "voltage", + "current", + "distance", + "speed", + "acceleration", + "gyroscope", + "magnetometer", + "gps_latitude", + "gps_longitude", + "gps_altitude", + "gps_accuracy", + "gps_speed", + "gps_heading", + "gps_satellites", + "gps_timestamp", + "note", + ] + + for _ in range(num_values): + key = "".join( + random.choice(possible_columns) + ) # Randomly select a key from the list + value = random.randint(-100, 100) # Random integer value + decoded_payload[key] = value + + # Add also a random version from v0 to v10 + version = random.randint(0, 10) + decoded_payload["version"] = f"v{version}" + + return decoded_payload + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Send mock TTN data to ingestion service." + ) + parser.add_argument( + "--num-devices", + type=int, + default=100, + help="Number of mock devices to generate (default: 100)", + ) + parser.add_argument( + "--num-applications", + type=int, + default=10, + help="Number of mock applications to generate (default: 10)", + ) + parser.add_argument( + "--num-messages", + type=int, + default=1000, + help="Number of mock messages to send (default: 1000). If set to zero it will run indefinitely.", + ) + parser.add_argument( + "--type", + type=str, + default="ttn", + choices=["ttn", "http"], + help="Type of data to send (default: ttn). If set to http, it will send data in general accepted format.", + ) + parser.add_argument( + "--url", + type=str, + default="http://localhost:50005/ttn", + help="URL of the ingestion service (default: http://localhost:50005/ttn)", + ) + args = parser.parse_args() + + # Generate device and application IDs based on the provided arguments + device_ids = [f"mock-dev-{i:03d}" for i in range(1, args.num_devices + 1)] + application_ids = [f"mock-app-{i}" for i in range(1, args.num_applications + 1)] + + # If num_messages is set to zero, we will run indefinitely + if args.num_messages == 0: + print("Running indefinitely. Press Ctrl+C to stop.") + + count = 0 + + # Check if url is default, if it is just use the type. If it isn't use the actual provided url + if args.url == "http://localhost:50005/ttn": + # If the URL is default, we will use the type to set the URL + url = f"http://localhost:50005/{args.type}" + else: + # If the URL is provided, we will use it + url = args.url + + while True: + if args.type == "ttn": + data = generate_ttn_data( + device_id=random.choice(device_ids), + application_id=random.choice(application_ids), + ) + if args.type == "http": + data = generate_http_data( + sensor_name=random.choice(device_ids), + application=random.choice(application_ids), + ) + serialized_data = json.dumps(data) + response = requests.post(url, headers=headers, json=data) + + print(f"Sent message {count + 1}/{args.num_messages}") + + count += 1 + + if args.num_messages > 0 and count >= args.num_messages: + break + + time.sleep(0.001) + + print(f"Sent {count} messages. Exiting.") From cd446db018c7e92dbbedf5a03cbf35e665138eb2 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 18 Jul 2025 12:57:22 +0200 Subject: [PATCH 12/28] feat: add logger --- common/constants.py | 3 + common/logging/logger.py | 136 +++++++++++++++++++++++++++++++++++++++ ingest_service/Makefile | 4 +- 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 common/constants.py create mode 100644 common/logging/logger.py diff --git a/common/constants.py b/common/constants.py new file mode 100644 index 0000000..bf594d5 --- /dev/null +++ b/common/constants.py @@ -0,0 +1,3 @@ +import logging + +LOG_LEVEL = logging.INFO diff --git a/common/logging/logger.py b/common/logging/logger.py new file mode 100644 index 0000000..9e50a45 --- /dev/null +++ b/common/logging/logger.py @@ -0,0 +1,136 @@ +# ruff: noqa: D107 + +"""This module is a custom wrapper around the Python logging module. + +It can be used to log messages to the console and to the Flask logger. +""" + +import logging +import sys + +from common.constants import LOG_LEVEL + + +class ColoredFormatter(logging.Formatter): + """Formatter that outputs colored logs based on level.""" + + COLORS = { + "DEBUG": "\033[94m", # Blue + "INFO": "\033[92m", # Green + "WARNING": "\033[93m", # Yellow + "ERROR": "\033[91m", # Red + "CRITICAL": "\033[91m\033[1m", # Bold Red + "RESET": "\033[0m", # Reset + } + + def __init__(self, fmt=None, datefmt=None, style="%", use_color=True): + super().__init__(fmt, datefmt, style) + self.use_color = ( + use_color and sys.stdout.isatty() + ) # Use color only if supported + + # Parse the format to separate prefix and message parts + if fmt and "%(" in fmt: + parts = fmt.split("%(message)s") + self.prefix_fmt = parts[0] + self.suffix_fmt = parts[1] if len(parts) > 1 else "" + else: + self.prefix_fmt = fmt + self.suffix_fmt = "" + + def format(self, record): + """Format the log record.""" + # Save the original module name + original_name = record.name + # Set module name to just the last part + record.name = original_name.split(".")[-1] + + if self.use_color: + # Get color based on log level + levelname = record.levelname + color = self.COLORS.get(levelname, self.COLORS["RESET"]) + + # Format only the prefix with color + prefix_formatter = logging.Formatter(self.prefix_fmt, self.datefmt) + prefix = prefix_formatter.format(record) + colored_prefix = f"{color}{prefix}{self.COLORS['RESET']}" + + # Format the message part separately (no coloring) + message = record.getMessage() + + # Format any suffix if needed + suffix = "" + if self.suffix_fmt: + suffix_formatter = logging.Formatter(self.suffix_fmt, self.datefmt) + suffix = suffix_formatter.format(record) + + # Combine the parts + formatted_msg = f"{colored_prefix}{message}{suffix}" + else: + # Regular formatting without color + formatted_msg = super().format(record) + + # Restore original name + record.name = original_name + return formatted_msg + + +root_logger = logging.getLogger() +# Clear existing handlers +for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + +# Add our custom handler +handler = logging.StreamHandler() +formatter = ColoredFormatter( + fmt="[%(asctime)s][%(levelname)s][%(name)s:%(lineno)d] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", # This format explicitly excludes milliseconds + use_color=True, +) +handler.setFormatter(formatter) +root_logger.addHandler(handler) +root_logger.setLevel(LOG_LEVEL) + +logging.getLogger("werkzeug").setLevel(logging.DEBUG) +logging.getLogger("paramiko.transport").setLevel(logging.WARNING) +logging.getLogger("kafka").setLevel(logging.WARNING) + + +class Logger: + """Custom logger class, used to log messages to the console and to the Flask logger.""" + + def __init__(self, logger, console_enabled=False): + """Custom logger class, used to log messages to the console and to the Flask logger. + + All formatted prints must be f-string formatted. + """ + self.logger = logger + self.console_enabled = console_enabled + + def info(self, message): + """Print message to console and Flask logger.""" + if self.console_enabled: + print(f"INFO: {message}") + if self.logger: + self.logger.info(message) + + def error(self, message): + """Print error message to console and Flask logger.""" + if self.console_enabled: + print(f"ERROR: {message}") + if self.logger: + self.logger.error(message) + + def warning(self, message): + """Print warning message to console and Flask logger.""" + if self.console_enabled: + print(f"WARNING: {message}") + if self.logger: + self.logger.warning(message) + + def debug(self, message): + """Print debug message to console and Flask logger.""" + if self.console_enabled: + print(f"DEBUG: {message}") + if self.logger: + self.logger.debug(message) diff --git a/ingest_service/Makefile b/ingest_service/Makefile index 4d17af5..d124a11 100644 --- a/ingest_service/Makefile +++ b/ingest_service/Makefile @@ -1,10 +1,10 @@ SHELL := /bin/bash clean: - # rm -r -f ./common + rm -r -f ./common install: - # cp -r ../common/ ./ + cp -r ../common/ ./ pip install -r requirements.txt run: clean install From 409df1fb5fa2126db140813fb007071451aea671 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 18 Jul 2025 12:57:44 +0200 Subject: [PATCH 13/28] feat: update sql schema --- docs/images/er_model.png | Bin 0 -> 58601 bytes init/postgres.sql | 97 ++++++++++++++++++++------------------- 2 files changed, 51 insertions(+), 46 deletions(-) create mode 100644 docs/images/er_model.png diff --git a/docs/images/er_model.png b/docs/images/er_model.png new file mode 100644 index 0000000000000000000000000000000000000000..96cd7b69ade89216ee79e9d873214d4760f57167 GIT binary patch literal 58601 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 TRUE, number_partitions => 10); +-- SELECT create_hypertable('positions', 'position_time', 'sensor_id', if_not_exists => TRUE, number_partitions => 10); -- SELECT create_hypertable('measurements', 'timestamp', 'sensor_id', if_not_exists => TRUE, number_partitions => 10); -- 3.2. Create additional indexes to optimize queries -CREATE INDEX IF NOT EXISTS idx_positions_id ON positions (id); +-- CREATE INDEX IF NOT EXISTS idx_positions_id ON positions (id); -- CREATE INDEX IF NOT EXISTS idx_measurements_timestamp ON measurements (timestamp); ALTER TABLE applications DROP CONSTRAINT IF EXISTS unique_application_name; @@ -107,19 +107,19 @@ ALTER TABLE sensors ADD CONSTRAINT unique_sensor_name UNIQUE (sensor_name); -- 4. Establishing Referential Integrity -- ----------------------------------------------------------------------------------- --- 4.5. Add a foreign key from `metadata.sensor_id` to `sensors.id` -ALTER TABLE metadata -ADD CONSTRAINT fk_metadata_sensors -FOREIGN KEY (sensor_id) -REFERENCES sensors(id) -ON DELETE CASCADE; +-- -- 4.5. Add a foreign key from `metadata.sensor_id` to `sensors.id` +-- ALTER TABLE metadata +-- ADD CONSTRAINT fk_metadata_sensors +-- FOREIGN KEY (sensor_id) +-- REFERENCES sensors(id) +-- ON DELETE CASCADE; --- 4.7. Add a foreign key from `metadata.location_id` to `location.id` -ALTER TABLE metadata -ADD CONSTRAINT fk_metadata_location -FOREIGN KEY (location_id) -REFERENCES locations(id) -ON DELETE CASCADE; +-- -- 4.7. Add a foreign key from `metadata.location_id` to `location.id` +-- ALTER TABLE metadata +-- ADD CONSTRAINT fk_metadata_location +-- FOREIGN KEY (location_id) +-- REFERENCES locations(id) +-- ON DELETE CASCADE; ALTER TABLE application_measurements_registry ADD CONSTRAINT fk_application_measurements_registry_applications @@ -127,12 +127,17 @@ FOREIGN KEY (application_id) REFERENCES applications(id) ON DELETE CASCADE; --- 4.8. Add a foreign key from `metadata.application_id` to `applications.id` -ALTER TABLE metadata -ADD CONSTRAINT fk_metadata_applications +ALTER TABLE sensors +ADD CONSTRAINT fk_sensors_applications FOREIGN KEY (application_id) -REFERENCES applications(id) -ON DELETE CASCADE; +REFERENCES applications(id); + +-- -- 4.8. Add a foreign key from `metadata.application_id` to `applications.id` +-- ALTER TABLE metadata +-- ADD CONSTRAINT fk_metadata_applications +-- FOREIGN KEY (application_id) +-- REFERENCES applications(id) +-- ON DELETE CASCADE; -- Warning: This affects all transactions on the PostgreSQL instance. -- Required for high-volume FK operations. Requires PostgreSQL restart. From 2e47d774fdd90b96740b6039f08c5f4f49b13a4a Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 18 Jul 2025 12:58:05 +0200 Subject: [PATCH 14/28] feat: refactor processor service, add docs --- processor_service/Makefile | 4 +- processor_service/README.md | 71 +++++++ processor_service/main.py | 13 +- processor_service/src/constants.py | 7 + processor_service/src/generate_table.py | 247 ------------------------ processor_service/src/handlers.py | 111 +++++++++++ processor_service/src/sql.py | 136 +++++++++++++ processor_service/src/util.py | 8 + 8 files changed, 340 insertions(+), 257 deletions(-) create mode 100644 processor_service/README.md create mode 100644 processor_service/src/constants.py delete mode 100644 processor_service/src/generate_table.py create mode 100644 processor_service/src/handlers.py create mode 100644 processor_service/src/sql.py create mode 100644 processor_service/src/util.py diff --git a/processor_service/Makefile b/processor_service/Makefile index 2549268..2e278d1 100644 --- a/processor_service/Makefile +++ b/processor_service/Makefile @@ -1,10 +1,10 @@ SHELL := /bin/bash clean: - # rm -r -f ./common + rm -r -f ./common install: - # cp -r ../common/ ./ + cp -r ../common/ ./ pip install -r requirements.txt run: clean install diff --git a/processor_service/README.md b/processor_service/README.md new file mode 100644 index 0000000..bf2a9a1 --- /dev/null +++ b/processor_service/README.md @@ -0,0 +1,71 @@ +# Processor Service + +The Processor service is responsible for processing data received from the Ingest Service. It +dynamically creates SQL tables based on the fields in the data and stores the processed data in a +PostgreSQL database. + +The SQL schema is defined in the [init/postgres.sql](../init/postgres.sql) file, which is executed +when the docker service first starts. This schema does not include a `measurements` table, as those +are generated dynamically. + +## Tables + +An `application` is the endpoint the data is receive from. In the case of a TTN application, it is +the name of the TTN application. In the case of an HTTP application, it is the name defined by the +sending party. + +The `sensors` table holds information about the sensors, such as the `sensor_name` and which +application it belongs to. + +The `measurements` table is generated based on the `version` of the decoder. The `version` should be +included in the JSON data sent by the application. If the `version` is not included, the default +table used will be `v0`. The table columns are also dynamically added each time a new key is +observed. The `measurements` tables are linked to `applications` via their names in the +`application_measurements_registry` table. + +### Example Measurements table + +An example of a generated `measurements` table for the `irnas-mock-application` would be +`irnas-mock-application_v0`. Let's define the following sensor data structure: + +```json +{ + "measurement": { + "voltage": 3.3, + "temperature": 22.5, + "humidity": 45.0 + } +} +``` + +Once the data is received and the table doesn't exist it would get created with these three columns +along with a `timestamp` and `sensor_id` column, which are static and always present. + +If the next measurement contained an additional field, `pressure`, the table would be updated to +include this new column. + +Once the `version` field would be added to the measurement, like so: + +```json +{ + "measurement": { + "voltage": 3.3, + "temperature": 22.5, + "humidity": 45.0, + "version": "v1" + } +} +``` + +The table would be created as `irnas-mock-application_v1` and the previous data would not be lost, +but rather stored in the `irnas-mock-application_v0` table. + +The table is created to be as general as possible, therefore all numeric fields are of type `FLOAT`. + +## ER Model + +In the image below the ER model is shown. You can see the two dynamically generated `measurements_1` +and `measurements_2` tables, which are logically linked to the `application` via their names (red +lines on the image). + +![ER Model](../docs/images/er_model.png) diff --git a/processor_service/main.py b/processor_service/main.py index 59e9c1b..ee23eef 100644 --- a/processor_service/main.py +++ b/processor_service/main.py @@ -2,7 +2,9 @@ import logging from kafka import KafkaConsumer -from src.generate_table import insert_data +from src.handlers import insert_data + +from common.logging.logger import Logger consumer = KafkaConsumer( "process_data", @@ -10,17 +12,12 @@ auto_offset_reset="earliest", group_id="ajdovscina-consumer", max_poll_interval_ms=900000, - # enable_auto_commit=True, ) -logging.basicConfig( - format="%(asctime)s %(levelname)s:%(message)s", - level=logging.INFO, -) -logger = logging.getLogger(__name__) +logger = Logger(logging.getLogger(__name__)) for message in consumer: - logger.info( + logger.debug( "%s:%d:%d: key=%s value=%s" % ( message.topic, diff --git a/processor_service/src/constants.py b/processor_service/src/constants.py new file mode 100644 index 0000000..d40674b --- /dev/null +++ b/processor_service/src/constants.py @@ -0,0 +1,7 @@ +DB_CONFIG = { + "dbname": "ajdovscina", + "user": "ajdovscina-geospatial-user", + "password": "password", + "host": "localhost", + "port": 13328, +} diff --git a/processor_service/src/generate_table.py b/processor_service/src/generate_table.py deleted file mode 100644 index bb446dc..0000000 --- a/processor_service/src/generate_table.py +++ /dev/null @@ -1,247 +0,0 @@ -"""This module dynamically creates SQL tables based on the sensor type and present fields. - -It infers the data type based on the value of the field and generates a SQL CREATE TABLE statement. -""" - -from datetime import datetime - -import psycopg2 - -"""The flow is as follows: - -1. Application data is received from device -2. A new application is created if it doesn't yet exist -3. A new _ table is created if it doesn't yet exist -4. The pair is added to the application_measurements_registry -5. Measurements are added to the _ table -6. Measurements are added to the `measurements` table -""" - -GEOM_DB_CONFIG = { - "dbname": "ajdovscina", - "user": "ajdovscina-geospatial-user", - "password": "password", - "host": "localhost", - "port": 13328, -} -conn = psycopg2.connect(**GEOM_DB_CONFIG) - - -def drop_duplicates(fields: dict): - """Drop duplicates from data fields in case they are present.""" - seen = {} - for k, v in fields.items(): - k = k.lower() - if k not in seen: - seen[k] = v # keep first occurrence - return seen - - -def insert_data(data: dict): - """Insert data into SQL database.""" - try: - with conn.cursor() as cur: - # 0. Verify integrity of data - this should rather be done on the receiver and the sending party should be notified if data is not good - - # 2. - app_name = data.get("application") - stmt = create_application(app_name) - cur.execute(stmt) - print(f"App create statement: {stmt}") - app_id = cur.fetchone()[0] - print(f"App id: {app_id}") - - # 3. - version = data.get("version") - fields = data.get("measurement", {}) - - print(fields) - - # Drop duplicate keys from fields - fields = drop_duplicates(fields) - - if fields is None or len(fields) == 0: - return - - meas_table_name = f"{app_name}_{version}" - stmt, table_name = create_sensor_readings_table(meas_table_name, fields) - print(f"Create measurement table statement: {stmt}") - cur.execute(stmt) - - # 3.05 Get existing columns - cur.execute( - f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}'" - ) - existing_columns = {row[0].lower() for row in cur.fetchall()} - print(f"Existing columns in table {table_name}: {existing_columns}") - - # 3.1 Alter the table to add new columns - stmt = alter_measurement_table(meas_table_name, fields, existing_columns) - if stmt is not None: - cur.execute(stmt) - - # 4. - add_to_registry_stmt = add_table_to_registry(app_id, table_name) - print(f"Add to registry statement: {add_to_registry_stmt}") - cur.execute(add_to_registry_stmt) - - # 5. Add measurement to table - timestamp = data.get("received_at", datetime.now()) - sensor_name = data.get("sensor_name", "") - - # 5.1 First create the sensor if it doesn't exist - sensor_insert_query = insert_sensor() - cur.execute( - sensor_insert_query, - ( - sensor_name, - app_id, - ), - ) - sens_id = cur.fetchone()[0] - print(f"Sens id: {sens_id}") - - insert_query = insert_sensor_readings( - table_name, fields, timestamp, sens_id - ) - print(f"Insert query: {insert_query}") - - cur.execute(insert_query) - - conn.commit() - except Exception as e: - conn.rollback() - print(f"Failed to insert data: {e}") - - -def insert_sensor(): - """Creates a SQL INSERT INTO statement for the sensors table.""" - query = """ - INSERT INTO sensors (sensor_name, application_id) - VALUES (%s, %s) - ON CONFLICT (sensor_name) - DO UPDATE SET sensor_name = EXCLUDED.sensor_name - RETURNING id; - """ - return query - - -def insert_sensor_readings( - table_name: str, fields: dict, timestamp: datetime, sensor_id: int -): - """Creates a SQL INSERT INTO statement based on the name and field.""" - keys = fields.keys() - values = fields.values() - columns = "timestamp, sensor_id, " - columns += ", ".join(keys) - values_placeholders = f"'{timestamp}', {sensor_id}, " - values_placeholders += ", ".join([f"'{value}'" for value in values]) - - query = f'INSERT INTO "{table_name}" ({columns}) VALUES ({values_placeholders})' - - return query - - -def create_sensor_readings_table(application_name: str, fields: dict): - """Creates a SQL CREATE TABLE statement based on the sensor type and fields. - - Args: - application_name (str): The name of the application, used to create the table name. - fields (dict): A dictionary containing field names and their corresponding values. - - Returns: - str: A SQL CREATE TABLE statement. - """ - table_name = application_name.replace(" ", "_").lower() - columns = [] - columns.append("id SERIAL PRIMARY KEY") - columns.append("sensor_id INTEGER") - columns.append("timestamp TIMESTAMPTZ") - - print(f"Fields: {fields}") - - for field, value in fields.items(): - if isinstance(value, int): - col_type = "FLOAT" - elif isinstance(value, float): - col_type = "DOUBLE PRECISION" - elif isinstance(value, str): - col_type = "TEXT" - elif isinstance(value, datetime): - col_type = "TIMESTAMPTZ" - - # Check if column might already exist - column = f"{field} {col_type}".lower() - if column in columns: - continue - - columns.append(column) - - columns_str = ", ".join(columns) - create_table_statement = ( - f'CREATE TABLE IF NOT EXISTS "{table_name}" ({columns_str})' - ) - - return create_table_statement, table_name - - -def alter_measurement_table(table_name, fields, existing_columns): - """A table can already exist but not all fields are present. If not all fields are present alter the table.""" - print(f"Fields: {fields}") - columns = [] - - for field, value in fields.items(): - if isinstance(value, int): - col_type = "FLOAT" - elif isinstance(value, float): - col_type = "DOUBLE PRECISION" - elif isinstance(value, str): - col_type = "TEXT" - elif isinstance(value, datetime): - col_type = "TIMESTAMPTZ" - - if field.lower() not in existing_columns: - columns.append(f"ADD COLUMN {field} {col_type},") - - if len(columns) == 0: - return None - - columns[-1] = columns[-1].rstrip(",") # Remove trailing comma from the last column - - columns_str = "\n".join(columns) - statement = f'ALTER TABLE "{table_name}"\n{columns_str};' - - print(f"Statement {statement}") - - return statement - - -def create_application(application_name): - """Insert application to applications table if it doesn't exist.""" - stmt = f""" - INSERT INTO applications (app_name) - VALUES ('{application_name}') - ON CONFLICT (app_name) - DO UPDATE SET app_name = EXCLUDED.app_name - RETURNING id; - """ - return stmt - - -def add_table_to_registry(application_id, measurement_table_name): - """Adds the created table to the registry. - - Args: - application_id (str): The ID of the application. - measurement_table_name (str): The name of the table to be added. - - Returns: - str: A SQL INSERT statement to add the table to the registry. - """ - stmt = ( - f"INSERT INTO application_measurements_registry (application_id, measurement_table_name) " - f"VALUES ('{application_id}', '{measurement_table_name}') " - f"ON CONFLICT (application_id, measurement_table_name) DO NOTHING;" - ) - - return stmt diff --git a/processor_service/src/handlers.py b/processor_service/src/handlers.py new file mode 100644 index 0000000..a818fdc --- /dev/null +++ b/processor_service/src/handlers.py @@ -0,0 +1,111 @@ +import logging +from datetime import datetime + +import psycopg2 +from src.constants import DB_CONFIG +from src.sql import ( + add_table_to_registry, + alter_measurement_table, + create_application, + create_sensor_readings_table, + insert_sensor, + insert_sensor_readings, +) +from src.util import drop_duplicates + +from common.logging.logger import Logger + +# Create connection to the PostgreSQL database +conn = psycopg2.connect(**DB_CONFIG) + +logger = Logger(logging.getLogger(__name__)) + + +def insert_data(data: dict): + """Insert data into SQL database.""" + try: + with conn.cursor() as cur: + # 1. + app_name = data.get("application") + stmt = create_application() + logger.info(f"Creating application {app_name}") + cur.execute(stmt, (app_name,)) + app_id = cur.fetchone()[0] + + # 2. + version = data.get("version") + fields = data.get("measurement", {}) + + # Drop duplicate keys from fields + fields = drop_duplicates(fields) + + if fields is None or len(fields) == 0: + return + + meas_table_name = f"{app_name}_{version}" + stmt, table_name = create_sensor_readings_table(meas_table_name, fields) + logger.debug(f"Create measurement table statement: {stmt}") + logger.info( + f"Creating measurement table {table_name} for application {app_name} with version {version}" + ) + cur.execute(stmt) + + # 3.05 Get existing columns + logger.info(f"Fetching existing columns for table {table_name}") + cur.execute( + f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}'" + ) + existing_columns = {row[0].lower() for row in cur.fetchall()} + logger.debug(f"Existing columns in table {table_name}: {existing_columns}") + + # 3.1 Alter the table to add new columns + stmt = alter_measurement_table(meas_table_name, fields, existing_columns) + logger.info( + f"Altering measurement table {table_name} to add new fields if necessary" + ) + logger.debug(f"Alter table statement: {stmt}") + if stmt is not None: + cur.execute(stmt) + + # 4. + add_to_registry_stmt = add_table_to_registry() + logger.info( + f"Adding {table_name} to application_measurements_registry for application {app_name}" + ) + logger.debug(f"Add to registry statement: {add_to_registry_stmt}") + cur.execute( + add_to_registry_stmt, + ( + app_id, + table_name, + ), + ) + + # 5. Add measurement to table + timestamp = data.get("received_at", datetime.now()) + sensor_name = data.get("sensor_name", "") + + # 5.1 First create the sensor if it doesn't exist + sensor_insert_stmt = insert_sensor() + logger.info(f"Inserting sensor {sensor_name} for application {app_name}") + logger.debug(f"Insert sensor statement: {sensor_insert_stmt}") + cur.execute( + sensor_insert_stmt, + ( + sensor_name, + app_id, + ), + ) + sens_id = cur.fetchone()[0] + + insert_data_stmt = insert_sensor_readings(table_name, list(fields.keys())) + logger.info( + f"Inserting data into {table_name} for sensor {sensor_name} with ID {sens_id}" + ) + logger.debug(f"Insert data statement: {insert_data_stmt}") + cur.execute(insert_data_stmt, (timestamp, sens_id, *fields.values())) + + conn.commit() + except Exception as e: + conn.rollback() + logger.error(f"Failed to insert data: {e}") diff --git a/processor_service/src/sql.py b/processor_service/src/sql.py new file mode 100644 index 0000000..0c63cd6 --- /dev/null +++ b/processor_service/src/sql.py @@ -0,0 +1,136 @@ +"""This module dynamically creates SQL tables based on the sensor type and present fields. + +It infers the data type based on the value of the field and generates a SQL CREATE TABLE statement. +""" + +from datetime import datetime + +"""The flow is as follows: + +1. Application data is received from device +2. A new application is created if it doesn't yet exist +3. A new _ table is created if it doesn't yet exist +4. The pair is added to the application_measurements_registry +5. Measurements are added to the _ table +6. Measurements are added to the `measurements` table +""" + + +def insert_sensor(): + """Creates a SQL INSERT INTO statement for the sensors table.""" + query = """ + INSERT INTO sensors (sensor_name, application_id) + VALUES (%s, %s) + ON CONFLICT (sensor_name) + DO UPDATE SET sensor_name = EXCLUDED.sensor_name + RETURNING id; + """ + return query + + +def insert_sensor_readings(table_name: str, keys: list): + """Creates a SQL INSERT INTO statement based on the name and field.""" + columns = "timestamp, sensor_id, " + columns += ", ".join(keys) + values_placeholders = "%s, %s, " + values_placeholders += ", ".join(["%s" for _ in keys]) + + query = f'INSERT INTO "{table_name}" ({columns}) VALUES ({values_placeholders})' + + return query + + +def create_sensor_readings_table(application_name: str, fields: dict): + """Creates a SQL CREATE TABLE statement based on the sensor type and fields. + + Args: + application_name (str): The name of the application, used to create the table name. + fields (dict): A dictionary containing field names and their corresponding values. + + Returns: + str: A SQL CREATE TABLE statement. + """ + table_name = application_name.replace(" ", "_").lower() + columns = [] + columns.append("id SERIAL PRIMARY KEY") + columns.append("sensor_id INTEGER REFERENCES sensors(id) ON DELETE CASCADE") + columns.append("timestamp TIMESTAMPTZ") + + for field, value in fields.items(): + if isinstance(value, int): + col_type = "FLOAT" + elif isinstance(value, float): + col_type = "DOUBLE PRECISION" + elif isinstance(value, str): + col_type = "TEXT" + elif isinstance(value, datetime): + col_type = "TIMESTAMPTZ" + + # Check if column might already exist + column = f"{field} {col_type}".lower() + if column in columns: + continue + + columns.append(column) + + columns_str = ", ".join(columns) + create_table_statement = ( + f'CREATE TABLE IF NOT EXISTS "{table_name}" ({columns_str})' + ) + + return create_table_statement, table_name + + +def alter_measurement_table(table_name, fields, existing_columns): + """A table can already exist but not all fields are present. If not all fields are present alter the table.""" + columns = [] + + for field, value in fields.items(): + if isinstance(value, int): + col_type = "FLOAT" + elif isinstance(value, float): + col_type = "DOUBLE PRECISION" + elif isinstance(value, str): + col_type = "TEXT" + elif isinstance(value, datetime): + col_type = "TIMESTAMPTZ" + + if field.lower() not in existing_columns: + columns.append(f"ADD COLUMN {field} {col_type},") + + if len(columns) == 0: + return None + + columns[-1] = columns[-1].rstrip(",") # Remove trailing comma from the last column + + columns_str = "\n".join(columns) + statement = f'ALTER TABLE "{table_name}"\n{columns_str};' + + return statement + + +def create_application(): + """Create statement to insert application to applications table if it doesn't exist.""" + stmt = """ + INSERT INTO applications (app_name) + VALUES (%s) + ON CONFLICT (app_name) + DO UPDATE SET app_name = EXCLUDED.app_name + RETURNING id; + """ + return stmt + + +def add_table_to_registry(): + """Creates the statement which adds the table to registry. + + Returns: + str: A SQL INSERT statement to add the table to the registry. + """ + stmt = ( + "INSERT INTO application_measurements_registry (application_id, measurement_table_name) " + "VALUES (%s, %s) " + "ON CONFLICT (application_id, measurement_table_name) DO NOTHING;" + ) + + return stmt diff --git a/processor_service/src/util.py b/processor_service/src/util.py new file mode 100644 index 0000000..2d6bbd7 --- /dev/null +++ b/processor_service/src/util.py @@ -0,0 +1,8 @@ +def drop_duplicates(fields: dict): + """Drop duplicates from data fields in case they are present.""" + seen = {} + for k, v in fields.items(): + k = k.lower() + if k not in seen: + seen[k] = v # keep first occurrence + return seen From 832839cf546a3fc2a194eac2ead595206d55b83c Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 18 Jul 2025 13:11:39 +0200 Subject: [PATCH 15/28] feat: move db connection string to common constants --- common/constants.py | 8 ++++++++ processor_service/src/constants.py | 7 ------- processor_service/src/handlers.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 processor_service/src/constants.py diff --git a/common/constants.py b/common/constants.py index bf594d5..691436b 100644 --- a/common/constants.py +++ b/common/constants.py @@ -1,3 +1,11 @@ import logging LOG_LEVEL = logging.INFO + +DB_CONFIG = { + "dbname": "ajdovscina", + "user": "ajdovscina-geospatial-user", + "password": "password", + "host": "localhost", + "port": 13328, +} diff --git a/processor_service/src/constants.py b/processor_service/src/constants.py deleted file mode 100644 index d40674b..0000000 --- a/processor_service/src/constants.py +++ /dev/null @@ -1,7 +0,0 @@ -DB_CONFIG = { - "dbname": "ajdovscina", - "user": "ajdovscina-geospatial-user", - "password": "password", - "host": "localhost", - "port": 13328, -} diff --git a/processor_service/src/handlers.py b/processor_service/src/handlers.py index a818fdc..ab160c7 100644 --- a/processor_service/src/handlers.py +++ b/processor_service/src/handlers.py @@ -2,7 +2,6 @@ from datetime import datetime import psycopg2 -from src.constants import DB_CONFIG from src.sql import ( add_table_to_registry, alter_measurement_table, @@ -13,6 +12,7 @@ ) from src.util import drop_duplicates +from common.constants import DB_CONFIG from common.logging.logger import Logger # Create connection to the PostgreSQL database From fd097ad4221e567fb793dc03e7c3c2274e67fb71 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 18 Jul 2025 13:15:15 +0200 Subject: [PATCH 16/28] feat: move authentication to common --- {ingest_service/util => common}/authentication.py | 0 ingest_service/main.py | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename {ingest_service/util => common}/authentication.py (100%) diff --git a/ingest_service/util/authentication.py b/common/authentication.py similarity index 100% rename from ingest_service/util/authentication.py rename to common/authentication.py diff --git a/ingest_service/main.py b/ingest_service/main.py index 0cb6489..b047947 100644 --- a/ingest_service/main.py +++ b/ingest_service/main.py @@ -8,13 +8,14 @@ # Import the Pydantic models for the API requests from models.ttn import DataTTN -from util.authentication import verify_token from util.formatter import ( format_http_data_for_processing, format_ttn_data_for_processing, ) from util.util import custom_value_serializer +from common.authentication import verify_token + # Initialize the Kafka producer with a custom serializer producer = KafkaProducer( bootstrap_servers="localhost:9992", From 3cc1d6e475ba355d5424aa39ff1e1b14c13dee98 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 18 Jul 2025 13:21:52 +0200 Subject: [PATCH 17/28] misc: move util --- processor_service/src/handlers.py | 2 +- processor_service/{src => util}/util.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename processor_service/{src => util}/util.py (100%) diff --git a/processor_service/src/handlers.py b/processor_service/src/handlers.py index ab160c7..556e30d 100644 --- a/processor_service/src/handlers.py +++ b/processor_service/src/handlers.py @@ -10,7 +10,7 @@ insert_sensor, insert_sensor_readings, ) -from src.util import drop_duplicates +from util.util import drop_duplicates from common.constants import DB_CONFIG from common.logging.logger import Logger diff --git a/processor_service/src/util.py b/processor_service/util/util.py similarity index 100% rename from processor_service/src/util.py rename to processor_service/util/util.py From a5d73fbbb24e31d964cdf9d9baa302a6dd7a7af0 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 18 Jul 2025 13:44:31 +0200 Subject: [PATCH 18/28] feat: restructure REST API, add docs --- processor_service/README.md | 9 ++ rest_api/Makefile | 4 +- rest_api/README.md | 112 +++++++++++++++++++++++ rest_api/main.py | 176 +++++++----------------------------- rest_api/models/models.py | 25 +++++ rest_api/src/handlers.py | 98 ++++++++++++++++++++ rest_api/src/sql.py | 37 ++++++++ rest_api/util/util.py | 12 +++ 8 files changed, 329 insertions(+), 144 deletions(-) create mode 100644 rest_api/README.md create mode 100644 rest_api/models/models.py create mode 100644 rest_api/src/handlers.py create mode 100644 rest_api/src/sql.py create mode 100644 rest_api/util/util.py diff --git a/processor_service/README.md b/processor_service/README.md index bf2a9a1..f2d8cbd 100644 --- a/processor_service/README.md +++ b/processor_service/README.md @@ -8,6 +8,15 @@ The SQL schema is defined in the [init/postgres.sql](../init/postgres.sql) file, when the docker service first starts. This schema does not include a `measurements` table, as those are generated dynamically. +## Usage + +To run the Processor service locally, run the following command from the `processor_service` +directory: + +```bash +make run-local-dev +``` + ## Tables An `application` is the endpoint the data is receive from. In the case of a TTN application, it is diff --git a/rest_api/Makefile b/rest_api/Makefile index 9a02ec0..373091a 100644 --- a/rest_api/Makefile +++ b/rest_api/Makefile @@ -1,10 +1,10 @@ SHELL := /bin/bash clean: - # rm -r -f ./common + rm -r -f ./common install: - # cp -r ../common/ ./ + cp -r ../common/ ./ pip install -r requirements.txt run: clean install diff --git a/rest_api/README.md b/rest_api/README.md new file mode 100644 index 0000000..64aed1f --- /dev/null +++ b/rest_api/README.md @@ -0,0 +1,112 @@ +# REST API + +The REST API provides a way for the integrators to fetch sensor data and application information. + +## Usage + +To run the REST API locally run the following command from the `rest_api` directory: + +```bash +make run-local-dev +``` + +## Endpoints + +Currently the following endpoints are available: + +### `GET /api/sensors` + +Lists all sensors in the system. The response includes sensor names, and the applications they +belong to. + +Example: + +```bash +curl http://localhost:51005/api/sensors -H "Authorization: Bearer your-api-token" | jq +``` + +Output: + +```json +{ + "sensors": [ + { + "sensor_name": "mock-dev-090", + "application_id": 6 + }, + { + "sensor_name": "mock-dev-004", + "application_id": 3 + } + ] +} +``` + +### `GET /api/applications` + +Lists all applications in the system. The response includes application names and their IDs. + +Example: + +```bash +curl http://localhost:51005/api/applications -H "Authorization: Bearer your-api-token" | jq +``` + +Output: + +```json +{ + "applications": [ + { + "app_name": "mock-app-4", + "description": null + }, + { + "app_name": "mock-app-2", + "description": null + }, + { + "app_name": "mock-app-10", + "description": null + } + ] +} +``` + +### `GET /api/measurements?sensor_name={sensor_name}` + +Fetches measurements for a specific sensor. The response includes the sensor name, table name, and a +list of measurements. + +Example: + +```bash +curl "http://localhost:51005/api/measurements?sensor_name=mock-dev-004" -H "Authorization: Bearer your-api-token" | jq +``` + +Output: + +```json +{ + "measurements": [ + { + "table_name": "mock-app-4_v4", + "timestamp": "2025-07-18 12:42:25.242002+00:00", + "data": { + "current": 24.0, + "gps_heading": -96.0, + "gps_timestamp": 6.0, + "gps_longitude": 87.0, + "gps_speed": -40.0, + "speed": -4.0, + "acceleration": -84.0, + "battery": 99.0, + "pressure": 91.0, + "gyroscope": -49.0, + "temperature": 49.0, + "voltage": null + } + } + ] +} +``` diff --git a/rest_api/main.py b/rest_api/main.py index e2656f4..e57adc6 100644 --- a/rest_api/main.py +++ b/rest_api/main.py @@ -1,175 +1,67 @@ -from typing import List, Optional +from typing import List -import orjson import psycopg2 from fastapi import FastAPI -from fastapi.encoders import jsonable_encoder from fastapi.responses import ORJSONResponse -from pydantic import BaseModel +from models.models import Application, Measurement, Sensor +from src.handlers import ( + fetch_applications, + fetch_measurements_for_sensor, + fetch_sensor_app_info, + fetch_sensors, + fetch_table_names_from_app_id, +) +from util.util import get_pretty_json + +from common.constants import DB_CONFIG app = FastAPI(default_response_class=ORJSONResponse) - -class Measurement(BaseModel): - """Data model for sensor measurements.""" - - table_name: str - timestamp: str - data: dict # dynamic columns from the database - - -class Sensor(BaseModel): - """Data model for sensor information.""" - - sensor_name: str - application_id: int - - -class Application(BaseModel): - """Data model for application information.""" - - app_name: str - description: Optional[str] = None - - -def get_db_connection(): - """Establish a connection to the PostgreSQL database.""" - return psycopg2.connect( - host="localhost", - port=13328, - user="ajdovscina-geospatial-user", - password="password", - dbname="ajdovscina", - ) +conn = psycopg2.connect(**DB_CONFIG) @app.get("/api/sensors", response_model=List[Sensor]) def get_sensors(): """Fetch all sensors from the database.""" - conn = get_db_connection() - cur = conn.cursor() - cur.execute(""" - SELECT id, sensor_uid, sensor_name, application_id, created_at, updated_at - FROM sensors - """) - rows = cur.fetchall() - sensors = [ - Sensor( - id=row[0], - sensor_uid=row[1], - sensor_name=row[2], - application_id=row[3], - created_at=str(row[4]), - updated_at=str(row[5]), - ) - for row in rows - ] - cur.close() - conn.close() + sensors = fetch_sensors(conn) - encoded = jsonable_encoder({"sensors": sensors}) - pretty_json = ORJSONResponse( - content=orjson.loads(orjson.dumps(encoded, option=orjson.OPT_INDENT_2)), - ) - return pretty_json + if sensors is None: + return ORJSONResponse(content={"sensors": []}) + + return get_pretty_json({"sensors": sensors}) @app.get("/api/applications", response_model=List[Application]) def get_applications(): """Fetch all applications from the database.""" - conn = get_db_connection() - cur = conn.cursor() - cur.execute(""" - SELECT id, app_name, description, created_at, updated_at - FROM applications - """) - rows = cur.fetchall() - - applications = [ - Application( - id=row[0], - app_name=row[1], - description=row[2], - created_at=str(row[3]), - updated_at=str(row[4]), - ) - for row in rows - ] - cur.close() - conn.close() + applications = fetch_applications(conn) - encoded = jsonable_encoder({"applications": applications}) - pretty_json = ORJSONResponse( - content=orjson.loads(orjson.dumps(encoded, option=orjson.OPT_INDENT_2)), - ) - return pretty_json + if applications is None: + return ORJSONResponse(content={"applications": []}) + return get_pretty_json({"applications": applications}) -@app.get("/api/measurements/sensor_name", response_model=List[Measurement]) + +@app.get("/api/measurements", response_model=List[Measurement]) def get_measurements(sensor_name): """Fetch measurements for a given sensor name.""" - conn = get_db_connection() - cur = conn.cursor() - - # Fetch application_id for the given sensor_name - query = "SELECT application_id, id FROM sensors WHERE sensor_name = %s" - # print(f"Executing query: {query} with sensor_name: {sensor_name}") - cur.execute(query, (sensor_name,)) - result = cur.fetchone() - # print(result) + result = fetch_sensor_app_info(conn, sensor_name) if not result: - cur.close() - conn.close() return ORJSONResponse(content={"error": "Sensor not found"}, status_code=404) application_id = result[0] sensor_id = result[1] - # Fetch measurement table names for the application_id - query = """ - SELECT measurement_table_name - FROM application_measurements_registry - WHERE application_id = %s - """ - # print(f"Measurement table query: {query} with application_id: {application_id}") - cur.execute(query, (application_id,)) - tables = cur.fetchall() + tables = fetch_table_names_from_app_id(conn, application_id) if not tables: - cur.close() - conn.close() return ORJSONResponse(content={"measurements": []}) measurements = [] for table_row in tables: - table_name = table_row[0] - # Fetch all measurements for the sensor_id from each table - query = f""" - SELECT * - FROM "{table_name}" - WHERE sensor_id = %s - """ - # print(f"Measurement query: {query} with sensor_id: {sensor_id}") - cur.execute(query, (sensor_id,)) - rows = cur.fetchall() - if cur.description is not None: - colnames = [desc[0] for desc in cur.description] - for row in rows: - # Build a dict of column names to values, excluding sensor_id and timestamp - - data = {col: val for col, val in zip(colnames[3:], row[3:])} - measurements.append( - Measurement( - # sensor_id=row[1], - table_name=table_name, - timestamp=str(row[2]), - data=data, - ) - ) - - cur.close() - conn.close() - - encoded = jsonable_encoder({"measurements": measurements}) - pretty_json = ORJSONResponse( - content=orjson.loads(orjson.dumps(encoded, option=orjson.OPT_INDENT_2)), - ) - return pretty_json + table_measurements = fetch_measurements_for_sensor( + conn, table_row[0], sensor_id + ) + + if table_measurements: + measurements.extend(table_measurements) + + return get_pretty_json({"measurements": measurements}) diff --git a/rest_api/models/models.py b/rest_api/models/models.py new file mode 100644 index 0000000..dc03876 --- /dev/null +++ b/rest_api/models/models.py @@ -0,0 +1,25 @@ +from typing import Optional + +from pydantic import BaseModel + + +class Measurement(BaseModel): + """Data model for sensor measurements.""" + + table_name: str + timestamp: str + data: dict # dynamic columns from the database + + +class Sensor(BaseModel): + """Data model for sensor information.""" + + sensor_name: str + application_id: int + + +class Application(BaseModel): + """Data model for application information.""" + + app_name: str + description: Optional[str] = None diff --git a/rest_api/src/handlers.py b/rest_api/src/handlers.py new file mode 100644 index 0000000..2017297 --- /dev/null +++ b/rest_api/src/handlers.py @@ -0,0 +1,98 @@ +from models.models import Application, Measurement, Sensor +from src.sql import ( + fetch_applications_statement, + fetch_measurements_for_sensor_statement, + fetch_sensor_app_info_statement, + fetch_sensors_statement, + fetch_table_names_from_app_id_statement, +) + + +def fetch_sensors(conn): + """Fetch sensors from the database.""" + with conn.cursor() as cur: + cur.execute(fetch_sensors_statement()) + rows = cur.fetchall() + sensors = [ + Sensor( + id=row[0], + sensor_uid=row[1], + sensor_name=row[2], + application_id=row[3], + created_at=str(row[4]), + updated_at=str(row[5]), + ) + for row in rows + ] + + return sensors + + +def fetch_applications(conn): + """Fetch applications from the database.""" + with conn.cursor() as cur: + cur.execute(fetch_applications_statement()) + rows = cur.fetchall() + applications = [ + Application( + id=row[0], + app_name=row[1], + description=row[2], + created_at=str(row[3]), + updated_at=str(row[4]), + ) + for row in rows + ] + + return applications + + +def fetch_sensor_app_info(conn, sensor_name: str) -> tuple[int, int] | None: + """Fetch application_id and sensor_id from sensors table based on sensor_name.""" + with conn.cursor() as cur: + cur.execute(fetch_sensor_app_info_statement(), (sensor_name,)) + result = cur.fetchone() + + if result is None: + return None + + application_id, sensor_id = result + return application_id, sensor_id + + +def fetch_table_names_from_app_id(conn, app_id: int) -> list | None: + """Fetch all measurement tables for a given application_id.""" + with conn.cursor() as cur: + cur.execute(fetch_table_names_from_app_id_statement(), (app_id,)) + table_names = cur.fetchall() + + return table_names if table_names else None + + +def fetch_measurements_for_sensor(conn, table_name: str, sensor_id: int) -> list | None: + """Fetch all measurements for a given sensor_id from a specific table.""" + with conn.cursor() as cur: + cur.execute(fetch_measurements_for_sensor_statement(table_name), (sensor_id,)) + rows = cur.fetchall() + + if not rows: + return None + + measurements = [] + + if cur.description is not None: + colnames = [desc[0] for desc in cur.description] + for row in rows: + # Build a dict of column names to values, excluding sensor_id and timestamp + + data = {col: val for col, val in zip(colnames[3:], row[3:])} + measurements.append( + Measurement( + # sensor_id=row[1], + table_name=table_name, + timestamp=str(row[2]), + data=data, + ) + ) + + return measurements diff --git a/rest_api/src/sql.py b/rest_api/src/sql.py new file mode 100644 index 0000000..5ebda0c --- /dev/null +++ b/rest_api/src/sql.py @@ -0,0 +1,37 @@ +def fetch_sensors_statement(): + """Return SQL statements to fetch sensors from the database.""" + return """ + SELECT id, sensor_uid, sensor_name, application_id, created_at, updated_at + FROM sensors + """ + + +def fetch_applications_statement(): + """Return SQL statements to fetch applications from the database.""" + return """ + SELECT id, app_name, description, created_at, updated_at + FROM applications + """ + + +def fetch_sensor_app_info_statement(): + """Return SQL statements to fetch application_id and sensor_id from sensors table based on sensor_name.""" + return "SELECT application_id, id FROM sensors WHERE sensor_name = %s" + + +def fetch_table_names_from_app_id_statement(): + """Return SQL statements to fetch all measurement tables for a given application_id.""" + return """ + SELECT measurement_table_name + FROM application_measurements_registry + WHERE application_id = %s + """ + + +def fetch_measurements_for_sensor_statement(table_name: str): + """Return SQL statements to fetch all measurements for a given sensor_id from a specific table.""" + return f""" + SELECT * + FROM "{table_name}" + WHERE sensor_id = %s + """ diff --git a/rest_api/util/util.py b/rest_api/util/util.py new file mode 100644 index 0000000..7785868 --- /dev/null +++ b/rest_api/util/util.py @@ -0,0 +1,12 @@ +import orjson +from fastapi.encoders import jsonable_encoder +from fastapi.responses import ORJSONResponse + + +def get_pretty_json(content: dict): + """Return a pretty JSON response.""" + encoded = jsonable_encoder(content) + pretty_json = ORJSONResponse( + content=orjson.loads(orjson.dumps(encoded, option=orjson.OPT_INDENT_2)), + ) + return pretty_json From eddc4e9fa7e677df903eb522cec5d002552615d9 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 18 Jul 2025 13:47:00 +0200 Subject: [PATCH 19/28] docs: update changelog --- CHANGELOG.md | 8 +++++++ README.md | 65 +++------------------------------------------------- 2 files changed, 11 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6258a8f..5bc7305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,3 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] + +### [Added] + +- Ingest Service +- Processor Service +- REST API +- Mock Device into Ingest Service +- Docker compose system architecture diff --git a/README.md b/README.md index aaf1cb4..2b06fe9 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,4 @@ -# Irnas's Projects template +# Ajdovscina Cloud Software -IRNAS template for a GitHub repository. It comes with a -[basic group](https://github.com/IRNAS/irnas-workflows-software/tree/main/workflow-templates/basic) -of CI workflows for release automation. - -## Checklist - -### General GitHub setup - -- [ ] Provide a concise and accurate description of your project in the GitHub "description" field. -- [ ] Provide a concise and accurate description of your project in this `README.md` file, replace - the default title. -- [ ] Ensure that your project follows [repository naming scheme]. -- [ ] Add a project logo to the `README.md` file. The logo should be placed in the `doc/images` - folder. Use the following syntax to include it: - - ```markdown - ![Project Logo](doc/images/logo.svg) - ``` - -[repository naming scheme]: - https://github.com/IRNAS/irnas-guidelines-docs/blob/main/docs/github_projects_guidelines.md#repository-naming-scheme- - -### Tooling - -- [ ] Turn on `pre-commit` tool by running `pre-commit install`. If you do not have it yet, follow - instructions - [here](https://github.com/IRNAS/irnas-guidelines-docs/tree/main/tools/pre-commit). **Note**: - This tool is optional for mechanical and electronic projects. - -### Non-software projects - -- [ ] If needed re-read [GitHub for non-software projects] section. It describes what files should - be committed to the repository and what files should be added to the release assets. -- [ ] Create `assets` folder with files that will be part of the next release. The files in this - folder will be automatically compressed into a zip file and attached to the release assets - during the release process. The files should be added to the `assets` folder before creating - the first release. This step is **optional**, it depends on the type of your project. - -[GitHub for non-software projects]: - https://github.com/IRNAS/irnas-guidelines-docs/blob/main/docs/github_projects_guidelines.md#github-for-non-software-projects- - -### Cleanup - -- [ ] If you don't want to use `pre-commit` tool delete the following files: - - `.markdownlint.yaml` - - `.pre-commit-config.yaml` - - `.prettierrc.yaml` - - `committed.toml` - - `ruff.toml` - - `typos.toml` - - `.github/workflows/pre-commit.yaml` -- [ ] As a final step delete this checklist and commit changes. - -## Repository folder structure - -- `docs/` - Project and development documentation. -- `docs/images/` - Images used in the documentation, such as diagrams, screenshots, or logos. -- `assets/` - Files that will be part of the next release process. -- `CHANGELOG.md` - Document used for tracking changes in the project. It follows the - [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format. -- `.github` - GitHub Actions workflows and other GitHub related files. +This repository contains code related to the `ajdovscina` project, which is a system for sensor data +ingestion from various endpoints and a REST API to expose this data. From 7368cd463c663bd5493c9b4fc8d8f5f15ec2059f Mon Sep 17 00:00:00 2001 From: NejcKle Date: Mon, 11 Aug 2025 15:39:17 +0200 Subject: [PATCH 20/28] fix: PR changes --- Makefile | 6 + README.md | 108 +++++++++++++++++ common/authentication.py | 3 +- common/constants.py | 2 + ingest_service/models/ttn.py | 2 + ingest_service/util/formatter.py | 2 + mock/ingest_service/README.md | 8 +- mock/ingest_service/send.py | 2 - processor_service/main.py | 1 + processor_service/src/handlers.py | 26 +++-- processor_service/src/sql.py | 109 +++++++++++++----- rest_api/main.py | 27 ++--- .../models/{models.py => return_models.py} | 18 +++ rest_api/src/handlers.py | 12 +- rest_api/src/sql.py | 27 +++-- rest_api/util/util.py | 12 -- 16 files changed, 278 insertions(+), 87 deletions(-) rename rest_api/models/{models.py => return_models.py} (56%) delete mode 100644 rest_api/util/util.py diff --git a/Makefile b/Makefile index 35c4b32..8dde1a5 100644 --- a/Makefile +++ b/Makefile @@ -7,3 +7,9 @@ dev-up: dev-down: docker compose -f docker-compose-dev.yml -p ajdovscina-dev down --remove-orphans + +prod-up: + docker compose -f docker-compose-prod.yml -p ajdovscina-prod up --build + +prod-down: + docker compose -f docker-compose-prod.yml -p ajdovscina-prod down --remove-orphans diff --git a/README.md b/README.md index 2b06fe9..4cf97ae 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,111 @@ This repository contains code related to the `ajdovscina` project, which is a system for sensor data ingestion from various endpoints and a REST API to expose this data. + +## Architecture + +The system is designed with a microservices architecture, where each component is responsible for a +specific functionality. The main components include: + +- **Data Ingestion Service**: Responsible for collecting data from various sensor endpoints. +- **Data Processing Service**: Processes the ingested data, applying any necessary transformations + or aggregations. +- **API Gateway**: Exposes a REST API for clients to access the processed data. + +## Folder Structure + +```text +└── πŸ“ajdovscina-cloud-software + └── πŸ“alertmanager + └── πŸ“common + └── πŸ“logging + └── πŸ“dockerfiles + └── πŸ“docs + └── πŸ“architecture + β”œβ”€β”€ ajdovscina-architecture.drawio + └── πŸ“images + └── πŸ“grafana + └── πŸ“dashboards + └── πŸ“ingest_service + └── πŸ“models + └── πŸ“util + β”œβ”€β”€ Makefile + └── πŸ“init + β”œβ”€β”€ postgres.sql + └── πŸ“mock + └── πŸ“ingest_service + └── πŸ“processor_service + └── πŸ“src + └── πŸ“util + β”œβ”€β”€ Makefile + └── πŸ“prometheus + └── πŸ“rest_api + └── πŸ“models + └── πŸ“src + └── πŸ“util + β”œβ”€β”€ Makefile + β”œβ”€β”€ .gitignore + β”œβ”€β”€ .prettierrc + β”œβ”€β”€ committed.toml + β”œβ”€β”€ Makefile + β”œβ”€β”€ ruff.toml + └── typos.toml +``` + +## Deployment + +Deployment is done via `docker`. The system can be run in development or production mode. + +To start the system in development mode, use the following command: + +```bash +make dev-up +``` + +To stop the system in development mode, use: + +```bash +make dev-down +``` + +For production mode, use: + +```bash +make prod-up +``` + +And to stop the production system: + +```bash +make prod-down +``` + +> [!IMPORTANT] Each of the services can be deployed independently using their respective makefiles. + +### Configuration and Constants + +Before deploying the system, you need to configure certain environment variables and constants. This +can be done by creating a `constants.py` file in specific directories. + +The following constants need to be defined in these folders: + +#### common/constants.py + +The constants found in the common/constants.py file are used throughout the application for +consistent configuration. + +```python +import logging + +LOG_LEVEL = logging.INFO + +DB_CONFIG = { + "dbname": "", # Database name + "user": "", # Database user + "password": "", # Database password + "host": "", # Internal database host + "port": , # Internal port +} + +API_KEY = "your-secret-api-key" # Replace with your actual key +``` diff --git a/common/authentication.py b/common/authentication.py index 965c0e3..e740e37 100644 --- a/common/authentication.py +++ b/common/authentication.py @@ -1,7 +1,6 @@ from fastapi import Header, HTTPException, status -# TODO: Move to constants -API_KEY = "your-secret-api-key" # Replace with your actual key +from common.constants import API_KEY def verify_token(Authorization: str = Header(...)): diff --git a/common/constants.py b/common/constants.py index 691436b..9389e14 100644 --- a/common/constants.py +++ b/common/constants.py @@ -9,3 +9,5 @@ "host": "localhost", "port": 13328, } + +API_KEY = "your-secret-api-key" # Replace with your actual key diff --git a/ingest_service/models/ttn.py b/ingest_service/models/ttn.py index 80b7d1d..d150a07 100644 --- a/ingest_service/models/ttn.py +++ b/ingest_service/models/ttn.py @@ -1,3 +1,5 @@ +"""Maps TheThingsNetwork request structure to pydantic models.""" + from datetime import datetime from typing import Optional diff --git a/ingest_service/util/formatter.py b/ingest_service/util/formatter.py index 81d64db..01463aa 100644 --- a/ingest_service/util/formatter.py +++ b/ingest_service/util/formatter.py @@ -1,3 +1,5 @@ +"""Used to format data for API responses.""" + from models.general import DataOut from models.http import DataHTTP from models.ttn import DataTTN diff --git a/mock/ingest_service/README.md b/mock/ingest_service/README.md index 782c064..e311178 100644 --- a/mock/ingest_service/README.md +++ b/mock/ingest_service/README.md @@ -6,10 +6,10 @@ There are two examples available: ## send.py -This script mimics the data format forwarded by the Things Network (TTN) to the ingest service. It -generates a specified amount of mock devices with a specified number of mock applications and sends -the specified number of messages. Each message contains a random combination of the -device/application pair. +This script mimics the data format forwarded by the Things Network (TTN) or an arbitrary HTTP client +(such as NodeRED for example) to the ingest service. It generates a specified amount of mock devices +with a specified number of mock applications and sends the specified number of messages. Each +message contains a random combination of the device/application pair. To run it use diff --git a/mock/ingest_service/send.py b/mock/ingest_service/send.py index 8eed257..8170c12 100644 --- a/mock/ingest_service/send.py +++ b/mock/ingest_service/send.py @@ -4,7 +4,6 @@ """ import argparse -import json import random import time from datetime import datetime @@ -173,7 +172,6 @@ def generate_random_decoded_payload(): sensor_name=random.choice(device_ids), application=random.choice(application_ids), ) - serialized_data = json.dumps(data) response = requests.post(url, headers=headers, json=data) print(f"Sent message {count + 1}/{args.num_messages}") diff --git a/processor_service/main.py b/processor_service/main.py index ee23eef..d9f0a9d 100644 --- a/processor_service/main.py +++ b/processor_service/main.py @@ -33,6 +33,7 @@ data_to_insert = json.loads(byte_string.decode("utf-8")) + # Insert received data into the database insert_data(data_to_insert) consumer.commit_async() diff --git a/processor_service/src/handlers.py b/processor_service/src/handlers.py index 556e30d..599fc20 100644 --- a/processor_service/src/handlers.py +++ b/processor_service/src/handlers.py @@ -2,6 +2,7 @@ from datetime import datetime import psycopg2 +from psycopg2 import sql from src.sql import ( add_table_to_registry, alter_measurement_table, @@ -44,7 +45,7 @@ def insert_data(data: dict): meas_table_name = f"{app_name}_{version}" stmt, table_name = create_sensor_readings_table(meas_table_name, fields) - logger.debug(f"Create measurement table statement: {stmt}") + logger.debug(f"Creating measurement table for {meas_table_name}") logger.info( f"Creating measurement table {table_name} for application {app_name} with version {version}" ) @@ -52,20 +53,23 @@ def insert_data(data: dict): # 3.05 Get existing columns logger.info(f"Fetching existing columns for table {table_name}") - cur.execute( - f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}'" - ) + query = sql.SQL( + "SELECT column_name FROM information_schema.columns WHERE table_name = {}" + ).format(sql.Literal(table_name)) + cur.execute(query) existing_columns = {row[0].lower() for row in cur.fetchall()} logger.debug(f"Existing columns in table {table_name}: {existing_columns}") # 3.1 Alter the table to add new columns - stmt = alter_measurement_table(meas_table_name, fields, existing_columns) + alter_stmt = alter_measurement_table( + meas_table_name, fields, existing_columns + ) logger.info( f"Altering measurement table {table_name} to add new fields if necessary" ) - logger.debug(f"Alter table statement: {stmt}") - if stmt is not None: - cur.execute(stmt) + logger.debug("Preparing to alter table if needed") + if alter_stmt is not None: + cur.execute(alter_stmt) # 4. add_to_registry_stmt = add_table_to_registry() @@ -102,8 +106,10 @@ def insert_data(data: dict): logger.info( f"Inserting data into {table_name} for sensor {sensor_name} with ID {sens_id}" ) - logger.debug(f"Insert data statement: {insert_data_stmt}") - cur.execute(insert_data_stmt, (timestamp, sens_id, *fields.values())) + logger.debug("Preparing to insert data") + # Create a complete list of values, starting with timestamp and sensor_id + values = [timestamp, sens_id] + list(fields.values()) + cur.execute(insert_data_stmt, values) conn.commit() except Exception as e: diff --git a/processor_service/src/sql.py b/processor_service/src/sql.py index 0c63cd6..d0ca22c 100644 --- a/processor_service/src/sql.py +++ b/processor_service/src/sql.py @@ -5,6 +5,8 @@ from datetime import datetime +from psycopg2 import sql + """The flow is as follows: 1. Application data is received from device @@ -12,7 +14,6 @@ 3. A new _ table is created if it doesn't yet exist 4. The pair is added to the application_measurements_registry 5. Measurements are added to the _ table -6. Measurements are added to the `measurements` table """ @@ -30,12 +31,21 @@ def insert_sensor(): def insert_sensor_readings(table_name: str, keys: list): """Creates a SQL INSERT INTO statement based on the name and field.""" - columns = "timestamp, sensor_id, " - columns += ", ".join(keys) - values_placeholders = "%s, %s, " - values_placeholders += ", ".join(["%s" for _ in keys]) + # Create a list of all columns including timestamp and sensor_id + all_columns = ["timestamp", "sensor_id"] + keys + + # Create placeholders for all values + placeholders = sql.SQL(", ").join( + [sql.Placeholder() for _ in range(len(all_columns))] + ) - query = f'INSERT INTO "{table_name}" ({columns}) VALUES ({values_placeholders})' + # Format column names safely + columns_sql = sql.SQL(", ").join([sql.Identifier(col) for col in all_columns]) + + # Build the query safely + query = sql.SQL("INSERT INTO {} ({}) VALUES ({})").format( + sql.Identifier(table_name), columns_sql, placeholders + ) return query @@ -48,15 +58,28 @@ def create_sensor_readings_table(application_name: str, fields: dict): fields (dict): A dictionary containing field names and their corresponding values. Returns: - str: A SQL CREATE TABLE statement. + tuple: A SQL CREATE TABLE statement and the sanitized table name. """ table_name = application_name.replace(" ", "_").lower() - columns = [] - columns.append("id SERIAL PRIMARY KEY") - columns.append("sensor_id INTEGER REFERENCES sensors(id) ON DELETE CASCADE") - columns.append("timestamp TIMESTAMPTZ") + column_definitions = [] + + # Define standard columns + column_definitions.append(sql.SQL("id SERIAL PRIMARY KEY")) + column_definitions.append( + sql.SQL("sensor_id INTEGER REFERENCES sensors(id) ON DELETE CASCADE") + ) + column_definitions.append(sql.SQL("timestamp TIMESTAMPTZ")) + + # Set of columns we've already added to avoid duplicates + added_columns = set(["id", "sensor_id", "timestamp"]) for field, value in fields.items(): + field_lower = field.lower() + + # Skip if we've already added this column + if field_lower in added_columns: + continue + if isinstance(value, int): col_type = "FLOAT" elif isinstance(value, float): @@ -65,27 +88,51 @@ def create_sensor_readings_table(application_name: str, fields: dict): col_type = "TEXT" elif isinstance(value, datetime): col_type = "TIMESTAMPTZ" + else: + # Default to TEXT for unknown types + col_type = "TEXT" - # Check if column might already exist - column = f"{field} {col_type}".lower() - if column in columns: - continue + # Add column definition + column_definitions.append( + sql.SQL("{} {}").format(sql.Identifier(field), sql.SQL(col_type)) + ) - columns.append(column) + # Track that we've added this column + added_columns.add(field_lower) - columns_str = ", ".join(columns) - create_table_statement = ( - f'CREATE TABLE IF NOT EXISTS "{table_name}" ({columns_str})' + # Join all column definitions + columns_sql = sql.SQL(", ").join(column_definitions) + + # Build the complete CREATE TABLE statement + create_table_statement = sql.SQL("CREATE TABLE IF NOT EXISTS {} ({})").format( + sql.Identifier(table_name), columns_sql ) return create_table_statement, table_name def alter_measurement_table(table_name, fields, existing_columns): - """A table can already exist but not all fields are present. If not all fields are present alter the table.""" - columns = [] + """A table can already exist but not all fields are present. If not all fields are present alter the table. + + Args: + table_name (str): The name of the table to alter + fields (dict): Dictionary of field names and their values + existing_columns (set): Set of existing column names (lowercase) + + Returns: + psycopg2.sql.Composed or None: SQL statement to alter table, or None if no changes needed + """ + # List to store column add operations + column_operations = [] for field, value in fields.items(): + field_lower = field.lower() + + # Skip if column already exists + if field_lower in existing_columns: + continue + + # Determine column type based on value type if isinstance(value, int): col_type = "FLOAT" elif isinstance(value, float): @@ -94,17 +141,23 @@ def alter_measurement_table(table_name, fields, existing_columns): col_type = "TEXT" elif isinstance(value, datetime): col_type = "TIMESTAMPTZ" + else: + # Default to TEXT for unknown types + col_type = "TEXT" - if field.lower() not in existing_columns: - columns.append(f"ADD COLUMN {field} {col_type},") + # Create add column operation + column_operations.append( + sql.SQL("ADD COLUMN {} {}").format(sql.Identifier(field), sql.SQL(col_type)) + ) - if len(columns) == 0: + # If no columns to add, return None + if not column_operations: return None - columns[-1] = columns[-1].rstrip(",") # Remove trailing comma from the last column - - columns_str = "\n".join(columns) - statement = f'ALTER TABLE "{table_name}"\n{columns_str};' + # Build the complete ALTER TABLE statement + statement = sql.SQL("ALTER TABLE {} {};").format( + sql.Identifier(table_name), sql.SQL(", ").join(column_operations) + ) return statement diff --git a/rest_api/main.py b/rest_api/main.py index e57adc6..89041a5 100644 --- a/rest_api/main.py +++ b/rest_api/main.py @@ -1,9 +1,11 @@ -from typing import List - import psycopg2 from fastapi import FastAPI from fastapi.responses import ORJSONResponse -from models.models import Application, Measurement, Sensor +from models.return_models import ( + ApplicationListResponse, + MeasurementListResponse, + SensorListResponse, +) from src.handlers import ( fetch_applications, fetch_measurements_for_sensor, @@ -11,7 +13,6 @@ fetch_sensors, fetch_table_names_from_app_id, ) -from util.util import get_pretty_json from common.constants import DB_CONFIG @@ -20,29 +21,29 @@ conn = psycopg2.connect(**DB_CONFIG) -@app.get("/api/sensors", response_model=List[Sensor]) +@app.get("/api/sensors", response_model=SensorListResponse) def get_sensors(): """Fetch all sensors from the database.""" sensors = fetch_sensors(conn) if sensors is None: - return ORJSONResponse(content={"sensors": []}) + return {"sensors": []} - return get_pretty_json({"sensors": sensors}) + return {"sensors": sensors} -@app.get("/api/applications", response_model=List[Application]) +@app.get("/api/applications", response_model=ApplicationListResponse) def get_applications(): """Fetch all applications from the database.""" applications = fetch_applications(conn) if applications is None: - return ORJSONResponse(content={"applications": []}) + return {"applications": []} - return get_pretty_json({"applications": applications}) + return {"applications": applications} -@app.get("/api/measurements", response_model=List[Measurement]) +@app.get("/api/measurements", response_model=MeasurementListResponse) def get_measurements(sensor_name): """Fetch measurements for a given sensor name.""" result = fetch_sensor_app_info(conn, sensor_name) @@ -53,7 +54,7 @@ def get_measurements(sensor_name): tables = fetch_table_names_from_app_id(conn, application_id) if not tables: - return ORJSONResponse(content={"measurements": []}) + return {"measurements": []} measurements = [] for table_row in tables: @@ -64,4 +65,4 @@ def get_measurements(sensor_name): if table_measurements: measurements.extend(table_measurements) - return get_pretty_json({"measurements": measurements}) + return {"measurements": measurements} diff --git a/rest_api/models/models.py b/rest_api/models/return_models.py similarity index 56% rename from rest_api/models/models.py rename to rest_api/models/return_models.py index dc03876..496e060 100644 --- a/rest_api/models/models.py +++ b/rest_api/models/return_models.py @@ -11,6 +11,12 @@ class Measurement(BaseModel): data: dict # dynamic columns from the database +class MeasurementListResponse(BaseModel): + """Data model for a list of sensor measurements.""" + + measurements: list[Measurement] + + class Sensor(BaseModel): """Data model for sensor information.""" @@ -18,8 +24,20 @@ class Sensor(BaseModel): application_id: int +class SensorListResponse(BaseModel): + """Data model for a list of sensors.""" + + sensors: list[Sensor] + + class Application(BaseModel): """Data model for application information.""" app_name: str description: Optional[str] = None + + +class ApplicationListResponse(BaseModel): + """Data model for a list of applications.""" + + applications: list[Application] diff --git a/rest_api/src/handlers.py b/rest_api/src/handlers.py index 2017297..bf318a3 100644 --- a/rest_api/src/handlers.py +++ b/rest_api/src/handlers.py @@ -1,4 +1,5 @@ -from models.models import Application, Measurement, Sensor +from models.return_models import Application, Measurement, Sensor +from psycopg2 import sql from src.sql import ( fetch_applications_statement, fetch_measurements_for_sensor_statement, @@ -50,7 +51,8 @@ def fetch_applications(conn): def fetch_sensor_app_info(conn, sensor_name: str) -> tuple[int, int] | None: """Fetch application_id and sensor_id from sensors table based on sensor_name.""" with conn.cursor() as cur: - cur.execute(fetch_sensor_app_info_statement(), (sensor_name,)) + query = fetch_sensor_app_info_statement().format(sql.Placeholder()) + cur.execute(query, (sensor_name,)) result = cur.fetchone() if result is None: @@ -63,7 +65,8 @@ def fetch_sensor_app_info(conn, sensor_name: str) -> tuple[int, int] | None: def fetch_table_names_from_app_id(conn, app_id: int) -> list | None: """Fetch all measurement tables for a given application_id.""" with conn.cursor() as cur: - cur.execute(fetch_table_names_from_app_id_statement(), (app_id,)) + query = fetch_table_names_from_app_id_statement().format(sql.Placeholder()) + cur.execute(query, (app_id,)) table_names = cur.fetchall() return table_names if table_names else None @@ -72,7 +75,8 @@ def fetch_table_names_from_app_id(conn, app_id: int) -> list | None: def fetch_measurements_for_sensor(conn, table_name: str, sensor_id: int) -> list | None: """Fetch all measurements for a given sensor_id from a specific table.""" with conn.cursor() as cur: - cur.execute(fetch_measurements_for_sensor_statement(table_name), (sensor_id,)) + query = fetch_measurements_for_sensor_statement(table_name) + cur.execute(query, (sensor_id,)) rows = cur.fetchall() if not rows: diff --git a/rest_api/src/sql.py b/rest_api/src/sql.py index 5ebda0c..5ae7d82 100644 --- a/rest_api/src/sql.py +++ b/rest_api/src/sql.py @@ -1,37 +1,40 @@ +from psycopg2 import sql + + def fetch_sensors_statement(): """Return SQL statements to fetch sensors from the database.""" - return """ + return sql.SQL(""" SELECT id, sensor_uid, sensor_name, application_id, created_at, updated_at FROM sensors - """ + """) def fetch_applications_statement(): """Return SQL statements to fetch applications from the database.""" - return """ + return sql.SQL(""" SELECT id, app_name, description, created_at, updated_at FROM applications - """ + """) def fetch_sensor_app_info_statement(): """Return SQL statements to fetch application_id and sensor_id from sensors table based on sensor_name.""" - return "SELECT application_id, id FROM sensors WHERE sensor_name = %s" + return sql.SQL("SELECT application_id, id FROM sensors WHERE sensor_name = {}") def fetch_table_names_from_app_id_statement(): """Return SQL statements to fetch all measurement tables for a given application_id.""" - return """ + return sql.SQL(""" SELECT measurement_table_name FROM application_measurements_registry - WHERE application_id = %s - """ + WHERE application_id = {} + """) def fetch_measurements_for_sensor_statement(table_name: str): """Return SQL statements to fetch all measurements for a given sensor_id from a specific table.""" - return f""" + return sql.SQL(""" SELECT * - FROM "{table_name}" - WHERE sensor_id = %s - """ + FROM {} + WHERE sensor_id = {} + """).format(sql.Identifier(table_name), sql.Placeholder()) diff --git a/rest_api/util/util.py b/rest_api/util/util.py deleted file mode 100644 index 7785868..0000000 --- a/rest_api/util/util.py +++ /dev/null @@ -1,12 +0,0 @@ -import orjson -from fastapi.encoders import jsonable_encoder -from fastapi.responses import ORJSONResponse - - -def get_pretty_json(content: dict): - """Return a pretty JSON response.""" - encoded = jsonable_encoder(content) - pretty_json = ORJSONResponse( - content=orjson.loads(orjson.dumps(encoded, option=orjson.OPT_INDENT_2)), - ) - return pretty_json From ac4ca57e0afb9ad9b59b9618c87c98cc51ba7973 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Tue, 12 Aug 2025 12:51:17 +0200 Subject: [PATCH 21/28] feat: add healthcheck to all services --- common/constants.py | 11 ++++++++ ingest_service/constants.py | 4 +++ ingest_service/main.py | 41 ++++++++++++++++++++++++++++-- ingest_service/requirements.txt | 1 + processor_service/constants.py | 4 +++ processor_service/main.py | 41 +++++++++++++++++++++++++++++- processor_service/requirements.txt | 3 +++ rest_api/main.py | 28 +++++++++++++++++++- 8 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 ingest_service/constants.py create mode 100644 processor_service/constants.py diff --git a/common/constants.py b/common/constants.py index 9389e14..d8d343f 100644 --- a/common/constants.py +++ b/common/constants.py @@ -2,6 +2,8 @@ LOG_LEVEL = logging.INFO +# Localhost config +""" DB_CONFIG = { "dbname": "ajdovscina", "user": "ajdovscina-geospatial-user", @@ -9,5 +11,14 @@ "host": "localhost", "port": 13328, } +""" + +DB_CONFIG = { + "dbname": "ajdovscina", + "user": "ajdovscina-geospatial-user", + "password": "password", + "host": "geospatial-data", + "port": 5432, +} API_KEY = "your-secret-api-key" # Replace with your actual key diff --git a/ingest_service/constants.py b/ingest_service/constants.py new file mode 100644 index 0000000..3b848fa --- /dev/null +++ b/ingest_service/constants.py @@ -0,0 +1,4 @@ +import os + +localhost = os.environ.get("LOCALHOST") == "true" +KAFKA_HOST = "localhost:9992" if localhost else "kafka:9993" diff --git a/ingest_service/main.py b/ingest_service/main.py index b047947..31b550e 100644 --- a/ingest_service/main.py +++ b/ingest_service/main.py @@ -1,8 +1,10 @@ +import time import traceback +from constants import KAFKA_HOST from fastapi import FastAPI, Header, HTTPException, Request, Response, status from fastapi.exceptions import RequestValidationError -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, PlainTextResponse from kafka import KafkaProducer from models.http import DataHTTP @@ -18,7 +20,7 @@ # Initialize the Kafka producer with a custom serializer producer = KafkaProducer( - bootstrap_servers="localhost:9992", + bootstrap_servers=KAFKA_HOST, key_serializer=str.encode, value_serializer=custom_value_serializer, ) @@ -26,6 +28,10 @@ # Prepare the app app = FastAPI() +# Health check constants +HEALTH_CHECK_INTERVAL_S = 300 # 5 minutes +last_update_time = time.time() + # Add custom exception handler to print more information about the 422 error @app.exception_handler(RequestValidationError) @@ -55,6 +61,10 @@ async def ingest_data(data: DataHTTP, Authorization: str = Header(...)): producer.send("process_data", key="http_service", value=data_to_insert.model_dump()) + # Update last_update_time after successful processing + global last_update_time + last_update_time = time.time() + return Response(status_code=200) @@ -74,5 +84,32 @@ async def ingest_data_ttn(data: DataTTN, Authorization: str = Header(...)): producer.send("process_data", key="ttn_service", value=data_to_insert.model_dump()) + # Update last_update_time after successful processing + global last_update_time + last_update_time = time.time() + # Process data here (e.g., store in DB) return Response(status_code=200) + + +@app.get("/health-check", response_class=PlainTextResponse) +def health_check(): + """Check if the service is running properly.""" + global last_update_time + + # Update the last update time to indicate the service is alive + current_time = time.time() + + # Check if service has been inactive for too long + if current_time - last_update_time > HEALTH_CHECK_INTERVAL_S: + return PlainTextResponse(content="Service has been inactive", status_code=500) + + # Update last_update_time + last_update_time = current_time + + # Check Kafka producer connection + try: + producer.metrics() # Get metrics to check if connection is alive + return PlainTextResponse(content="OK", status_code=200) + except Exception as e: + return PlainTextResponse(content=f"Service error: {str(e)}", status_code=500) diff --git a/ingest_service/requirements.txt b/ingest_service/requirements.txt index feb6772..d970352 100644 --- a/ingest_service/requirements.txt +++ b/ingest_service/requirements.txt @@ -2,3 +2,4 @@ fastapi[all]==0.116.1 psycopg2==2.9.10 sqlalchemy==2.0.41 requests==2.32.4 +kafka-python==2.0.2 diff --git a/processor_service/constants.py b/processor_service/constants.py new file mode 100644 index 0000000..3b848fa --- /dev/null +++ b/processor_service/constants.py @@ -0,0 +1,4 @@ +import os + +localhost = os.environ.get("LOCALHOST") == "true" +KAFKA_HOST = "localhost:9992" if localhost else "kafka:9993" diff --git a/processor_service/main.py b/processor_service/main.py index d9f0a9d..4852eec 100644 --- a/processor_service/main.py +++ b/processor_service/main.py @@ -1,14 +1,27 @@ import json import logging +import time +from threading import Thread +import uvicorn +from constants import KAFKA_HOST +from fastapi import FastAPI +from fastapi.responses import PlainTextResponse from kafka import KafkaConsumer from src.handlers import insert_data from common.logging.logger import Logger +# Health check constants +HEALTH_CHECK_INTERVAL_S = 300 # 5 minutes +last_update_time = time.time() + +# Create FastAPI app for health check endpoint +app = FastAPI() + consumer = KafkaConsumer( "process_data", - bootstrap_servers="localhost:9992", + bootstrap_servers=KAFKA_HOST, auto_offset_reset="earliest", group_id="ajdovscina-consumer", max_poll_interval_ms=900000, @@ -16,6 +29,29 @@ logger = Logger(logging.getLogger(__name__)) + +@app.get("/health-check", response_class=PlainTextResponse) +def health_check(): + """Check if the service is running.""" + global last_update_time + current_time = time.time() + + # Check if the service has been active recently + if current_time - last_update_time > HEALTH_CHECK_INTERVAL_S: + return PlainTextResponse(content="Service is not running", status_code=500) + + # Update the last update time + last_update_time = current_time + return PlainTextResponse(content="OK", status_code=200) + + +# Start uvicorn server in a separate thread +Thread( + target=uvicorn.run, + args=(app,), + kwargs={"host": "0.0.0.0", "port": 6000, "log_level": "info"}, +).start() + for message in consumer: logger.debug( "%s:%d:%d: key=%s value=%s" @@ -36,4 +72,7 @@ # Insert received data into the database insert_data(data_to_insert) + # Update the last update time after successful processing + last_update_time = time.time() + consumer.commit_async() diff --git a/processor_service/requirements.txt b/processor_service/requirements.txt index 7aedb58..0f3a8cc 100644 --- a/processor_service/requirements.txt +++ b/processor_service/requirements.txt @@ -1 +1,4 @@ kafka-python==2.0.2 +fastapi[all]==0.116.1 +uvicorn==0.22.0 +psycopg2==2.9.10 diff --git a/rest_api/main.py b/rest_api/main.py index 89041a5..cf3427b 100644 --- a/rest_api/main.py +++ b/rest_api/main.py @@ -1,6 +1,8 @@ +import time + import psycopg2 from fastapi import FastAPI -from fastapi.responses import ORJSONResponse +from fastapi.responses import ORJSONResponse, PlainTextResponse from models.return_models import ( ApplicationListResponse, MeasurementListResponse, @@ -16,6 +18,10 @@ from common.constants import DB_CONFIG +# Health check constants +HEALTH_CHECK_INTERVAL_S = 300 # 5 minutes +last_update_time = time.time() + app = FastAPI(default_response_class=ORJSONResponse) conn = psycopg2.connect(**DB_CONFIG) @@ -66,3 +72,23 @@ def get_measurements(sensor_name): measurements.extend(table_measurements) return {"measurements": measurements} + + +@app.get("/health-check", response_class=PlainTextResponse) +def health_check(): + """Check if the service is running properly.""" + global last_update_time + + # Update the last update time to indicate the service is alive + current_time = time.time() + last_update_time = current_time + + # Check database connection + try: + with conn.cursor() as cur: + cur.execute("SELECT 1") + return PlainTextResponse(content="OK", status_code=200) + except Exception as e: + return PlainTextResponse( + content=f"Service database error: {str(e)}", status_code=500 + ) From 0c68260cab2f3a16006fc2e2ccffa40b4e143289 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Tue, 12 Aug 2025 12:51:34 +0200 Subject: [PATCH 22/28] feat: add docker compose for production --- docker-compose-prod.yml | 277 +++++++++++++++++++++++ dockerfiles/Dockerfile.ingest-service | 17 ++ dockerfiles/Dockerfile.processor-service | 14 ++ dockerfiles/Dockerfile.rest-api | 17 ++ 4 files changed, 325 insertions(+) create mode 100644 docker-compose-prod.yml create mode 100644 dockerfiles/Dockerfile.ingest-service create mode 100644 dockerfiles/Dockerfile.processor-service create mode 100644 dockerfiles/Dockerfile.rest-api diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 0000000..84c3045 --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,277 @@ +services: + + ingest-service: + container_name: ajdovscina-ingest-service + build: + context: . + dockerfile: ./dockerfiles/Dockerfile.ingest-service + # flask requires SIGINT to stop gracefully + # (default stop signal from Compose is SIGTERM) + environment: + - ENV=development + - LOCALHOST=false + - PYTHONUNBUFFERED=1 + logging: + driver: "local" + options: + max-size: "50M" + max-file: "3" + stop_signal: SIGINT + labels: + - "autoheal=true" + healthcheck: + test: curl --fail http://localhost:5000/health-check || exit 1 + interval: 60s + timeout: 30s + retries: 3 + start_period: 30s + depends_on: + kafka: + condition: service_healthy + geospatial-data: + condition: service_healthy + ports: + - 40005:5000 + expose: + - 40005 + + rest-api: + container_name: ajdovscina-rest-api + build: + context: . + dockerfile: ./dockerfiles/Dockerfile.rest-api + # flask requires SIGINT to stop gracefully + # (default stop signal from Compose is SIGTERM) + environment: + - ENV=development + - LOCALHOST=false + - PYTHONUNBUFFERED=1 + logging: + driver: "local" + options: + max-size: "50M" + max-file: "3" + stop_signal: SIGINT + labels: + - "autoheal=true" + healthcheck: + test: curl --fail http://localhost:5005/health-check || exit 1 + interval: 60s + timeout: 30s + retries: 3 + start_period: 30s + depends_on: + kafka: + condition: service_healthy + geospatial-data: + condition: service_healthy + ports: + - 41005:5005 + expose: + - 41005 + + ajdovscina-processor: + container_name: ajdovscina-processor + build: + context: . + dockerfile: ./dockerfiles/Dockerfile.processor-service + environment: + - PYTHONUNBUFFERED=1 + - LOCALHOST=false + logging: + driver: "local" + options: + max-size: "50M" + max-file: "3" + stop_signal: SIGINT + labels: + - "autoheal=true" + healthcheck: + test: curl --fail http://localhost:6000/health-check || exit 1 + interval: 60s + timeout: 30s + retries: 3 + start_period: 30s + depends_on: + kafka: + condition: service_healthy + + nodered: + image: nodered/node-red:3.1.15-minimal + container_name: ajdovscina-nodered + ports: + - "11880:1880" # Exposes Node-RED UI on http://localhost:1880 + volumes: + - nodered-data:/data # Persist flows and configuration + restart: unless-stopped + + geospatial-data: + container_name: ajdovscina-geospatial-data + image: timescale/timescaledb-ha:pg17-all-amd64 + restart: always + environment: + POSTGRES_DB: "ajdovscina" + POSTGRES_USER: "ajdovscina-geospatial-user" + POSTGRES_PASSWORD: "password" + ports: + - "13328:5432" + volumes: + - ajdovscina-geospatial-db:/var/lib/postgresql/data:rw + - ./init/postgres.sql:/docker-entrypoint-initdb.d/1.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ajdovscina-geospatial-user -d ajdovscina"] + timeout: 2s + retries: 2 + + geospatial-data-exporter: + container_name: ajdovscina-geospatial-exporter + image: quay.io/prometheuscommunity/postgres-exporter:v0.17.0 + environment: + - DATA_SOURCE_NAME=postgresql://ajdovscina-geospatial-user:password@geospatial-data:5432/ajdovscina?sslmode=disable + ports: + - 9204:9187 + links: + - geospatial-data + depends_on: + - geospatial-data + + grafana: + container_name: ajdovscina-grafana + image: grafana/grafana-enterprise:10.2.2 + restart: always + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=password + - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/dashboard.json + ports: + - "13201:3000" + volumes: + - grafana-volume:/var/lib/grafana + + prometheus: + container_name: ajdovscina-prometheus + image: prom/prometheus:v3.1.0 + restart: always + volumes: + - ./prometheus:/etc/prometheus + - prometheus-volume:/prometheus + command: + - "--config.file=/etc/prometheus/prometheus-dev.yml" + - "--log.level=debug" + ports: + - 19290:9090 + + node-exporter: + container_name: ajdovscina-node-exporter + image: prom/node-exporter:v1.8.2 + restart: always + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.rootfs=/rootfs' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + ports: + - "19200:9100" + + autoheal: + container_name: ajdovscina-autoheal + image: willfarrell/autoheal:latest + tty: true + restart: always + environment: + - AUTOHEAL_INTERVAL=60 + - AUTOHEAL_START_PERIOD=120 + - AUTOHEAL_DEFAULT_STOP_TIMEOUT=10 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + alert-manager: + image: prom/alertmanager:v0.28.0 + volumes: + - "./alertmanager:/config" # Mount local alertmanager config to container + - alertmanager:/data + ports: + - 19193:9093 + command: + - "--config.file=/config/alertmanager.yml" # Start alertmanager with config + - "--log.level=debug" + + zookeeper: + image: confluentinc/cp-zookeeper:7.8.0 + ports: + - "2281:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + healthcheck: + test: nc -z localhost 2181 || exit -1 + interval: 10s + timeout: 5s + retries: 3 + labels: + - "autoheal=true" + volumes: + - zookeeper-data:/var/lib/zookeeper/data + - zookeeper-log:/var/lib/zookeeper/log + + kafka: + image: confluentinc/cp-kafka:7.8.0 + container_name: ajdovscina-kafka + environment: + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9993,OUTSIDE://localhost:9992 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_LISTENERS: INSIDE://0.0.0.0:9993,OUTSIDE://0.0.0.0:9992 + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + ports: + - "9992:9992" + expose: + - "9993" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - kafka-data:/var/lib/kafka/data + depends_on: + - zookeeper + healthcheck: + test: kafka-topics --bootstrap-server localhost:9992 --list + interval: 5s + timeout: 10s + retries: 5 + labels: + - "autoheal=true" + + kafka-exporter: + image: danielqsj/kafka-exporter:v1.8.0 + command: ["--kafka.server=kafka:9993"] + depends_on: + kafka: + condition: service_healthy + ports: + - 9328:9308 + + postgres-exporter: + image: quay.io/prometheuscommunity/postgres-exporter:v0.16.0 + container_name: ajdovscina-postgres-exporter + environment: + - DATA_SOURCE_NAME=postgresql://username:password@172.17.0.1:5432/postgres?sslmode=disable + ports: + - 9287:9187 + + +volumes: + grafana-volume: + + prometheus-volume: + alertmanager: + + ajdovscina-geospatial-db: + + nodered-data: + + kafka-data: + zookeeper-data: + zookeeper-log: diff --git a/dockerfiles/Dockerfile.ingest-service b/dockerfiles/Dockerfile.ingest-service new file mode 100644 index 0000000..f82c2b5 --- /dev/null +++ b/dockerfiles/Dockerfile.ingest-service @@ -0,0 +1,17 @@ +FROM python:3.10 + +WORKDIR /ingest_service + +# Install dependencies +COPY ingest_service/requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +# Copy application code +COPY ingest_service/ . +COPY common/ ./common/ + +# Expose the port that uvicorn will run on +EXPOSE 5000 + +# Command to run the service +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"] diff --git a/dockerfiles/Dockerfile.processor-service b/dockerfiles/Dockerfile.processor-service new file mode 100644 index 0000000..24fff0b --- /dev/null +++ b/dockerfiles/Dockerfile.processor-service @@ -0,0 +1,14 @@ +FROM python:3.10 + +WORKDIR /processor_service + +# Install dependencies +COPY processor_service/requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +# Copy application code +COPY processor_service/ . +COPY common/ ./common/ + +# Command to run the service +CMD ["python3", "main.py"] diff --git a/dockerfiles/Dockerfile.rest-api b/dockerfiles/Dockerfile.rest-api new file mode 100644 index 0000000..40a8c93 --- /dev/null +++ b/dockerfiles/Dockerfile.rest-api @@ -0,0 +1,17 @@ +FROM python:3.10 + +WORKDIR /rest_api + +# Install dependencies +COPY rest_api/requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +# Copy application code +COPY rest_api/ . +COPY common/ ./common/ + +# Expose the port that uvicorn will run on +EXPOSE 51005 + +# Command to run the service +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5005"] From 9322307f602185bedfddc54ad20980a79f685fc4 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Tue, 12 Aug 2025 13:08:12 +0200 Subject: [PATCH 23/28] feat: add data view Grafana dashboard --- grafana/dashboards/data-view.json | 288 ++++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 grafana/dashboards/data-view.json diff --git a/grafana/dashboards/data-view.json b/grafana/dashboards/data-view.json new file mode 100644 index 0000000..735b335 --- /dev/null +++ b/grafana/dashboards/data-view.json @@ -0,0 +1,288 @@ +{ + "__inputs": [ + { + "name": "DS_POSTGRESQL", + "label": "PostgreSQL", + "description": "", + "type": "datasource", + "pluginId": "postgres", + "pluginName": "PostgreSQL" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.2.2" + }, + { + "type": "datasource", + "id": "postgres", + "name": "PostgreSQL", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "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": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "postgres", + "uid": "${DS_POSTGRESQL}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 20, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "${DS_POSTGRESQL}" + }, + "editorMode": "builder", + "format": "table", + "rawSql": "SELECT * FROM \"irnas-ranger-test_v0\" LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [ + { + "name": "*", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + }, + "table": "\"irnas-ranger-test_v0\"" + } + ], + "title": "irnas-ranger-test app data", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "${DS_POSTGRESQL}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "${DS_POSTGRESQL}" + }, + "editorMode": "builder", + "format": "table", + "rawSql": "SELECT measurement_table_name FROM application_measurements_registry LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [ + { + "name": "measurement_table_name", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + }, + "table": "application_measurements_registry" + } + ], + "title": "Data Tables", + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "New dashboard", + "uid": "d5966a3d-6010-4dce-a9fb-c6dcf1209afc", + "version": 1, + "weekStart": "" +} From e9fb2bfb34634f3aeda07359d761f2e0c194fc61 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Wed, 13 Aug 2025 13:22:00 +0200 Subject: [PATCH 24/28] fix: use database connection pooling, add cleanup --- processor_service/main.py | 51 +++++++++++++++++++++++++++---- processor_service/src/handlers.py | 19 +++++++++--- rest_api/main.py | 42 ++++++++++++++++++++----- 3 files changed, 93 insertions(+), 19 deletions(-) diff --git a/processor_service/main.py b/processor_service/main.py index 4852eec..f7c6567 100644 --- a/processor_service/main.py +++ b/processor_service/main.py @@ -1,6 +1,8 @@ +import atexit import json import logging import time +from contextlib import asynccontextmanager from threading import Thread import uvicorn @@ -8,7 +10,7 @@ from fastapi import FastAPI from fastapi.responses import PlainTextResponse from kafka import KafkaConsumer -from src.handlers import insert_data +from src.handlers import connection_pool, insert_data from common.logging.logger import Logger @@ -16,8 +18,22 @@ HEALTH_CHECK_INTERVAL_S = 300 # 5 minutes last_update_time = time.time() -# Create FastAPI app for health check endpoint -app = FastAPI() + +# Define lifespan context manager for the app +@asynccontextmanager +async def lifespan(app): + """Method to manage startup and cleanup of the application.""" + # Startup logic, if any + yield + # Shutdown logic - close database connections + logger.info("Shutting down - Closing database connection pool...") + if connection_pool: + connection_pool.closeall() + logger.info("Database connection pool closed.") + + +# Create FastAPI app for health check endpoint with the lifespan +app = FastAPI(lifespan=lifespan) consumer = KafkaConsumer( "process_data", @@ -40,9 +56,20 @@ def health_check(): if current_time - last_update_time > HEALTH_CHECK_INTERVAL_S: return PlainTextResponse(content="Service is not running", status_code=500) - # Update the last update time - last_update_time = current_time - return PlainTextResponse(content="OK", status_code=200) + # Check database connection + try: + conn = connection_pool.getconn() + with conn.cursor() as cur: + cur.execute("SELECT 1") + connection_pool.putconn(conn) + # Update the last update time + last_update_time = current_time + return PlainTextResponse(content="OK", status_code=200) + except Exception as e: + logger.error(f"Database connection error: {e}") + return PlainTextResponse( + content=f"Service database error: {str(e)}", status_code=500 + ) # Start uvicorn server in a separate thread @@ -52,6 +79,18 @@ def health_check(): kwargs={"host": "0.0.0.0", "port": 6000, "log_level": "info"}, ).start() + +def close_pool(): + """Close all connections in the pool when application exits.""" + if connection_pool: + logger.info("Closing database connection pool...") + connection_pool.closeall() + logger.info("Database connection pool closed.") + + +# Register the function to be called on exit (for non-FastAPI shutdown scenarios) +atexit.register(close_pool) + for message in consumer: logger.debug( "%s:%d:%d: key=%s value=%s" diff --git a/processor_service/src/handlers.py b/processor_service/src/handlers.py index 599fc20..730f602 100644 --- a/processor_service/src/handlers.py +++ b/processor_service/src/handlers.py @@ -1,8 +1,7 @@ import logging from datetime import datetime -import psycopg2 -from psycopg2 import sql +from psycopg2 import pool, sql from src.sql import ( add_table_to_registry, alter_measurement_table, @@ -16,14 +15,16 @@ from common.constants import DB_CONFIG from common.logging.logger import Logger -# Create connection to the PostgreSQL database -conn = psycopg2.connect(**DB_CONFIG) +# Create a connection pool instead of a single connection +connection_pool = pool.ThreadedConnectionPool(minconn=1, maxconn=20, **DB_CONFIG) logger = Logger(logging.getLogger(__name__)) def insert_data(data: dict): """Insert data into SQL database.""" + # Get a connection from the pool + conn = connection_pool.getconn() try: with conn.cursor() as cur: # 1. @@ -41,6 +42,7 @@ def insert_data(data: dict): fields = drop_duplicates(fields) if fields is None or len(fields) == 0: + connection_pool.putconn(conn) # Return connection to the pool return meas_table_name = f"{app_name}_{version}" @@ -112,6 +114,13 @@ def insert_data(data: dict): cur.execute(insert_data_stmt, values) conn.commit() + connection_pool.putconn( + conn + ) # Return connection to the pool after successful commit except Exception as e: - conn.rollback() + if conn: + conn.rollback() + connection_pool.putconn( + conn + ) # Return connection to the pool even if there's an error logger.error(f"Failed to insert data: {e}") diff --git a/rest_api/main.py b/rest_api/main.py index cf3427b..4e463c2 100644 --- a/rest_api/main.py +++ b/rest_api/main.py @@ -1,13 +1,14 @@ import time +from contextlib import asynccontextmanager -import psycopg2 -from fastapi import FastAPI +from fastapi import Depends, FastAPI from fastapi.responses import ORJSONResponse, PlainTextResponse from models.return_models import ( ApplicationListResponse, MeasurementListResponse, SensorListResponse, ) +from psycopg2 import pool from src.handlers import ( fetch_applications, fetch_measurements_for_sensor, @@ -22,13 +23,38 @@ HEALTH_CHECK_INTERVAL_S = 300 # 5 minutes last_update_time = time.time() -app = FastAPI(default_response_class=ORJSONResponse) +# Create a connection pool +connection_pool = pool.ThreadedConnectionPool(minconn=1, maxconn=20, **DB_CONFIG) -conn = psycopg2.connect(**DB_CONFIG) + +# Define lifespan context manager for the app +@asynccontextmanager +async def lifespan(app): + """Method to manage startup and cleanup of the application.""" + # Startup logic, if any + yield + # Shutdown logic - close database connections + if connection_pool: + connection_pool.closeall() + + +# Create FastAPI app with the lifespan +app = FastAPI(default_response_class=ORJSONResponse, lifespan=lifespan) + + +# Dependency to get a connection from the pool +def get_db_connection(): + """Get a database connection from the pool.""" + conn = connection_pool.getconn() + try: + yield conn + finally: + # Return the connection to the pool + connection_pool.putconn(conn) @app.get("/api/sensors", response_model=SensorListResponse) -def get_sensors(): +def get_sensors(conn=Depends(get_db_connection)): """Fetch all sensors from the database.""" sensors = fetch_sensors(conn) @@ -39,7 +65,7 @@ def get_sensors(): @app.get("/api/applications", response_model=ApplicationListResponse) -def get_applications(): +def get_applications(conn=Depends(get_db_connection)): """Fetch all applications from the database.""" applications = fetch_applications(conn) @@ -50,7 +76,7 @@ def get_applications(): @app.get("/api/measurements", response_model=MeasurementListResponse) -def get_measurements(sensor_name): +def get_measurements(sensor_name, conn=Depends(get_db_connection)): """Fetch measurements for a given sensor name.""" result = fetch_sensor_app_info(conn, sensor_name) if not result: @@ -75,7 +101,7 @@ def get_measurements(sensor_name): @app.get("/health-check", response_class=PlainTextResponse) -def health_check(): +def health_check(conn=Depends(get_db_connection)): """Check if the service is running properly.""" global last_update_time From 9063067a1ecc516fc30d078840af6ceb1d090976 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Thu, 14 Aug 2025 10:56:14 +0200 Subject: [PATCH 25/28] feat: add FastAPI HTTPBearer authentication --- common/authentication.py | 18 +++++++++--------- common/constants.py | 17 +++++------------ ingest_service/main.py | 10 +++------- rest_api/main.py | 40 +++++++++++++++++++++++----------------- rest_api/util/db.py | 9 +++++++++ 5 files changed, 49 insertions(+), 45 deletions(-) create mode 100644 rest_api/util/db.py diff --git a/common/authentication.py b/common/authentication.py index e740e37..aa727a4 100644 --- a/common/authentication.py +++ b/common/authentication.py @@ -1,17 +1,17 @@ -from fastapi import Header, HTTPException, status +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from common.constants import API_KEY +security = HTTPBearer() -def verify_token(Authorization: str = Header(...)): + +def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): """Verify the API key from the Authorization header.""" - if not Authorization.startswith("Bearer "): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication scheme", - ) - token = Authorization.removeprefix("Bearer ").strip() + token = credentials.credentials if token != API_KEY: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key" + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid API key", ) + return token diff --git a/common/constants.py b/common/constants.py index d8d343f..fe0f2b0 100644 --- a/common/constants.py +++ b/common/constants.py @@ -1,24 +1,17 @@ import logging +import os LOG_LEVEL = logging.INFO -# Localhost config -""" -DB_CONFIG = { - "dbname": "ajdovscina", - "user": "ajdovscina-geospatial-user", - "password": "password", - "host": "localhost", - "port": 13328, -} -""" +localhost = os.environ.get("LOCALHOST") == "true" +# Localhost config DB_CONFIG = { "dbname": "ajdovscina", "user": "ajdovscina-geospatial-user", "password": "password", - "host": "geospatial-data", - "port": 5432, + "host": "localhost" if localhost else "geospatial-data", + "port": 13328 if localhost else 5432, } API_KEY = "your-secret-api-key" # Replace with your actual key diff --git a/ingest_service/main.py b/ingest_service/main.py index 31b550e..369f4b5 100644 --- a/ingest_service/main.py +++ b/ingest_service/main.py @@ -2,7 +2,7 @@ import traceback from constants import KAFKA_HOST -from fastapi import FastAPI, Header, HTTPException, Request, Response, status +from fastapi import Depends, FastAPI, HTTPException, Request, Response, status from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse, PlainTextResponse from kafka import KafkaProducer @@ -47,10 +47,8 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE @app.post("/http") -async def ingest_data(data: DataHTTP, Authorization: str = Header(...)): +async def ingest_data(data: DataHTTP, token: str = Depends(verify_token)): """Ingest data from any source. This is a generic endpoint that can be used to ingest data from various sources.""" - verify_token(Authorization) - try: data_to_insert = format_http_data_for_processing(data) except Exception as e: @@ -69,10 +67,8 @@ async def ingest_data(data: DataHTTP, Authorization: str = Header(...)): @app.post("/ttn") -async def ingest_data_ttn(data: DataTTN, Authorization: str = Header(...)): +async def ingest_data_ttn(data: DataTTN, token: str = Depends(verify_token)): """Ingest data from TTN webhook.""" - verify_token(Authorization) - data_to_insert = None try: data_to_insert = format_ttn_data_for_processing(data) diff --git a/rest_api/main.py b/rest_api/main.py index 4e463c2..f5ad8be 100644 --- a/rest_api/main.py +++ b/rest_api/main.py @@ -2,7 +2,7 @@ from contextlib import asynccontextmanager from fastapi import Depends, FastAPI -from fastapi.responses import ORJSONResponse, PlainTextResponse +from fastapi.responses import ORJSONResponse, PlainTextResponse, RedirectResponse from models.return_models import ( ApplicationListResponse, MeasurementListResponse, @@ -16,7 +16,9 @@ fetch_sensors, fetch_table_names_from_app_id, ) +from util.db import get_db_connection +from common.authentication import verify_token from common.constants import DB_CONFIG # Health check constants @@ -42,19 +44,15 @@ async def lifespan(app): app = FastAPI(default_response_class=ORJSONResponse, lifespan=lifespan) -# Dependency to get a connection from the pool -def get_db_connection(): - """Get a database connection from the pool.""" - conn = connection_pool.getconn() - try: - yield conn - finally: - # Return the connection to the pool - connection_pool.putconn(conn) +# Define all routes +@app.get("/", include_in_schema=False) +def redirect_to_docs(): + """Redirect root path to the API documentation.""" + return RedirectResponse(url="/docs") -@app.get("/api/sensors", response_model=SensorListResponse) -def get_sensors(conn=Depends(get_db_connection)): +@app.get("/api/sensors", response_model=SensorListResponse, tags=["sensors"]) +def get_sensors(conn=Depends(get_db_connection), token: str = Depends(verify_token)): """Fetch all sensors from the database.""" sensors = fetch_sensors(conn) @@ -64,8 +62,12 @@ def get_sensors(conn=Depends(get_db_connection)): return {"sensors": sensors} -@app.get("/api/applications", response_model=ApplicationListResponse) -def get_applications(conn=Depends(get_db_connection)): +@app.get( + "/api/applications", response_model=ApplicationListResponse, tags=["applications"] +) +def get_applications( + conn=Depends(get_db_connection), token: str = Depends(verify_token) +): """Fetch all applications from the database.""" applications = fetch_applications(conn) @@ -75,8 +77,12 @@ def get_applications(conn=Depends(get_db_connection)): return {"applications": applications} -@app.get("/api/measurements", response_model=MeasurementListResponse) -def get_measurements(sensor_name, conn=Depends(get_db_connection)): +@app.get( + "/api/measurements", response_model=MeasurementListResponse, tags=["measurements"] +) +def get_measurements( + sensor_name, conn=Depends(get_db_connection), token: str = Depends(verify_token) +): """Fetch measurements for a given sensor name.""" result = fetch_sensor_app_info(conn, sensor_name) if not result: @@ -100,7 +106,7 @@ def get_measurements(sensor_name, conn=Depends(get_db_connection)): return {"measurements": measurements} -@app.get("/health-check", response_class=PlainTextResponse) +@app.get("/health-check", response_class=PlainTextResponse, include_in_schema=False) def health_check(conn=Depends(get_db_connection)): """Check if the service is running properly.""" global last_update_time diff --git a/rest_api/util/db.py b/rest_api/util/db.py new file mode 100644 index 0000000..c93e508 --- /dev/null +++ b/rest_api/util/db.py @@ -0,0 +1,9 @@ +# Dependency to get a connection from the pool +def get_db_connection(connection_pool): + """Get a database connection from the pool and returns it after route is done processing.""" + conn = connection_pool.getconn() + try: + yield conn + finally: + # Return the connection to the pool + connection_pool.putconn(conn) From af75b3cf109bc144e6def08d263979bf21e47aad Mon Sep 17 00:00:00 2001 From: NejcKle Date: Thu, 14 Aug 2025 11:09:40 +0200 Subject: [PATCH 26/28] feat: cleanup and docs --- ingest_service/main.py | 20 ++++++-- processor_service/main.py | 103 +++++++++++++++++++++----------------- rest_api/main.py | 7 ++- 3 files changed, 79 insertions(+), 51 deletions(-) diff --git a/ingest_service/main.py b/ingest_service/main.py index 369f4b5..ead2be7 100644 --- a/ingest_service/main.py +++ b/ingest_service/main.py @@ -4,7 +4,7 @@ from constants import KAFKA_HOST from fastapi import Depends, FastAPI, HTTPException, Request, Response, status from fastapi.exceptions import RequestValidationError -from fastapi.responses import JSONResponse, PlainTextResponse +from fastapi.responses import JSONResponse, PlainTextResponse, RedirectResponse from kafka import KafkaProducer from models.http import DataHTTP @@ -26,7 +26,10 @@ ) # Prepare the app -app = FastAPI() +app = FastAPI( + title="Ingest Service", + summary="Service for ingesting device or mock data from various sources", +) # Health check constants HEALTH_CHECK_INTERVAL_S = 300 # 5 minutes @@ -46,7 +49,14 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE ) -@app.post("/http") +# Define all routes +@app.get("/", include_in_schema=False) +def redirect_to_docs(): + """Redirect root path to the API documentation.""" + return RedirectResponse(url="/docs") + + +@app.post("/http", tags=["various"]) async def ingest_data(data: DataHTTP, token: str = Depends(verify_token)): """Ingest data from any source. This is a generic endpoint that can be used to ingest data from various sources.""" try: @@ -66,7 +76,7 @@ async def ingest_data(data: DataHTTP, token: str = Depends(verify_token)): return Response(status_code=200) -@app.post("/ttn") +@app.post("/ttn", tags=["TTN"]) async def ingest_data_ttn(data: DataTTN, token: str = Depends(verify_token)): """Ingest data from TTN webhook.""" data_to_insert = None @@ -88,7 +98,7 @@ async def ingest_data_ttn(data: DataTTN, token: str = Depends(verify_token)): return Response(status_code=200) -@app.get("/health-check", response_class=PlainTextResponse) +@app.get("/health-check", response_class=PlainTextResponse, include_in_schema=False) def health_check(): """Check if the service is running properly.""" global last_update_time diff --git a/processor_service/main.py b/processor_service/main.py index f7c6567..b285928 100644 --- a/processor_service/main.py +++ b/processor_service/main.py @@ -18,6 +18,17 @@ HEALTH_CHECK_INTERVAL_S = 300 # 5 minutes last_update_time = time.time() +logger = Logger(logging.getLogger(__name__)) +consumer = KafkaConsumer( + "process_data", + bootstrap_servers=KAFKA_HOST, + auto_offset_reset="earliest", + group_id="ajdovscina-consumer", + max_poll_interval_ms=900000, +) + +# Start uvicorn server in a separate thread + # Define lifespan context manager for the app @asynccontextmanager @@ -33,20 +44,33 @@ async def lifespan(app): # Create FastAPI app for health check endpoint with the lifespan -app = FastAPI(lifespan=lifespan) +app = FastAPI(lifespan=lifespan, docs_url=None, redoc_url=None) -consumer = KafkaConsumer( - "process_data", - bootstrap_servers=KAFKA_HOST, - auto_offset_reset="earliest", - group_id="ajdovscina-consumer", - max_poll_interval_ms=900000, -) -logger = Logger(logging.getLogger(__name__)) +# Cleanup +def close_pool(): + """Close all connections in the pool when application exits.""" + if connection_pool: + logger.info("Closing database connection pool...") + connection_pool.closeall() + logger.info("Database connection pool closed.") -@app.get("/health-check", response_class=PlainTextResponse) +# Register the function to be called on exit (for non-FastAPI shutdown scenarios) +atexit.register(close_pool) + +# This is used for the health check endpoint +Thread( + target=uvicorn.run, + args=(app,), + kwargs={"host": "0.0.0.0", "port": 6000, "log_level": "info"}, +).start() + +# Define routes + + +# Healthcheck is the only route and it's used internally +@app.get("/health-check", response_class=PlainTextResponse, include_in_schema=False) def health_check(): """Check if the service is running.""" global last_update_time @@ -72,46 +96,35 @@ def health_check(): ) -# Start uvicorn server in a separate thread -Thread( - target=uvicorn.run, - args=(app,), - kwargs={"host": "0.0.0.0", "port": 6000, "log_level": "info"}, -).start() - - -def close_pool(): - """Close all connections in the pool when application exits.""" - if connection_pool: - logger.info("Closing database connection pool...") - connection_pool.closeall() - logger.info("Database connection pool closed.") +def process_kafka_messages(): + """Process messages from the Kafka topic.""" + global last_update_time + for message in consumer: + logger.debug( + "%s:%d:%d: key=%s value=%s" + % ( + message.topic, + message.partition, + message.offset, + message.key, + message.value, + ) + ) -# Register the function to be called on exit (for non-FastAPI shutdown scenarios) -atexit.register(close_pool) + if message.key == b"ttn_service" or message.key == b"http_service": + byte_string = message.value -for message in consumer: - logger.debug( - "%s:%d:%d: key=%s value=%s" - % ( - message.topic, - message.partition, - message.offset, - message.key, - message.value, - ) - ) + data_to_insert = json.loads(byte_string.decode("utf-8")) - if message.key == b"ttn_service" or message.key == b"http_service": - byte_string = message.value + # Insert received data into the database + insert_data(data_to_insert) - data_to_insert = json.loads(byte_string.decode("utf-8")) + # Update the last update time after successful processing + last_update_time = time.time() - # Insert received data into the database - insert_data(data_to_insert) + consumer.commit_async() - # Update the last update time after successful processing - last_update_time = time.time() - consumer.commit_async() +# Start the endless processing loop +process_kafka_messages() diff --git a/rest_api/main.py b/rest_api/main.py index f5ad8be..a7362c3 100644 --- a/rest_api/main.py +++ b/rest_api/main.py @@ -41,7 +41,12 @@ async def lifespan(app): # Create FastAPI app with the lifespan -app = FastAPI(default_response_class=ORJSONResponse, lifespan=lifespan) +app = FastAPI( + title="REST API", + description="API for retrieving data and sensor information", + default_response_class=ORJSONResponse, + lifespan=lifespan, +) # Define all routes From 8e834283d71c074fe8524620d13ae951a27d0b9a Mon Sep 17 00:00:00 2001 From: NejcKle Date: Thu, 14 Aug 2025 15:59:52 +0200 Subject: [PATCH 27/28] fix: connection pool expected at API routes --- rest_api/main.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/rest_api/main.py b/rest_api/main.py index a7362c3..377ee3c 100644 --- a/rest_api/main.py +++ b/rest_api/main.py @@ -1,5 +1,6 @@ import time from contextlib import asynccontextmanager +from functools import partial from fastapi import Depends, FastAPI from fastapi.responses import ORJSONResponse, PlainTextResponse, RedirectResponse @@ -57,7 +58,10 @@ def redirect_to_docs(): @app.get("/api/sensors", response_model=SensorListResponse, tags=["sensors"]) -def get_sensors(conn=Depends(get_db_connection), token: str = Depends(verify_token)): +def get_sensors( + conn=Depends(partial(get_db_connection, connection_pool)), + token: str = Depends(verify_token), +): """Fetch all sensors from the database.""" sensors = fetch_sensors(conn) @@ -71,7 +75,8 @@ def get_sensors(conn=Depends(get_db_connection), token: str = Depends(verify_tok "/api/applications", response_model=ApplicationListResponse, tags=["applications"] ) def get_applications( - conn=Depends(get_db_connection), token: str = Depends(verify_token) + conn=Depends(partial(get_db_connection, connection_pool)), + token: str = Depends(verify_token), ): """Fetch all applications from the database.""" applications = fetch_applications(conn) @@ -86,7 +91,9 @@ def get_applications( "/api/measurements", response_model=MeasurementListResponse, tags=["measurements"] ) def get_measurements( - sensor_name, conn=Depends(get_db_connection), token: str = Depends(verify_token) + sensor_name, + conn=Depends(partial(get_db_connection, connection_pool)), + token: str = Depends(verify_token), ): """Fetch measurements for a given sensor name.""" result = fetch_sensor_app_info(conn, sensor_name) @@ -112,7 +119,7 @@ def get_measurements( @app.get("/health-check", response_class=PlainTextResponse, include_in_schema=False) -def health_check(conn=Depends(get_db_connection)): +def health_check(conn=Depends(partial(get_db_connection, connection_pool))): """Check if the service is running properly.""" global last_update_time From 4527d4fac08dc8ba210278aa2d850578bd108b7f Mon Sep 17 00:00:00 2001 From: NejcKle Date: Wed, 10 Sep 2025 11:37:08 +0200 Subject: [PATCH 28/28] feat: add pagination, redue data size by dropping empty fields --- processor_service/src/sql.py | 41 ++++++++++++++--------------- rest_api/Makefile | 2 +- rest_api/main.py | 45 +++++++++++++++++++++++++++++++- rest_api/models/return_models.py | 14 ++++++++++ rest_api/src/handlers.py | 8 +++--- rest_api/src/sql.py | 2 +- 6 files changed, 84 insertions(+), 28 deletions(-) diff --git a/processor_service/src/sql.py b/processor_service/src/sql.py index d0ca22c..c61450f 100644 --- a/processor_service/src/sql.py +++ b/processor_service/src/sql.py @@ -50,6 +50,23 @@ def insert_sensor_readings(table_name: str, keys: list): return query +def get_col_type(value): + """Get column type from value.""" + if isinstance(value, int): + col_type = "FLOAT" + elif isinstance(value, float): + col_type = "DOUBLE PRECISION" + elif isinstance(value, str): + col_type = "TEXT" + elif isinstance(value, datetime): + col_type = "TIMESTAMPTZ" + else: + # Default to TEXT for unknown types + col_type = "TEXT" + + return col_type + + def create_sensor_readings_table(application_name: str, fields: dict): """Creates a SQL CREATE TABLE statement based on the sensor type and fields. @@ -80,17 +97,7 @@ def create_sensor_readings_table(application_name: str, fields: dict): if field_lower in added_columns: continue - if isinstance(value, int): - col_type = "FLOAT" - elif isinstance(value, float): - col_type = "DOUBLE PRECISION" - elif isinstance(value, str): - col_type = "TEXT" - elif isinstance(value, datetime): - col_type = "TIMESTAMPTZ" - else: - # Default to TEXT for unknown types - col_type = "TEXT" + col_type = get_col_type(value) # Add column definition column_definitions.append( @@ -133,17 +140,7 @@ def alter_measurement_table(table_name, fields, existing_columns): continue # Determine column type based on value type - if isinstance(value, int): - col_type = "FLOAT" - elif isinstance(value, float): - col_type = "DOUBLE PRECISION" - elif isinstance(value, str): - col_type = "TEXT" - elif isinstance(value, datetime): - col_type = "TIMESTAMPTZ" - else: - # Default to TEXT for unknown types - col_type = "TEXT" + col_type = get_col_type(value) # Create add column operation column_operations.append( diff --git a/rest_api/Makefile b/rest_api/Makefile index 373091a..b742519 100644 --- a/rest_api/Makefile +++ b/rest_api/Makefile @@ -8,7 +8,7 @@ install: pip install -r requirements.txt run: clean install - bash -c "trap 'echo \"Cleaning up...\"; make clean' EXIT; uvicorn main:app --host 0.0.0.0 --port 51005" + bash -c "trap 'echo \"Cleaning up...\"; make clean' EXIT; uvicorn main:app --host 0.0.0.0 --port 51005 --reload" run-local-dev: python3.10 -m venv ../venv diff --git a/rest_api/main.py b/rest_api/main.py index 377ee3c..cd03a45 100644 --- a/rest_api/main.py +++ b/rest_api/main.py @@ -92,10 +92,17 @@ def get_applications( ) def get_measurements( sensor_name, + limit=None, + offset=None, conn=Depends(partial(get_db_connection, connection_pool)), token: str = Depends(verify_token), ): """Fetch measurements for a given sensor name.""" + if limit is not None: + limit = int(limit) + if offset is not None: + offset = int(offset) + result = fetch_sensor_app_info(conn, sensor_name) if not result: return ORJSONResponse(content={"error": "Sensor not found"}, status_code=404) @@ -115,7 +122,43 @@ def get_measurements( if table_measurements: measurements.extend(table_measurements) - return {"measurements": measurements} + # Now sort the measurements by timestamp, timestamp is a dictionary key + measurements.sort(key=lambda x: x.timestamp, reverse=True) + + # Calculate pagination details + total_measurements = len(measurements) + + # Now apply limit and offset if provided + if offset is not None: + measurements = measurements[offset:] + if limit is not None: + measurements = measurements[:limit] + + # Create pagination info + + # Default values + page = 1 + total_pages = 1 + current_offset = offset if offset is not None else 0 + + # Calculate current page and total pages if limit is provided + if limit is not None and limit > 0: + page = (current_offset // limit) + 1 + total_pages = (total_measurements + limit - 1) // limit + + # Create pagination info + pagination_info = { + "total": total_measurements, + "count": len(measurements), # Count after applying limit/offset + "limit": limit, + "offset": current_offset, + "page": page, + "total_pages": total_pages, + "has_next": page < total_pages, + "has_previous": page > 1, + } + + return {"measurements": measurements, "page_info": pagination_info} @app.get("/health-check", response_class=PlainTextResponse, include_in_schema=False) diff --git a/rest_api/models/return_models.py b/rest_api/models/return_models.py index 496e060..9f379b3 100644 --- a/rest_api/models/return_models.py +++ b/rest_api/models/return_models.py @@ -11,10 +11,24 @@ class Measurement(BaseModel): data: dict # dynamic columns from the database +class PageInfo(BaseModel): + """Data model for pagination information.""" + + total: int + count: int + limit: int | None + offset: int | None + page: int + total_pages: int + has_next: bool + has_previous: bool + + class MeasurementListResponse(BaseModel): """Data model for a list of sensor measurements.""" measurements: list[Measurement] + page_info: PageInfo # Forward reference to PageInfo class Sensor(BaseModel): diff --git a/rest_api/src/handlers.py b/rest_api/src/handlers.py index bf318a3..174d215 100644 --- a/rest_api/src/handlers.py +++ b/rest_api/src/handlers.py @@ -1,8 +1,8 @@ from models.return_models import Application, Measurement, Sensor from psycopg2 import sql from src.sql import ( + fetch_all_measurements_for_sensor_statement, fetch_applications_statement, - fetch_measurements_for_sensor_statement, fetch_sensor_app_info_statement, fetch_sensors_statement, fetch_table_names_from_app_id_statement, @@ -75,7 +75,7 @@ def fetch_table_names_from_app_id(conn, app_id: int) -> list | None: def fetch_measurements_for_sensor(conn, table_name: str, sensor_id: int) -> list | None: """Fetch all measurements for a given sensor_id from a specific table.""" with conn.cursor() as cur: - query = fetch_measurements_for_sensor_statement(table_name) + query = fetch_all_measurements_for_sensor_statement(table_name) cur.execute(query, (sensor_id,)) rows = cur.fetchall() @@ -90,12 +90,14 @@ def fetch_measurements_for_sensor(conn, table_name: str, sensor_id: int) -> list # Build a dict of column names to values, excluding sensor_id and timestamp data = {col: val for col, val in zip(colnames[3:], row[3:])} + # Filter out None values from data + filtered_data = {k: v for k, v in data.items() if v is not None} measurements.append( Measurement( # sensor_id=row[1], table_name=table_name, timestamp=str(row[2]), - data=data, + data=filtered_data, ) ) diff --git a/rest_api/src/sql.py b/rest_api/src/sql.py index 5ae7d82..dd704be 100644 --- a/rest_api/src/sql.py +++ b/rest_api/src/sql.py @@ -31,7 +31,7 @@ def fetch_table_names_from_app_id_statement(): """) -def fetch_measurements_for_sensor_statement(table_name: str): +def fetch_all_measurements_for_sensor_statement(table_name: str): """Return SQL statements to fetch all measurements for a given sensor_id from a specific table.""" return sql.SQL(""" SELECT *