diff --git a/examples/vtadmin/Makefile b/examples/vtadmin/Makefile new file mode 100644 index 00000000000..e41aea7cd27 --- /dev/null +++ b/examples/vtadmin/Makefile @@ -0,0 +1,31 @@ +# Makefile for VTAdmin demo cluster + +.PHONY: up down restart logs clean reset + +# Base command with all required files and environment variables +COMPOSE_CMD = docker compose -f ../compose/docker-compose.yml -f docker-compose.yml --env-file ../compose/template.env + +# Start the cluster +up: + $(COMPOSE_CMD) up -d --force-recreate + +# Stop the cluster +down: + $(COMPOSE_CMD) down --remove-orphans + +# Restart running services +restart: + $(COMPOSE_CMD) restart + +# Perform a full reset (down with volume cleanup, then up) +reset: + $(COMPOSE_CMD) down --remove-orphans + $(COMPOSE_CMD) up -d --force-recreate + +# Stream logs from all services +logs: + $(COMPOSE_CMD) logs -f + +# Clean up all resources including volumes +clean: + $(COMPOSE_CMD) down -v --remove-orphans diff --git a/examples/vtadmin/README.md b/examples/vtadmin/README.md new file mode 100644 index 00000000000..1add40ab068 --- /dev/null +++ b/examples/vtadmin/README.md @@ -0,0 +1,58 @@ +# VTAdmin Demo + +This example provides a fully functional local Vitess cluster with **VTAdmin**, **Grafana**, and **Percona Monitoring and Management (PMM)** pre-configured and integrated. + +It demonstrates how VTAdmin can serve as a single pane of glass for your database infrastructure, providing context-aware deep links to your monitoring dashboards. + +## Quick Start + +1. **Start the cluster**: + ```bash + cd examples/vtadmin + make up + ``` +2. **Access VTAdmin**: + Open **http://localhost:5173** in your browser. + +## Features + +- **Unified Interface**: View cluster topology, tablet health, and gates. +- **Integrated Monitoring**: + - **Vitess Metrics**: Deep links to Grafana dashboards for clusters, tablets, and gates. + - **MySQL Metrics**: Deep links to PMM for database instance analysis. +- **Pre-configured Stack**: Includes Prometheus, Grafana, and PMM running alongside Vitess. + +## Service Endpoints + +| Service | URL | Credentials | +|---------|-----|-------------| +| **VTAdmin** | http://localhost:5173 | - | +| **Grafana** | http://localhost:3000 | default | +| **PMM** | http://localhost:8888 | default | +| **Prometheus** | http://localhost:9090 | - | + +## Configuration + +The dashboard links in VTAdmin are configured via environment variables in `docker-compose.yml`. You can modify these to point to your own external monitoring infrastructure: + +```yaml +vtadmin-web: + environment: + VITE_VITESS_MONITORING_CLUSTER_TEMPLATE: "http://your-grafana/..." + VITE_MYSQL_MONITORING_TEMPLATE: "http://your-pmm/..." +``` + +See `web/vtadmin/README.md` for all available environment variables. + +## Common Commands + +### Cluster Control +```bash +make up # Start cluster with current configuration +make restart # Restart all services +make reset # Full teardown and fresh start (removes volumes) +make down # Stop all services +make clean # Stop and remove all data volumes +make logs # Stream logs from all services +``` + diff --git a/examples/vtadmin/config/discovery.json b/examples/vtadmin/config/discovery.json new file mode 100644 index 00000000000..d998d4b114b --- /dev/null +++ b/examples/vtadmin/config/discovery.json @@ -0,0 +1,25 @@ +{ + "clusters": { + "local": { + "name": "local", + "discovery": "staticfile", + "discovery-staticfile-path": "/app/discovery.json" + } + }, + "vtgates": [ + { + "host": { + "hostname": "vtgate:15999" + }, + "tags": ["cell:test"] + } + ], + "vtctlds": [ + { + "host": { + "hostname": "vtctld:15999" + }, + "tags": ["cell:test"] + } + ] +} diff --git a/examples/vtadmin/config/grafana-datasource-prometheus.yaml b/examples/vtadmin/config/grafana-datasource-prometheus.yaml new file mode 100644 index 00000000000..0b0a57dde1a --- /dev/null +++ b/examples/vtadmin/config/grafana-datasource-prometheus.yaml @@ -0,0 +1,15 @@ +# Grafana datasource configuration +# +# Automatically provisions Prometheus as a datasource for Grafana. +# Used by the Grafana container to connect to Prometheus metrics. +# +# Access: http://localhost:3000 (admin/admin) + +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true diff --git a/examples/vtadmin/config/prometheus.yml b/examples/vtadmin/config/prometheus.yml new file mode 100644 index 00000000000..ae9009635d0 --- /dev/null +++ b/examples/vtadmin/config/prometheus.yml @@ -0,0 +1,17 @@ +# Prometheus configuration for VTAdmin demo cluster +# +# Collects metrics from all Vitess components: +# - vtctld: Topology server +# - vtgate: Query router +# - vttablets: Database tablets +# +# Scrape interval: 10 seconds +# Metrics accessible at: http://localhost:9090 + +global: + scrape_interval: 10s + +scrape_configs: + - job_name: 'vitess' + static_configs: + - targets: ['vtctld:8080', 'vtgate:8080', 'vttablet101:8080', 'vttablet102:8080', 'vttablet201:8080', 'vttablet202:8080', 'vttablet301:8080', 'vttablet302:8080'] diff --git a/examples/vtadmin/docker-compose.yml b/examples/vtadmin/docker-compose.yml new file mode 100644 index 00000000000..6469435e71a --- /dev/null +++ b/examples/vtadmin/docker-compose.yml @@ -0,0 +1,160 @@ +# VTAdmin Demo - Docker Compose Overrides +# +# This file extends the base Vitess cluster (../compose/docker-compose.yml) with: +# - VTAdmin web UI (port 5173) +# - Grafana dashboards for Vitess metrics (port 3000) +# - PMM monitoring for MySQL instances (port 8888) +# - Prometheus metrics collection (port 9090) +# +# Usage: +# make up # Start cluster +# make logs # View logs +# make reset # Full cleanup and restart +# +# See README.md for detailed documentation. + +services: + vtadmin-web: + image: node:22 + working_dir: /app + volumes: + - ${PWD}/../../web/vtadmin:/app + environment: + VITE_VTADMIN_API_ADDRESS: http://localhost:14200 + # VITE_VITESS_MONITORING_DASHBOARD_TITLE: Grafana + # VITE_MYSQL_MONITORING_DASHBOARD_TITLE: PMM + VITE_VITESS_MONITORING_CLUSTER_TEMPLATE: http://localhost:3000/d/vitess_summary/vitess-summary + VITE_VITESS_MONITORING_VTTABLET_TEMPLATE: http://localhost:3000/d/vitess_summary/vitess-summary?var-alias={alias} + VITE_VITESS_MONITORING_VTGATE_TEMPLATE: http://localhost:3000/d/vitess_summary/vitess-summary + VITE_MYSQL_MONITORING_TEMPLATE: http://localhost:8888/graph/d/mysql-instance-overview/mysql-instances-overview?var-service_name={hostname}-mysql + command: + - /bin/sh + - -c + - npm install && npm run start -- --host + ports: + - "5173:5173" + depends_on: + - vtadmin-api + + vtadmin-api: + image: vitess/lite:${VITESS_TAG:-latest} + command: + - vtadmin + - --addr=:14200 + - --http-origin=* + - --http-tablet-url-tmpl=http://{{ .Tablet.Hostname }}:80 + - --cluster-config=/app/discovery.json + - --no-rbac + volumes: + - ${PWD}/config/discovery.json:/app/discovery.json + ports: + - "14200:14200" + depends_on: + - vtctld + + vttablet101: + volumes: + - ${PWD}/scripts/vttablet-up.sh:/script/vttablet-up.sh + + vttablet102: + volumes: + - ${PWD}/scripts/vttablet-up.sh:/script/vttablet-up.sh + + vttablet201: + volumes: + - ${PWD}/scripts/vttablet-up.sh:/script/vttablet-up.sh + + vttablet202: + volumes: + - ${PWD}/scripts/vttablet-up.sh:/script/vttablet-up.sh + + vttablet301: + volumes: + - ${PWD}/scripts/vttablet-up.sh:/script/vttablet-up.sh + + vttablet302: + volumes: + - ${PWD}/scripts/vttablet-up.sh:/script/vttablet-up.sh + + cluster-init: + image: vitess/lite:${VITESS_TAG:-latest} + command: + - sh + - -c + - /script/init_cluster.sh + volumes: + - ${PWD}/scripts/init_cluster.sh:/script/init_cluster.sh + depends_on: + - vtctld + - vttablet101 + - vttablet102 + - vttablet201 + - vttablet202 + - vttablet301 + - vttablet302 + + pmm-server: + image: percona/pmm-server:2 + ports: + - "8888:80" + - "8889:443" + volumes: + - pmm-data:/srv + + pmm-setup: + image: percona/pmm-client:2 + entrypoint: /bin/sh + depends_on: + - pmm-server + - vtgate + - vttablet101 + - vttablet102 + - vttablet201 + - vttablet202 + - vttablet301 + - vttablet302 + command: + - -c + - | + echo "Waiting for PMM Server..." + until curl -k -s https://admin:admin@pmm-server/v1/ready; do sleep 5; done + echo "PMM Server is ready." + + # Register PMM Client + pmm-agent setup --config-file=/usr/local/percona/pmm2/config/pmm-agent.yaml --server-address=pmm-server:443 --server-insecure-tls --server-username=admin --server-password=admin + + # Start pmm-agent in background + pmm-agent --config-file=/usr/local/percona/pmm2/config/pmm-agent.yaml & + + # Wait for agent to be ready + sleep 10 + + # Add vttablet MySQLs + for i in 101 102 201 202 301 302; do + echo "Adding vttablet$$i MySQL..." + pmm-admin add mysql --username=pmm --password=pmm --host=vttablet$$i --port=3306 --service-name=vttablet$$i-mysql --query-source=perfschema || true + done + + echo "Setup complete." + + # Keep container running + wait + + prometheus: + image: prom/prometheus + ports: + - "9090:9090" + volumes: + - ${PWD}/config/prometheus.yml:/etc/prometheus/prometheus.yml + + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + volumes: + - ${PWD}/config/grafana-datasource-prometheus.yaml:/etc/grafana/provisioning/datasources/prometheus.yaml + depends_on: + - prometheus + +volumes: + pmm-data: diff --git a/examples/vtadmin/scripts/init_cluster.sh b/examples/vtadmin/scripts/init_cluster.sh new file mode 100644 index 00000000000..a255766bce0 --- /dev/null +++ b/examples/vtadmin/scripts/init_cluster.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Copyright 2025 The Vitess Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -u + +echo "Waiting for vtctld..." +until vtctldclient --server vtctld:15999 GetKeyspaces; do + sleep 1 +done + +echo "Waiting for tablets..." +# We expect 6 tablets +while [ $(vtctldclient --server vtctld:15999 GetTablets | wc -l) -lt 6 ]; do + sleep 1 +done + +echo "Initializing shards..." + +# Initialize test_keyspace/-80 +echo "Initializing test_keyspace/-80..." +vtctldclient --server vtctld:15999 PlannedReparentShard --new-primary test-0000000101 test_keyspace/-80 || echo "Failed to init test_keyspace/-80 or already initialized" + +# Initialize test_keyspace/80- +echo "Initializing test_keyspace/80-..." +vtctldclient --server vtctld:15999 PlannedReparentShard --new-primary test-0000000201 test_keyspace/80- || echo "Failed to init test_keyspace/80- or already initialized" + +# Initialize lookup_keyspace/- +echo "Initializing lookup_keyspace/-..." +vtctldclient --server vtctld:15999 PlannedReparentShard --new-primary test-0000000301 lookup_keyspace/- || echo "Failed to init lookup_keyspace/- or already initialized" + +echo "Cluster initialization complete." diff --git a/examples/vtadmin/scripts/vttablet-up.sh b/examples/vtadmin/scripts/vttablet-up.sh new file mode 100644 index 00000000000..a8ce749451c --- /dev/null +++ b/examples/vtadmin/scripts/vttablet-up.sh @@ -0,0 +1,198 @@ +#!/bin/bash + +# Copyright 2025 The Vitess Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -u +export VTROOT=/vt +export VTDATAROOT=/vt/vtdataroot + +keyspace=${KEYSPACE:-'test_keyspace'} +shard=${SHARD:-'0'} +grpc_port=${GRPC_PORT:-'15999'} +web_port=${WEB_PORT:-'8080'} +role=${ROLE:-'replica'} +vthost=${VTHOST:-`hostname -i`} +sleeptime=${SLEEPTIME:-'0'} +uid=$1 +external=${EXTERNAL_DB:-0} + +# If DB is not explicitly set, we default to behaviour of prefixing with vt_ +# If there is an external db, the db_nmae will always match the keyspace name +[ $external = 0 ] && db_name=${DB:-"vt_$keyspace"} || db_name=${DB:-"$keyspace"} +db_charset=${DB_CHARSET:-''} +tablet_hostname='' + +# Use IPs to simplify connections when testing in docker. +# Otherwise, blank hostname means the tablet auto-detects FQDN. +# This is now set further up + +printf -v alias '%s-%010d' $CELL $uid +printf -v tablet_dir 'vt_%010d' $uid + +tablet_role=$role +tablet_type='replica' + +# Make every 3rd tablet rdonly +if (( $uid % 100 % 3 == 0 )) ; then + tablet_type='rdonly' +fi + +# Consider every tablet with %d00 as external primary +if [ $external = 1 ] && (( $uid % 100 == 0 )) ; then + tablet_type='replica' + tablet_role='externalprimary' + keyspace="ext_$keyspace" +fi + +# Copy config directory +cp -R /script/config $VTROOT +init_db_sql_file="$VTROOT/config/init_db.sql" + +# Prepend SET sql_log_bin = 0; to init_db.sql to prevent Errant GTIDs during initialization +sed -i '1s/^/SET sql_log_bin = 0;\n/' $init_db_sql_file + +# Clear in-place edits of init_db_sql_file if any exist +sed -i '/##\[CUSTOM_SQL/{:a;N;/END\]##/!ba};//d' $init_db_sql_file + +echo "##[CUSTOM_SQL_START]##" >> $init_db_sql_file + +if [ "$external" = "1" ]; then + # We need a common user for the unmanaged and managed tablets else tools like orchestrator will not function correctly + echo "Creating matching user for managed tablets..." + echo "CREATE USER IF NOT EXISTS '$DB_USER'@'%' IDENTIFIED BY '$DB_PASS';" >> $init_db_sql_file + echo "GRANT ALL ON *.* TO '$DB_USER'@'%';" >> $init_db_sql_file +fi +echo "##[CUSTOM_SQL_END]##" >> $init_db_sql_file + +echo "##[CUSTOM_SQL_END]##" >> $init_db_sql_file + +mkdir -p $VTDATAROOT/backups + + +export KEYSPACE=$keyspace +export SHARD=$shard +export TABLET_ID=$alias +export TABLET_DIR=$tablet_dir +export MYSQL_PORT=3306 +export TABLET_ROLE=$tablet_role +export DB_PORT=${DB_PORT:-3306} +export DB_HOST=${DB_HOST:-""} +export DB_NAME=$db_name + +# Delete socket files before running mysqlctld if exists. +# This is the primary reason for unhealthy state on restart. +# https://github.com/vitessio/vitess/pull/5115/files +echo "Removing $VTDATAROOT/$tablet_dir/{mysql.sock,mysql.sock.lock}..." +rm -rf $VTDATAROOT/$tablet_dir/{mysql.sock,mysql.sock.lock} + +# Create mysql instances +# Do not create mysql instance for primary if connecting to external mysql database +#TODO: Remove underscore(_) flags in v25, replace them with dashed(-) notation +if [[ $tablet_role != "externalprimary" ]]; then + echo "Initing mysql for tablet: $uid role: $role external: $external.. " + $VTROOT/bin/mysqlctld \ + --init-db-sql-file=$init_db_sql_file \ + --logtostderr=true \ + --tablet-uid=$uid \ + & + + # Wait for MySQL to be ready + echo "Waiting for MySQL to be ready on $VTDATAROOT/$tablet_dir/mysql.sock..." + set -x + for i in {1..60}; do + if mysql -u root --socket=$VTDATAROOT/$tablet_dir/mysql.sock -e "SELECT 1" >/dev/null 2>&1; then + set +x + echo "MySQL is ready!" + set -x + + # Ensure the database exists + echo "Ensuring database $db_name exists..." + mysql -u root --socket=$VTDATAROOT/$tablet_dir/mysql.sock -e "SET sql_log_bin = 0; SET GLOBAL super_read_only=0; SET GLOBAL read_only=0; CREATE DATABASE IF NOT EXISTS $db_name" || echo "Failed to create DB" + + # Create PMM user + echo "Creating PMM user..." + mysql -u root --socket=$VTDATAROOT/$tablet_dir/mysql.sock -e "SET sql_log_bin = 0; SET GLOBAL super_read_only=0; SET GLOBAL read_only=0; CREATE USER IF NOT EXISTS 'pmm'@'%' IDENTIFIED BY 'pmm'; GRANT SELECT, PROCESS, REPLICATION CLIENT, RELOAD ON *.* TO 'pmm'@'%'; GRANT SELECT, UPDATE, DELETE, DROP ON performance_schema.* TO 'pmm'@'%'; FLUSH PRIVILEGES;" || echo "Failed to create PMM user" + + # Verify PMM user + echo "Verifying PMM user..." + mysql -u root --socket=$VTDATAROOT/$tablet_dir/mysql.sock -e "SELECT user, host FROM mysql.user WHERE user='pmm'" || echo "Failed to verify PMM user" + + set +x + break + fi + if [ $i -eq 60 ]; then + echo "ERROR: MySQL failed to start after 60 attempts" + exit 1 + fi + echo "Attempt $i/60: MySQL not ready yet, waiting..." + sleep 2 + done +fi + +sleep $sleeptime + +# Create the cell +# https://vitess.io/blog/2020-04-27-life-of-a-cluster/ +$VTROOT/bin/vtctldclient --server vtctld:$GRPC_PORT AddCellInfo --root vitess/$CELL --server-address consul1:8500 $CELL || true + +#Populate external db conditional args +if [ $tablet_role = "externalprimary" ]; then + echo "Setting external db args for primary: $DB_NAME" + external_db_args="--db-host $DB_HOST \ + --db-port $DB_PORT \ + --init-db-name-override $DB_NAME \ + --init-tablet-type $tablet_type \ + --mycnf-server-id $uid \ + --db-app-user $DB_USER \ + --db-app-password $DB_PASS \ + --db-allprivs-user $DB_USER \ + --db-allprivs-password $DB_PASS \ + --db-appdebug-user $DB_USER \ + --db-appdebug-password $DB_PASS \ + --db-dba-user $DB_USER \ + --db-dba-password $DB_PASS \ + --db-filtered-user $DB_USER \ + --db-filtered-password $DB_PASS \ + --db-repl-user $DB_USER \ + --db-repl-password $DB_PASS \ + --enable-replication-reporter=false \ + --enforce-strict-trans-tables=false \ + --track-schema-versions=true \ + --vreplication-tablet-type=primary \ + --watch-replication-stream=true" +else + external_db_args="--init-db-name-override $DB_NAME \ + --init-tablet-type $tablet_type \ + --enable-replication-reporter=true \ + --restore-from-backup" +fi + +#TODO: Remove underscore(_) flags in v25, replace them with dashed(-) notation +echo "Starting vttablet..." +exec $VTROOT/bin/vttablet \ + $TOPOLOGY_FLAGS \ + --logtostderr=true \ + --tablet-path $alias \ + --tablet-hostname "$vthost" \ + --health-check-interval 5s \ + --port $web_port \ + --grpc-port $grpc_port \ + --service-map 'grpc-queryservice,grpc-tabletmanager,grpc-updatestream' \ + --init-keyspace $keyspace \ + --init-shard $shard \ + --backup-storage-implementation file \ + --file-backup-storage-root $VTDATAROOT/backups \ + --queryserver-config-schema-reload-time 60s \ + $external_db_args diff --git a/web/vtadmin/README.md b/web/vtadmin/README.md index 629fe116711..ae8dfc32c4d 100644 --- a/web/vtadmin/README.md +++ b/web/vtadmin/README.md @@ -18,6 +18,31 @@ Scripts for common and not-so-common tasks. These are always run from the `vites | `npm run lint:fix` | Run all of the linters and fix errors (where possible) in place. Note that this will overwrite your files so you may want to consider committing your work beforehand! | | `npm run build` | Generates a build of vtadmin-web for production and outputs the files to the `vitess/web/vtadmin/build` folder. In most cases, you won't need to run this locally, but it _can_ be useful for debugging production-specific issues. See the vite documentation about [testing the build locally](https://vitejs.dev/guide/static-deploy.html#testing-the-app-locally) for more information. | +## Dashboards + +VTAdmin can be configured to link to external dashboards (e.g., Grafana, PMM) for clusters, gates and tablets. These links are configured via environment variables. + +The variables accept template strings with placeholders that are replaced with the relevant entity's data. + +| Variable | Description | Supported Placeholders | +|---|---|---| +| `VITE_VITESS_MONITORING_CLUSTER_TEMPLATE` | URL template for cluster metrics. | `{cluster}`, `{cluster_id}`, `{id}` | +| `VITE_VITESS_MONITORING_VTTABLET_TEMPLATE` | URL template for tablet metrics. | `{cluster}`, `{keyspace}`, `{shard}`, `{alias}`, `{hostname}`, `{type}`, `{cell}` | +| `VITE_VITESS_MONITORING_VTGATE_TEMPLATE` | URL template for VTGate metrics. | `{cluster}`, `{cluster_id}`, `{cell}`, `{hostname}`, `{pool}` | +| `VITE_MYSQL_MONITORING_TEMPLATE` | URL template for MySQL metrics (e.g., PMM). Adds a "Metrics" column to Tablets and Gates views. | `{cluster}`, `{keyspace}`, `{shard}`, `{alias}`, `{hostname}`, `{type}`, `{cell}`, `{pool}` | +| `VITE_VITESS_MONITORING_DASHBOARD_TITLE` | Title for the Vitess monitoring column/link. Defaults to "Vt Monitoring Dashboard". | N/A | +| `VITE_MYSQL_MONITORING_DASHBOARD_TITLE` | Title for the MySQL monitoring column/link. Defaults to "DB Monitoring Dashboard". | N/A | + +### Example Configuration + +Create a `.env.local` file in `web/vtadmin/` to test locally: + +```bash +VITE_VITESS_MONITORING_CLUSTER_TEMPLATE="https://grafana.example.com/d/cluster?var-cluster={cluster}" +VITE_VITESS_MONITORING_VTTABLET_TEMPLATE="https://grafana.example.com/d/tablet?var-alias={alias}" +VITE_MYSQL_MONITORING_TEMPLATE="https://pmm.example.com/graph/d/mysql?var-host={hostname}" +``` + ## Toolchain - [React](https://reactjs.org/) diff --git a/web/vtadmin/src/components/routes/Clusters.tsx b/web/vtadmin/src/components/routes/Clusters.tsx index c9ccf64772a..01e06acd6eb 100644 --- a/web/vtadmin/src/components/routes/Clusters.tsx +++ b/web/vtadmin/src/components/routes/Clusters.tsx @@ -25,6 +25,8 @@ import { WorkspaceHeader } from '../layout/WorkspaceHeader'; import { WorkspaceTitle } from '../layout/WorkspaceTitle'; import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; import ClusterRow from './clusters/ClusterRow'; +import { getVTClusterMonitoringTemplate, getVitessMonitoringDashboardTitle } from '../../util/env'; + export const Clusters = () => { useDocumentTitle('Clusters'); const clustersQuery = useClusters(); @@ -36,6 +38,11 @@ export const Clusters = () => { const renderRows = (rows: pb.Cluster[]) => rows.map((cluster, idx) => ); + const columns = ['Name', 'Id', 'Validate']; + if (getVTClusterMonitoringTemplate()) { + columns.push(getVitessMonitoringDashboardTitle()); + } + return (
@@ -44,7 +51,7 @@ export const Clusters = () => {
- +
diff --git a/web/vtadmin/src/components/routes/Gates.tsx b/web/vtadmin/src/components/routes/Gates.tsx index 5e0f0fefd24..42af8dc0764 100644 --- a/web/vtadmin/src/components/routes/Gates.tsx +++ b/web/vtadmin/src/components/routes/Gates.tsx @@ -27,6 +27,8 @@ import { ContentContainer } from '../layout/ContentContainer'; import { WorkspaceHeader } from '../layout/WorkspaceHeader'; import { WorkspaceTitle } from '../layout/WorkspaceTitle'; import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; +import { formatDashboardUrl } from '../../util/dashboards'; +import { getVitessMonitoringDashboardTitle, getVTGateMonitoringTemplate } from '../../util/env'; export const Gates = () => { useDocumentTitle('Gates'); @@ -38,6 +40,7 @@ export const Gates = () => { const mapped = (gatesQuery.data || []).map((g) => ({ cell: g.cell, cluster: g.cluster?.name, + cluster_id: g.cluster?.id, hostname: g.hostname, keyspaces: g.keyspaces, pool: g.pool, @@ -67,9 +70,34 @@ export const Gates = () => { {gate.cell} {(gate.keyspaces || []).join(', ')} + {getVTGateMonitoringTemplate() && ( + + + {gate.hostname as string}-metrics + + + )} )); + const columns = ['Pool', 'Hostname', 'Cell', 'Keyspaces']; + if (getVTGateMonitoringTemplate()) { + columns.push(getVitessMonitoringDashboardTitle()); + } + return (
@@ -83,7 +111,7 @@ export const Gates = () => { placeholder="Filter gates" value={filter || ''} /> - +
diff --git a/web/vtadmin/src/components/routes/Tablets.tsx b/web/vtadmin/src/components/routes/Tablets.tsx index 75f0980dd6b..14baee9d481 100644 --- a/web/vtadmin/src/components/routes/Tablets.tsx +++ b/web/vtadmin/src/components/routes/Tablets.tsx @@ -35,14 +35,27 @@ import { TabletLink } from '../links/TabletLink'; import { ExternalTabletLink } from '../links/ExternalTabletLink'; import { ShardLink } from '../links/ShardLink'; import InfoDropdown from './tablets/InfoDropdown'; -import { isReadOnlyMode } from '../../util/env'; import { ReadOnlyGate } from '../ReadOnlyGate'; import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; +import { formatDashboardUrl } from '../../util/dashboards'; +import { + getVitessMonitoringDashboardTitle, + getMysqlMonitoringDashboardTitle, + getMysqlMonitoringTemplate, + getVTTabletMonitoringTemplate, + isReadOnlyMode, +} from '../../util/env'; const COLUMNS = ['Keyspace', 'Shard', 'Alias', 'Type', 'Tablet State', 'Hostname']; if (!isReadOnlyMode()) { COLUMNS.push('Actions'); } +if (getVTTabletMonitoringTemplate()) { + COLUMNS.push(getVitessMonitoringDashboardTitle()); +} +if (getMysqlMonitoringTemplate()) { + COLUMNS.push(getMysqlMonitoringDashboardTitle()); +} export const Tablets = () => { useDocumentTitle('Tablets'); @@ -98,12 +111,57 @@ export const Tablets = () => { {t.hostname} - + + {getVTTabletMonitoringTemplate() && ( + + + {t.alias as string}-metrics + + + )} + + {getMysqlMonitoringTemplate() && ( + + + {t.alias as string}-MySQL-metrics + + + )} )); }, @@ -151,6 +209,7 @@ export const formatRows = ( return { alias: formatAlias(t.tablet?.alias), + cell: t.tablet?.alias?.cell, cluster: t.cluster?.name, hostname: t.tablet?.hostname, isShardServing: shard?.shard?.is_primary_serving, diff --git a/web/vtadmin/src/components/routes/clusters/ClusterRow.tsx b/web/vtadmin/src/components/routes/clusters/ClusterRow.tsx index fba35d5257f..8efa69c7e61 100644 --- a/web/vtadmin/src/components/routes/clusters/ClusterRow.tsx +++ b/web/vtadmin/src/components/routes/clusters/ClusterRow.tsx @@ -7,6 +7,8 @@ import { useValidate } from '../../../hooks/api'; import { Label } from '../../inputs/Label'; import Toggle from '../../toggle/Toggle'; import ValidationResults from '../../ValidationResults'; +import { formatDashboardUrl } from '../../../util/dashboards'; +import { getVTClusterMonitoringTemplate } from '../../../util/env'; interface Props { cluster: pb.Cluster; @@ -86,6 +88,24 @@ const ClusterRow: React.FC = ({ cluster }) => { Validate + {getVTClusterMonitoringTemplate() && ( + + + {cluster.name as string}-metrics + + + )} ); }; diff --git a/web/vtadmin/src/util/dashboards.test.ts b/web/vtadmin/src/util/dashboards.test.ts new file mode 100644 index 00000000000..fb56a2c2693 --- /dev/null +++ b/web/vtadmin/src/util/dashboards.test.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2025 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; +import { formatDashboardUrl } from './dashboards'; + +describe('formatDashboardUrl', () => { + it('returns null if template is undefined', () => { + expect(formatDashboardUrl(undefined, {})).toBeNull(); + }); + + it('returns the template as-is if no params are provided', () => { + const template = 'https://example.com'; + expect(formatDashboardUrl(template, {})).toBe(template); + }); + + it('replaces placeholders with values', () => { + const template = 'https://example.com/cluster/{cluster}/keyspace/{keyspace}'; + const params = { cluster: 'prod', keyspace: 'commerce' }; + expect(formatDashboardUrl(template, params)).toBe('https://example.com/cluster/prod/keyspace/commerce'); + }); + + it('handles multiple occurrences of the same placeholder', () => { + const template = 'https://example.com/c/{cluster}/d/{cluster}'; + const params = { cluster: 'prod' }; + expect(formatDashboardUrl(template, params)).toBe('https://example.com/c/prod/d/prod'); + }); + + it('ignores placeholders that are not in params', () => { + const template = 'https://example.com/{cluster}/{missing}'; + const params = { cluster: 'prod' }; + expect(formatDashboardUrl(template, params)).toBe('https://example.com/prod/{missing}'); + }); + + it('ignores params that are undefined or null', () => { + const template = 'https://example.com/{cluster}'; + const params = { cluster: undefined }; + expect(formatDashboardUrl(template, params)).toBe('https://example.com/{cluster}'); + }); +}); diff --git a/web/vtadmin/src/util/dashboards.ts b/web/vtadmin/src/util/dashboards.ts new file mode 100644 index 00000000000..1c93f4b485f --- /dev/null +++ b/web/vtadmin/src/util/dashboards.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2025 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Replaces placeholders in a template string with values from a params object. + * Only placeholders present in the template will be replaced. + * Example: formatDashboardUrl("https://grafana.com/d/123?var-cluster={cluster}", { cluster: "prod" }) + * + * Supported placeholders: {cluster}, {keyspace}, {shard}, {alias}, {hostname}, {type}, {cell}, {pool} + */ +export const formatDashboardUrl = ( + template: string | undefined, + params: Record +): string | null => { + if (!template) return null; + + let url = template; + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + // Replace all occurrences of {key} + url = url.replace(new RegExp(`{${key}}`, 'g'), value); + } + }); + + return url; +}; diff --git a/web/vtadmin/src/util/env.ts b/web/vtadmin/src/util/env.ts index e5009d6cb63..19f43874347 100644 --- a/web/vtadmin/src/util/env.ts +++ b/web/vtadmin/src/util/env.ts @@ -22,3 +22,16 @@ export const env: () => Vite.ImportMetaEnv = () => { // to transmute it into a boolean. It is a function, rather than a constant, // to support dynamic updates to import.meta.env in tests. export const isReadOnlyMode = (): boolean => env().VITE_READONLY_MODE === 'true'; + +// Monitoring templates. +// These functions retrieve the URL templates for various monitoring dashboards from the environment variables. +export const getVTClusterMonitoringTemplate = (): string | undefined => env().VITE_VITESS_MONITORING_CLUSTER_TEMPLATE; +export const getVTTabletMonitoringTemplate = (): string | undefined => env().VITE_VITESS_MONITORING_VTTABLET_TEMPLATE; +export const getVTGateMonitoringTemplate = (): string | undefined => env().VITE_VITESS_MONITORING_VTGATE_TEMPLATE; +export const getMysqlMonitoringTemplate = (): string | undefined => env().VITE_MYSQL_MONITORING_TEMPLATE; + +// Column titles for monitoring dashboards. +export const getVitessMonitoringDashboardTitle = (): string => + env().VITE_VITESS_MONITORING_DASHBOARD_TITLE || 'Vt Monitoring Dashboard'; +export const getMysqlMonitoringDashboardTitle = (): string => + env().VITE_MYSQL_MONITORING_DASHBOARD_TITLE || 'DB Monitoring Dashboard'; diff --git a/web/vtadmin/vite-env.d.ts b/web/vtadmin/vite-env.d.ts index 618e9062f4b..3e726c3169b 100644 --- a/web/vtadmin/vite-env.d.ts +++ b/web/vtadmin/vite-env.d.ts @@ -44,6 +44,20 @@ declare namespace Vite { // to also configure vtadmin-api for role-based access control (RBAC) if needed; // see https://github.com/vitessio/vitess/blob/main/go/vt/vtadmin/rbac/rbac.go VITE_READONLY_MODE?: string; + + // Optional. Monitoring URL templates. + // Supports placeholders like {cluster}, {keyspace}, {shard}, {hostname}, {alias}, {cell}, {pool} + VITE_VITESS_MONITORING_CLUSTER_TEMPLATE?: string; + VITE_VITESS_MONITORING_VTTABLET_TEMPLATE?: string; + VITE_VITESS_MONITORING_VTGATE_TEMPLATE?: string; + + // Optional. MySQL Monitoring URL template (e.g. for vtgates or tablets) + VITE_MYSQL_MONITORING_TEMPLATE?: string; + + // Optional. Column titles for monitoring dashboards. + // Defaults to "Vt Monitoring Dashboard" and "DB Monitoring Dashboard" respectively. + VITE_VITESS_MONITORING_DASHBOARD_TITLE?: string; + VITE_MYSQL_MONITORING_DASHBOARD_TITLE?: string; } }