## Architecture
This repository contains both Dashboard HTTP API and Dashboard UI. Dashboard HTTP API is placed in
`pkg/` directory, written in Golang. Dashboard UI is placed in `ui/` directory, powered by React.
TiDB Dashboard can also be integrated into PD, as follows:

## License
[Apache License](/LICENSE)
Copyright 2020 PingCAP, Inc.
[pd]: https://github.com/pingcap/pd
[asktug.com]: https://asktug.com/
================================================
FILE: SECURITY.md
================================================
# Security Vulnerability Disclosure and Response Process
TiDB is a fast-growing open source database. To ensure its security, a security vulnerability disclosure and response process is adopted.
The primary goal of this process is to reduce the total exposure time of users to publicly known vulnerabilities. To quickly fix vulnerabilities of TiDB products, the security team is responsible for the entire vulnerability management process, including internal communication and external disclosure.
If you find a vulnerability or encounter a security incident involving vulnerabilities of TiDB products, please report it as soon as possible to the TiDB security team (security@tidb.io).
Please kindly help provide as much vulnerability information as possible in the following format:
- Issue title*:
- Overview*:
- Affected components and version number*:
- CVE number (if any):
- Vulnerability verification process*:
- Contact information*:
The asterisk (*) indicates the required field.
# Response Time
The TiDB security team will confirm the vulnerabilities and contact you within 2 working days after your submission.
We will publicly thank you after fixing the security vulnerability. To avoid negative impact, please keep the vulnerability confidential until we fix it. We would appreciate it if you could obey the following code of conduct:
The vulnerability will not be disclosed until TiDB releases a patch for it.
The details of the vulnerability, for example, exploits code, will not be disclosed.
================================================
FILE: cmd/tidb-dashboard/main.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
// @title Dashboard API
// @version 1.0
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @BasePath /dashboard/api
// @query.collection.format multi
// @securityDefinitions.apikey JwtAuth
// @in header
// @name Authorization
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
_ "net/http/pprof" // #nosec
"os"
"os/signal"
"path"
"slices"
"strconv"
"strings"
"sync"
"syscall"
"github.com/pingcap/log"
flag "github.com/spf13/pflag"
"go.etcd.io/etcd/client/pkg/v3/transport"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/pingcap/tidb-dashboard/pkg/apiserver"
"github.com/pingcap/tidb-dashboard/pkg/config"
keyvisualregion "github.com/pingcap/tidb-dashboard/pkg/keyvisual/region"
"github.com/pingcap/tidb-dashboard/pkg/swaggerserver"
"github.com/pingcap/tidb-dashboard/pkg/uiserver"
"github.com/pingcap/tidb-dashboard/pkg/utils/version"
"github.com/pingcap/tidb-dashboard/util/distro"
)
type DashboardCLIConfig struct {
ListenHost string
ListenPort int
EnableDebugLog bool
CoreConfig *config.Config
// key-visual file mode for debug
KVFileStartTime int64
KVFileEndTime int64
}
// NewCLIConfig generates the configuration of the dashboard in standalone mode.
func NewCLIConfig() *DashboardCLIConfig {
cfg := &DashboardCLIConfig{}
cfg.CoreConfig = config.Default()
flag.StringVarP(&cfg.ListenHost, "host", "h", "127.0.0.1", "listen host of the Dashboard Server")
flag.IntVarP(&cfg.ListenPort, "port", "p", 12333, "listen port of the Dashboard Server")
flag.BoolVarP(&cfg.EnableDebugLog, "debug", "d", false, "enable debug logs")
flag.StringVar(&cfg.CoreConfig.DataDir, "data-dir", cfg.CoreConfig.DataDir, "path to the Dashboard Server data directory")
flag.StringVar(&cfg.CoreConfig.TempDir, "temp-dir", cfg.CoreConfig.TempDir, "path to the Dashboard Server temporary directory, used to store the searched logs")
flag.StringVar(&cfg.CoreConfig.PublicPathPrefix, "path-prefix", cfg.CoreConfig.PublicPathPrefix, "public URL path prefix for reverse proxies")
flag.StringVar(&cfg.CoreConfig.PDEndPoint, "pd", cfg.CoreConfig.PDEndPoint, "PD endpoint address that Dashboard Server connects to")
flag.BoolVar(&cfg.CoreConfig.EnableTelemetry, "telemetry", cfg.CoreConfig.EnableTelemetry, "allow telemetry")
flag.BoolVar(&cfg.CoreConfig.EnableExperimental, "experimental", cfg.CoreConfig.EnableExperimental, "allow experimental features")
flag.StringVar(&cfg.CoreConfig.FeatureVersion, "feature-version", cfg.CoreConfig.FeatureVersion, "target TiDB version for standalone mode")
flag.IntVar(&cfg.CoreConfig.NgmTimeout, "ngm-timeout", cfg.CoreConfig.NgmTimeout, "timeout secs for accessing the ngm API")
flag.BoolVar(&cfg.CoreConfig.EnableKeyVisualizer, "keyviz", true, "enable/disable key visualizer(default: true)")
flag.BoolVar(&cfg.CoreConfig.DisableCustomPromAddr, "disable-custom-prom-addr", false, "do not allow custom prometheus address")
showVersion := flag.BoolP("version", "v", false, "print version information and exit")
clusterCaPath := flag.String("cluster-ca", "", "(TLS between components of the TiDB cluster) path of file that contains list of trusted SSL CAs")
clusterCertPath := flag.String("cluster-cert", "", "(TLS between components of the TiDB cluster) path of file that contains X509 certificate in PEM format")
clusterKeyPath := flag.String("cluster-key", "", "(TLS between components of the TiDB cluster) path of file that contains X509 key in PEM format")
clusterAllowedNames := flag.String("cluster-allowed-names", "", "comma-delimited list of acceptable peer certificate SAN identities")
tidbCaPath := flag.String("tidb-ca", "", "(TLS for MySQL client) path of file that contains list of trusted SSL CAs")
tidbCertPath := flag.String("tidb-cert", "", "(TLS for MySQL client) path of file that contains X509 certificate in PEM format")
tidbKeyPath := flag.String("tidb-key", "", "(TLS for MySQL client) path of file that contains X509 key in PEM format")
tidbAllowedNames := flag.String("tidb-allowed-names", "", "comma-delimited list of acceptable peer certificate SAN identities")
// debug for keyvisual,hide help information
flag.Int64Var(&cfg.KVFileStartTime, "keyviz-file-start", 0, "(debug) start time for file range in file mode")
flag.Int64Var(&cfg.KVFileEndTime, "keyviz-file-end", 0, "(debug) end time for file range in file mode")
_ = flag.CommandLine.MarkHidden("keyviz-file-start")
_ = flag.CommandLine.MarkHidden("keyviz-file-end")
flag.Parse()
if *showVersion {
version.PrintStandaloneModeInfo()
_ = log.Sync()
os.Exit(0)
}
cfg.CoreConfig.NormalizePublicPathPrefix()
// setup TLS config for TiDB components
if len(*clusterCaPath) != 0 && len(*clusterCertPath) != 0 && len(*clusterKeyPath) != 0 {
tlsInfo := &transport.TLSInfo{
TrustedCAFile: *clusterCaPath,
KeyFile: *clusterKeyPath,
CertFile: *clusterCertPath,
}
cfg.CoreConfig.ClusterTLSInfo = tlsInfo
cfg.CoreConfig.ClusterTLSConfig = buildTLSConfig(tlsInfo, clusterAllowedNames)
}
// setup TLS config for MySQL client
// See https://github.com/pingcap/docs/blob/7a62321b3ce9318cbda8697503c920b2a01aeb3d/how-to/secure/enable-tls-clients.md#enable-authentication
if (len(*tidbCertPath) != 0 && len(*tidbKeyPath) != 0) || len(*tidbCaPath) != 0 {
tlsInfo := &transport.TLSInfo{
TrustedCAFile: *tidbCaPath,
KeyFile: *tidbKeyPath,
CertFile: *tidbCertPath,
}
cfg.CoreConfig.TiDBTLSConfig = buildTLSConfig(tlsInfo, tidbAllowedNames)
}
if err := cfg.CoreConfig.NormalizePDEndPoint(); err != nil {
log.Fatal("Invalid PD Endpoint", zap.Error(err))
}
// keyvisual check
startTime := cfg.KVFileStartTime
endTime := cfg.KVFileEndTime
if startTime != 0 || endTime != 0 {
// file mode (debug)
if startTime == 0 || endTime == 0 || startTime >= endTime {
log.Fatal("keyviz-file-start must be smaller than keyviz-file-end, and none of them are 0")
}
}
return cfg
}
func getContext() context.Context {
ctx, cancel := context.WithCancel(context.Background())
go func() {
sc := make(chan os.Signal, 1)
signal.Notify(sc,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT)
<-sc
cancel()
}()
return ctx
}
func buildTLSConfig(tlsInfo *transport.TLSInfo, allowedNames *string) *tls.Config {
tlsConfig, err := tlsInfo.ClientConfig()
if err != nil {
log.Fatal("Failed to load certificates", zap.Error(err))
}
// Disable the default server verification routine in favor of a manually defined connection
// verification callback. The custom verification process verifies that the server
// certificate is issued by a trusted root CA, and that the peer certificate identities
// matches at least one entry specified in verifyNames (if specified). This is required
// because tidb-dashboard directs requests to a loopback-bound forwarding proxy, which would
// otherwise cause server hostname verification to fail.
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
opts := x509.VerifyOptions{
Intermediates: x509.NewCertPool(),
Roots: tlsConfig.RootCAs,
}
for _, cert := range state.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}
_, err := state.PeerCertificates[0].Verify(opts)
// Optionally verify the peer SANs when available. If no peer identities are
// provided, simply reuse the verification result of the CA verification.
if err != nil || *allowedNames == "" {
return err
}
for name := range strings.SplitSeq(*allowedNames, ",") {
if slices.Contains(state.PeerCertificates[0].DNSNames, name) {
return nil
}
for _, uri := range state.PeerCertificates[0].URIs {
if name == uri.String() {
return nil
}
}
}
return fmt.Errorf(
"no SANs in server certificate (%v, %v) match allowed names %v",
state.PeerCertificates[0].DNSNames,
state.PeerCertificates[0].URIs,
strings.Split(*allowedNames, ","),
)
}
return tlsConfig
}
const (
distroResFolderName string = "distro-res"
distroStringsResFileName string = "strings.json"
)
func loadDistroStringsRes() {
exePath, err := os.Executable()
if err != nil {
log.Fatal("Failed to get executable path", zap.Error(err))
}
distroStringsResPath := path.Join(path.Dir(exePath), distroResFolderName, distroStringsResFileName)
distroStringsRes, err := distro.ReadResourceStringsFromFile(distroStringsResPath)
if err != nil {
log.Fatal("Failed to load distro strings res", zap.String("path", distroStringsResPath), zap.Error(err))
}
distro.ReplaceGlobal(distroStringsRes)
}
func main() {
// Flushing any buffered log entries
defer log.Sync() //nolint:errcheck
// init log will register the `pingcap-log` logfmt for
_, _, err := log.InitLogger(&log.Config{})
if err != nil {
log.Fatal("failed to init log", zap.Error(err))
}
cliConfig := NewCLIConfig()
ctx := getContext()
if cliConfig.EnableDebugLog {
log.SetLevel(zapcore.DebugLevel)
}
loadDistroStringsRes()
listenAddr := net.JoinHostPort(cliConfig.ListenHost, strconv.Itoa(cliConfig.ListenPort))
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
log.Fatal("Dashboard server listen failed", zap.String("addr", listenAddr), zap.Error(err))
}
var customKeyVisualProvider *keyvisualregion.DataProvider
if cliConfig.KVFileStartTime > 0 {
customKeyVisualProvider = &keyvisualregion.DataProvider{
FileStartTime: cliConfig.KVFileStartTime,
FileEndTime: cliConfig.KVFileEndTime,
}
}
assets := uiserver.Assets(cliConfig.CoreConfig)
s := apiserver.NewService(
cliConfig.CoreConfig,
apiserver.StoppedHandler,
assets,
customKeyVisualProvider,
)
if err := s.Start(ctx); err != nil {
log.Fatal("Can not start server", zap.Error(err))
}
defer s.Stop(context.Background()) //nolint:errcheck
mux := http.DefaultServeMux
uiHandler := http.StripPrefix(strings.TrimRight(config.UIPathPrefix, "/"), uiserver.Handler(assets))
mux.Handle("/", http.RedirectHandler(config.UIPathPrefix, http.StatusFound))
mux.Handle(config.UIPathPrefix, uiHandler)
mux.Handle(config.APIPathPrefix, apiserver.Handler(s))
mux.Handle(config.SwaggerPathPrefix, swaggerserver.Handler())
log.Info(fmt.Sprintf("Dashboard server is listening at %s", listenAddr))
log.Info(fmt.Sprintf("UI: http://%s/dashboard/", net.JoinHostPort(cliConfig.ListenHost, strconv.Itoa(cliConfig.ListenPort))))
log.Info(fmt.Sprintf("API: http://%s/dashboard/api/", net.JoinHostPort(cliConfig.ListenHost, strconv.Itoa(cliConfig.ListenPort))))
log.Info(fmt.Sprintf("Swagger: http://%s/dashboard/api/swagger/", net.JoinHostPort(cliConfig.ListenHost, strconv.Itoa(cliConfig.ListenPort))))
srv := &http.Server{Handler: mux} // nolint:gosec
var wg sync.WaitGroup
wg.Go(func() {
if err := srv.Serve(listener); err != http.ErrServerClosed {
log.Error("Server aborted with an error", zap.Error(err))
}
})
<-ctx.Done()
if err := srv.Shutdown(context.Background()); err != nil {
log.Error("Can not stop server", zap.Error(err))
}
wg.Wait()
log.Info("Stop dashboard server")
}
================================================
FILE: dockerfiles/docker-compose.yml
================================================
version: '2'
services:
tidb-dashboard:
image: pingcap/tidb-dashboard:nightly
ports:
- "12333:12333"
command:
- --pd=http://pd:2379
- --debug
- --experimental
- --feature-version=999.999.999
- --host=0.0.0.0
depends_on:
- "pd"
restart: on-failure
pd:
image: pingcap/pd:nightly
ports:
- "2379:2379"
command:
- --name=pd
- --client-urls=http://0.0.0.0:2379
- --peer-urls=http://0.0.0.0:2380
- --advertise-client-urls=http://pd:2379
- --advertise-peer-urls=http://pd:2380
- --initial-cluster=pd=http://pd:2380
restart: on-failure
tikv:
image: pingcap/tikv:nightly
ports:
- "20180:20180"
command:
- --addr=0.0.0.0:20160
- --advertise-addr=tikv:20160
- --status-addr=0.0.0.0:20180
- --pd=pd:2379
depends_on:
- "pd"
restart: on-failure
tidb:
image: pingcap/tidb:nightly
ports:
- "4000:4000"
- "10080:10080"
command:
- --host=0.0.0.0
- --advertise-address=tidb
- --store=tikv
- --path=pd:2379
depends_on:
- "tikv"
restart: on-failure
tiflash:
image: pingcap/tiflash:nightly
volumes:
- ./tiflash.toml:/tiflash.toml:ro
ports:
- "9000:9000"
- "8123:8123"
- "8234:8234"
- "3930:3930"
- "20170:20170"
- "20292:20292"
command:
- --config=/tiflash.toml
depends_on:
- "tikv"
- "tidb"
restart: on-failure
error-metric-trigger:
image: mariadb:10.6.5
command:
- mysql
- -uroot
- -htidb
- -P4000
- -e
- "select * from foo.bar;"
depends_on:
- "tikv"
- "tidb"
- "tiflash"
restart: on-failure
================================================
FILE: etc/go.mod
================================================
module ignore_etc // a hack to ignore this directory in go commands
go 1.13
================================================
FILE: etc/manualTestEnv/.gitignore
================================================
.vagrant/
tiup-cluster-*.log
================================================
FILE: etc/manualTestEnv/_shared/Vagrantfile.partial.pubKey.rb
================================================
Vagrant.configure("2") do |config|
ssh_pub_key = File.readlines("#{File.dirname(__FILE__)}/vagrant_key.pub").first.strip
config.vm.box = "hashicorp/bionic64"
config.vm.provision "zsh", type: "shell", privileged: false, inline: <<-SHELL
echo "Installing zsh"
sudo apt install -y zsh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
sudo chsh -s /usr/bin/zsh vagrant
SHELL
config.vm.provision "private_key", type: "shell", privileged: false, inline: <<-SHELL
echo "Inserting private key"
echo #{ssh_pub_key} >> /home/vagrant/.ssh/authorized_keys
SHELL
config.vm.provision "ulimit", type: "shell", privileged: true, inline: <<-SHELL
echo "Setting ulimit"
echo "fs.file-max = 65535" >> /etc/sysctl.conf
sysctl -p
echo "* hard nofile 65535" >> /etc/security/limits.conf
echo "* soft nofile 65535" >> /etc/security/limits.conf
echo "root hard nofile 65535" >> /etc/security/limits.conf
echo "root hard nofile 65535" >> /etc/security/limits.conf
SHELL
end
================================================
FILE: etc/manualTestEnv/_shared/vagrant_key
================================================
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAxboZzYumqNoVOQ/hKKhIZHxNhf5tmnkLZry8i6Xur4FPLDiRxos/
xVVDx0ynTPOyQVVaXtNxZnAmbR4HuNBzRvNoklwSXazt5YgWeiKCHtPpKFt3PJeE2cn6FJ
p6F6qFChG0NSPbZxJWWxv4noX0U3PLKgHNIehYK2Fu0E6plhSZazzJEVWapwo9d7aGnAsz
bBCd5TNZ5ogrXn+3bSFcdCbAfWOwYg54a+PzTQlzgt6JmhlEjpFfPhhpBW92pQXxmQ2c17
iPCbA8G++FiaEwA5teex8k1+HzmHf7YjyhPr+I67EzEiIueJg2+0PYbM1p06S8kVTNDXsf
0eJx4Dr8qQAAA9iFPcpVhT3KVQAAAAdzc2gtcnNhAAABAQDFuhnNi6ao2hU5D+EoqEhkfE
2F/m2aeQtmvLyLpe6vgU8sOJHGiz/FVUPHTKdM87JBVVpe03FmcCZtHge40HNG82iSXBJd
rO3liBZ6IoIe0+koW3c8l4TZyfoUmnoXqoUKEbQ1I9tnElZbG/iehfRTc8sqAc0h6FgrYW
7QTqmWFJlrPMkRVZqnCj13toacCzNsEJ3lM1nmiCtef7dtIVx0JsB9Y7BiDnhr4/NNCXOC
3omaGUSOkV8+GGkFb3alBfGZDZzXuI8JsDwb74WJoTADm157HyTX4fOYd/tiPKE+v4jrsT
MSIi54mDb7Q9hszWnTpLyRVM0Nex/R4nHgOvypAAAAAwEAAQAAAQBtk0+/YDgQ9SKzx8AQ
xwmvXk+cBT76T0BpRAj9HwziiDe3GvZ2YC8MDc+NAEbq11ae7E0zpdv/WAGDkRPYcPShij
0Wdx3aef4wqLVEJCGWMfvRWLcAhjuiclM73cvxl5c42EzU8jUhrsDapuql9zhKky4w7mSe
+OL7z3gYyq8isvcQMe+1eXJqiv27AJJfAir+rLJZO/gDW36hOowhnZxYRlVYPgZ8GwetxD
VdCrgwUgR/2HYmbXYdVxI0PwswGc6rEqs5XXOYRzwvPTvRKdD3J5MxmsvJljT7FMr4kCLT
X1+aWysk1cgAUIdzzwQL8DLE/N9PFFYdZyNBkZMgedl9AAAAgCtP3F8XYFR18gQLPGLDyQ
FFg8+JHN9b/yIg2pymC6SI8qEp+GnuEK9IKhqh/Uw14KEKcs/9sgbZo0K9uTBTDG5F6Qmp
hADVbWXJ/97Xeya6kH2Sa56UKLCQ/uQWBKwLQ0auU/qwxATIZowh31XUXjzVBg6wgUjT7Q
+3Fk1zGYxnAAAAgQD5USIRUNwkI+htv+f1g8QdmrFAGymcGEkXAixKvBTon9cWQb2iyiK+
2IO8EwFwRdL5kw2foILCnlp/4FevfxHU7wTcoFEp3PItUlcxYqO8vY2VCZ913oNLKBIt9p
uFfG2BZM5szMRNMh0svelu61FePsfN5Z8J0ltPrS8UKB95ywAAAIEAywbyNbjz1AxEjWIX
2Vbk4/MjQyjui8Wi7H0F+LDWyMfPJHzhnbr79Z/lIZmDAo++3EYU9J9s0C+wJ6vXGK+gvC
7e5qGfT/0J0DwBfLbpeTdDELCa/LmfLWVPzZ9Q+9Fq0AjmW9YXFZ/+qT9xfY1v9XfztFRS
xR1iXJ42q6ff5NsAAAAeYnJlZXpld2lzaEBCcmVlemV3aXNoTUJQLmxvY2FsAQIDBAU=
-----END OPENSSH PRIVATE KEY-----
================================================
FILE: etc/manualTestEnv/_shared/vagrant_key.pub
================================================
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFuhnNi6ao2hU5D+EoqEhkfE2F/m2aeQtmvLyLpe6vgU8sOJHGiz/FVUPHTKdM87JBVVpe03FmcCZtHge40HNG82iSXBJdrO3liBZ6IoIe0+koW3c8l4TZyfoUmnoXqoUKEbQ1I9tnElZbG/iehfRTc8sqAc0h6FgrYW7QTqmWFJlrPMkRVZqnCj13toacCzNsEJ3lM1nmiCtef7dtIVx0JsB9Y7BiDnhr4/NNCXOC3omaGUSOkV8+GGkFb3alBfGZDZzXuI8JsDwb74WJoTADm157HyTX4fOYd/tiPKE+v4jrsTMSIi54mDb7Q9hszWnTpLyRVM0Nex/R4nHgOvyp
================================================
FILE: etc/manualTestEnv/complexCase1/README.md
================================================
# complexCase1
TiDB, PD, TiKV, TiFlash each in different hosts.
## Usage
1. Start the box:
```bash
VAGRANT_EXPERIMENTAL="disks" vagrant up
```
1. Use [TiUP](https://tiup.io/) to deploy the cluster to the box (only need to do it once):
```bash
tiup cluster deploy complexCase1 v4.0.8 topology.yaml -i ../_shared/vagrant_key -y --user vagrant
```
1. Start the cluster in the box:
```bash
tiup cluster start complexCase1
```
1. Start TiDB Dashboard server:
```bash
bin/tidb-dashboard --pd http://10.0.1.31:2379
```
## Cleanup
```bash
tiup cluster destroy complexCase1 -y
vagrant destroy --force
```
================================================
FILE: etc/manualTestEnv/complexCase1/Vagrantfile
================================================
load "#{File.dirname(__FILE__)}/../_shared/Vagrantfile.partial.pubKey.rb"
Vagrant.configure("2") do |config|
config.vm.provider "virtualbox" do |v|
v.memory = 1024
v.cpus = 1
end
(1..5).each do |i|
config.vm.define "node#{i}" do |node|
node.vm.network "private_network", ip: "10.0.1.#{i+30}"
(1..4).each do |j|
node.vm.disk :disk, size: "10GB", name: "disk-#{i}-#{j}"
end
end
end
config.vm.provision "disk", type: "shell", privileged: false, inline: <<-SHELL
echo "Formatting disks"
sudo mkfs.ext4 -j -L hdd1 /dev/sdb
sudo mkfs.ext4 -j -L hdd2 /dev/sdc
sudo mkfs.ext4 -j -L hdd3 /dev/sdd
sudo mkfs.ext4 -j -L hdd4 /dev/sde
echo "Mounting directories"
sudo mkdir -p /pingcap/tidb-data
echo "/dev/sdb /pingcap/tidb-data ext4 defaults 0 0" | sudo tee -a /etc/fstab
sudo mount /pingcap/tidb-data
sudo mkdir -p /pingcap/tidb-deploy
sudo mkdir -p /pingcap/tidb-data/tikv-1
sudo mkdir -p /pingcap/tidb-data/tikv-2
echo "/dev/sdc /pingcap/tidb-deploy ext4 defaults 0 0" | sudo tee -a /etc/fstab
echo "/dev/sdd /pingcap/tidb-data/tikv-1 ext4 defaults 0 0" | sudo tee -a /etc/fstab
echo "/dev/sde /pingcap/tidb-data/tikv-2 ext4 defaults 0 0" | sudo tee -a /etc/fstab
sudo mount /pingcap/tidb-deploy
sudo mount /pingcap/tidb-data/tikv-1
sudo mount /pingcap/tidb-data/tikv-2
SHELL
end
================================================
FILE: etc/manualTestEnv/complexCase1/topology.yaml
================================================
global:
user: tidb
deploy_dir: /pingcap/tidb-deploy
data_dir: /pingcap/tidb-data
server_configs:
tikv:
server.grpc-concurrency: 1
raftstore.apply-pool-size: 1
raftstore.store-pool-size: 1
readpool.unified.max-thread-count: 1
readpool.storage.use-unified-pool: false
readpool.coprocessor.use-unified-pool: true
storage.block-cache.capacity: 256MB
raftstore.capacity: 5GB
# Overview:
# 31: 1 PD, 1 TiDB, 2 TiKV
# 32: 1 TiDB, 2 TiKV
# 33: 1 PD, 1 TiFlash
# 34: 2 TiKV, 1 TiFlash
# 35: 1 TiFlash
pd_servers:
- host: 10.0.1.31
- host: 10.0.1.33
tikv_servers:
- host: 10.0.1.31
port: 20160
status_port: 20180
data_dir: /pingcap/tidb-data/tikv-1/tikv-20160
config:
server.labels: { host: "tikv1" }
- host: 10.0.1.31
port: 20161
status_port: 20181
data_dir: /pingcap/tidb-data/tikv-2/tikv-20161
config:
server.labels: { host: "tikv2" }
- host: 10.0.1.32
port: 20160
status_port: 20180
data_dir: /pingcap/tidb-data/tikv-1/tikv-20160
config:
server.labels: { host: "tikv1" }
- host: 10.0.1.32
port: 20161
status_port: 20181
data_dir: /pingcap/tidb-data/tikv-2/tikv-20161
config:
server.labels: { host: "tikv2" }
- host: 10.0.1.34
port: 20160
status_port: 20180
data_dir: /pingcap/tidb-data/tikv-1/tikv-20160
config:
server.labels: { host: "tikv1" }
- host: 10.0.1.34
port: 20161
status_port: 20181
data_dir: /pingcap/tidb-data/tikv-2/tikv-20161
config:
server.labels: { host: "tikv2" }
tiflash_servers:
- host: 10.0.1.33
data_dir: /pingcap/tidb-data/tikv-1/tiflash
- host: 10.0.1.34
data_dir: /pingcap/tidb-data/tikv-2/tiflash
- host: 10.0.1.35
data_dir: /pingcap/tidb-data/tikv-1/tiflash
tidb_servers:
- host: 10.0.1.31
- host: 10.0.1.32
grafana_servers:
- host: 10.0.1.31
monitoring_servers:
- host: 10.0.1.31
alertmanager_servers:
- host: 10.0.1.31
================================================
FILE: etc/manualTestEnv/multiHost/README.md
================================================
# multiHost
TiDB, PD, TiKV, TiFlash each in different hosts.
## Usage
1. Start the box:
```bash
vagrant up
```
1. Use [TiUP](https://tiup.io/) to deploy the cluster to the box (only need to do it once):
```bash
tiup cluster deploy multiHost v4.0.8 topology.yaml -i ../_shared/vagrant_key -y --user vagrant
```
1. Start the cluster in the box:
```bash
tiup cluster start multiHost
```
1. Start TiDB Dashboard server:
```bash
bin/tidb-dashboard --pd http://10.0.1.11:2379
```
## Cleanup
```bash
tiup cluster destroy multiHost -y
vagrant destroy --force
```
================================================
FILE: etc/manualTestEnv/multiHost/Vagrantfile
================================================
load "#{File.dirname(__FILE__)}/../_shared/Vagrantfile.partial.pubKey.rb"
Vagrant.configure("2") do |config|
config.vm.provider "virtualbox" do |v|
v.memory = 1024
v.cpus = 1
end
(1..4).each do |i|
config.vm.define "node#{i}" do |node|
node.vm.network "private_network", ip: "10.0.1.#{i+10}"
end
end
end
================================================
FILE: etc/manualTestEnv/multiHost/topology.yaml
================================================
global:
user: tidb
deploy_dir: tidb-deploy
data_dir: tidb-data
server_configs:
tikv:
server.grpc-concurrency: 1
raftstore.apply-pool-size: 1
raftstore.store-pool-size: 1
readpool.unified.max-thread-count: 1
readpool.storage.use-unified-pool: false
readpool.coprocessor.use-unified-pool: true
storage.block-cache.capacity: 256MB
raftstore.capacity: 10GB
pd:
replication.enable-placement-rules: true
pd_servers:
- host: 10.0.1.11
- host: 10.0.1.12
- host: 10.0.1.13
tikv_servers:
- host: 10.0.1.12
tidb_servers:
- host: 10.0.1.11
- host: 10.0.1.12
- host: 10.0.1.13
tiflash_servers:
- host: 10.0.1.14
grafana_servers:
- host: 10.0.1.11
monitoring_servers:
- host: 10.0.1.11
alertmanager_servers:
- host: 10.0.1.11
================================================
FILE: etc/manualTestEnv/multiReplica/README.md
================================================
# multiReplica
Multiple TiKV nodes in different labels.
## Usage
1. Start the box:
```bash
vagrant up
```
1. Use [TiUP](https://tiup.io/) to deploy the cluster to the box (only need to do it once):
```bash
tiup cluster deploy multiReplica v4.0.8 topology.yaml -i ../_shared/vagrant_key -y --user vagrant
```
1. Start the cluster in the box:
```bash
tiup cluster start multiReplica
```
1. Start TiDB Dashboard server:
```bash
bin/tidb-dashboard --pd http://10.0.1.20:2379
```
## Cleanup
```bash
tiup cluster destroy multiReplica -y
vagrant destroy --force
```
================================================
FILE: etc/manualTestEnv/multiReplica/Vagrantfile
================================================
load "#{File.dirname(__FILE__)}/../_shared/Vagrantfile.partial.pubKey.rb"
Vagrant.configure("2") do |config|
config.vm.provider "virtualbox" do |v|
v.memory = 4 * 1024
v.cpus = 2
end
config.vm.network "private_network", ip: "10.0.1.20"
end
================================================
FILE: etc/manualTestEnv/multiReplica/topology.yaml
================================================
global:
user: tidb
deploy_dir: tidb-deploy
data_dir: tidb-data
server_configs:
tikv:
server.grpc-concurrency: 1
raftstore.apply-pool-size: 1
raftstore.store-pool-size: 1
readpool.unified.max-thread-count: 1
readpool.storage.use-unified-pool: false
readpool.coprocessor.use-unified-pool: true
storage.block-cache.capacity: 256MB
raftstore.capacity: 10GB
pd:
replication.location-labels:
- zone
- rack
- host
pd_servers:
- host: 10.0.1.20
tikv_servers:
- host: 10.0.1.20
port: 20160
status_port: 20180
config:
server.labels: { host: tikv1, rack: rack1 }
- host: 10.0.1.20
port: 20161
status_port: 20181
config:
server.labels: { host: tikv1, rack: rack1 }
- host: 10.0.1.20
port: 20162
status_port: 20182
config:
server.labels: { host: tikv2, rack: rack1 }
- host: 10.0.1.20
port: 20163
status_port: 20183
config:
server.labels: { host: tikv2, rack: rack1 }
- host: 10.0.1.20
port: 20164
status_port: 20184
config:
server.labels: { host: tikv3, rack: rack2 }
- host: 10.0.1.20
port: 20165
status_port: 20185
config:
server.labels: { host: tikv3, rack: rack2 }
tidb_servers:
- host: 10.0.1.20
grafana_servers:
- host: 10.0.1.20
monitoring_servers:
- host: 10.0.1.20
================================================
FILE: etc/manualTestEnv/singleHost/README.md
================================================
# singleHost
TiDB, PD, TiKV, TiFlash in the same host.
## Usage
1. Start the box:
```bash
vagrant up
```
1. Use [TiUP](https://tiup.io/) to deploy the cluster to the box (only need to do it once):
```bash
tiup cluster deploy singleHost v4.0.8 topology.yaml -i ../_shared/vagrant_key -y --user vagrant
```
1. Start the cluster in the box:
```bash
tiup cluster start singleHost
```
1. Start TiDB Dashboard server:
```bash
bin/tidb-dashboard --pd http://10.0.1.2:2379
```
## Cleanup
```bash
tiup cluster destroy singleHost -y
vagrant destroy --force
```
================================================
FILE: etc/manualTestEnv/singleHost/Vagrantfile
================================================
load "#{File.dirname(__FILE__)}/../_shared/Vagrantfile.partial.pubKey.rb"
Vagrant.configure("2") do |config|
config.vm.provider "virtualbox" do |v|
v.memory = 3 * 1024
v.cpus = 2
end
config.vm.network "private_network", ip: "10.0.1.2"
end
================================================
FILE: etc/manualTestEnv/singleHost/topology.yaml
================================================
global:
user: tidb
deploy_dir: tidb-deploy
data_dir: tidb-data
server_configs:
tikv:
server.grpc-concurrency: 1
raftstore.apply-pool-size: 1
raftstore.store-pool-size: 1
readpool.unified.max-thread-count: 1
readpool.storage.use-unified-pool: false
readpool.coprocessor.use-unified-pool: true
storage.block-cache.capacity: 256MB
pd:
replication.enable-placement-rules: true
pd_servers:
- host: 10.0.1.2
tikv_servers:
- host: 10.0.1.2
tidb_servers:
- host: 10.0.1.2
tiflash_servers:
- host: 10.0.1.2
grafana_servers:
- host: 10.0.1.2
monitoring_servers:
- host: 10.0.1.2
alertmanager_servers:
- host: 10.0.1.2
================================================
FILE: etc/manualTestEnv/singleHostMultiDisk/.gitignore
================================================
data/
================================================
FILE: etc/manualTestEnv/singleHostMultiDisk/README.md
================================================
# singleHostMultiDisk
All instances in a single host, but on different disks.
## Usage
1. Start the box:
```bash
vagrant up
```
1. Use [TiUP](https://tiup.io/) to deploy the cluster to the box (only need to do it once):
```bash
tiup cluster deploy singleHostMultiDisk v4.0.8 topology.yaml -i ../_shared/vagrant_key -y --user vagrant
```
1. Start the cluster in the box:
```bash
tiup cluster start singleHostMultiDisk
```
1. Start TiDB Dashboard server:
```bash
bin/tidb-dashboard --pd http://10.0.1.3:2379
```
## Cleanup
```bash
tiup cluster destroy singleHostMultiDisk -y
vagrant destroy --force
```
================================================
FILE: etc/manualTestEnv/singleHostMultiDisk/Vagrantfile
================================================
load "#{File.dirname(__FILE__)}/../_shared/Vagrantfile.partial.pubKey.rb"
Vagrant.configure("2") do |config|
config.vm.provider "virtualbox" do |v|
v.memory = 3 * 1024
v.cpus = 2
end
config.vm.network "private_network", ip: "10.0.1.3"
end
================================================
FILE: etc/manualTestEnv/singleHostMultiDisk/topology.yaml
================================================
global:
user: vagrant
deploy_dir: tidb-deploy
data_dir: tidb-data
server_configs:
tikv:
server.grpc-concurrency: 1
raftstore.apply-pool-size: 1
raftstore.store-pool-size: 1
readpool.unified.max-thread-count: 1
readpool.storage.use-unified-pool: false
readpool.coprocessor.use-unified-pool: true
storage.block-cache.capacity: 256MB
pd:
replication.enable-placement-rules: true
pd_servers:
- host: 10.0.1.3
tikv_servers:
- host: 10.0.1.3
tidb_servers:
- host: 10.0.1.3
deploy_dir: /vagrant/data/tidb
log_dir: /vagrant/data/tidb/log
tiflash_servers:
- host: 10.0.1.3
grafana_servers:
- host: 10.0.1.3
monitoring_servers:
- host: 10.0.1.3
alertmanager_servers:
- host: 10.0.1.3
================================================
FILE: go.mod
================================================
module github.com/pingcap/tidb-dashboard
go 1.25.7
require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/Masterminds/semver v1.5.0
github.com/ReneKroon/ttlcache/v2 v2.3.0
github.com/VividCortex/mysqlerr v1.0.0
github.com/Xeoncross/go-aesctr-with-hmac v0.0.0-20200623134604-12b17a7ff502
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/antonmedv/expr v1.9.0
github.com/appleboy/gin-jwt/v2 v2.10.3
github.com/bitly/go-simplejson v0.5.0
github.com/cenkalti/backoff/v4 v4.2.1
github.com/fatih/structtag v1.2.0
github.com/gin-contrib/gzip v0.0.1
github.com/gin-gonic/gin v1.11.0
github.com/go-resty/resty/v2 v2.6.0
github.com/go-sql-driver/mysql v1.7.0
github.com/goccy/go-graphviz v0.0.9
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang/snappy v0.0.4
github.com/google/pprof v0.0.0-20211122183932-1daafda22083
github.com/google/uuid v1.6.0
github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69
github.com/henrylee2cn/ameda v1.4.10
github.com/jarcoal/httpmock v1.0.8
github.com/joho/godotenv v1.4.0
github.com/joomcode/errorx v1.0.1
github.com/json-iterator/go v1.1.12
github.com/minio/sio v0.3.0
github.com/oleiade/reflections v1.0.1
github.com/pingcap/check v0.0.0-20191216031241-8a5a85928f12
github.com/pingcap/errors v0.11.5-0.20200917111840-a15ef68f753d
github.com/pingcap/kvproto v0.0.0-20200411081810-b85805c9476c
github.com/pingcap/log v0.0.0-20210906054005-afc726e70354
github.com/pingcap/tipb v0.0.0-20220718022156-3e2483c20a9e
github.com/rs/cors v1.7.0
github.com/samber/lo v1.37.0
github.com/shhdgit/testfixtures/v3 v3.6.2-0.20211219171712-c4f264d673d3
github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger v1.2.6
github.com/swaggo/swag v1.7.9
github.com/vmihailenco/msgpack/v5 v5.3.5
go.etcd.io/etcd/client/pkg/v3 v3.5.15
go.etcd.io/etcd/client/v3 v3.5.15
go.uber.org/atomic v1.9.0
go.uber.org/fx v1.12.0
go.uber.org/goleak v1.1.10
go.uber.org/zap v1.19.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.18.0
google.golang.org/grpc v1.75.1
google.golang.org/protobuf v1.36.10
gorm.io/datatypes v1.1.0
gorm.io/driver/mysql v1.4.5
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
moul.io/zapgorm2 v1.1.0
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.etcd.io/etcd/api/v3 v3.5.15 // indirect
go.uber.org/dig v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/tools/godoc v0.1.0-deprecated // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/ReneKroon/ttlcache/v2 v2.3.0 h1:qZnUjRKIrbKHH6vF5T7Y9Izn5ObfTZfyYpGhvz2BKPo=
github.com/ReneKroon/ttlcache/v2 v2.3.0/go.mod h1:zbo6Pv/28e21Z8CzzqgYRArQYGYtjONRxaAKGxzQvG4=
github.com/VividCortex/mysqlerr v1.0.0 h1:5pZ2TZA+YnzPgzBfiUWGqWmKDVNBdrkf9g+DNe1Tiq8=
github.com/VividCortex/mysqlerr v1.0.0/go.mod h1:xERx8E4tBhLvpjzdUyQiSfUxeMcATEQrflDAfXsqcAE=
github.com/Xeoncross/go-aesctr-with-hmac v0.0.0-20200623134604-12b17a7ff502 h1:L8IbaI/W6h5Cwgh0n4zGeZpVK78r/jBf9ASurHo9+/o=
github.com/Xeoncross/go-aesctr-with-hmac v0.0.0-20200623134604-12b17a7ff502/go.mod h1:pmnBM9bxWSiHvC/gSWunUIyDvGn33EkP2CUjxFKtTTM=
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU=
github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
github.com/appleboy/gin-jwt/v2 v2.10.3 h1:KNcPC+XPRNpuoBh+j+rgs5bQxN+SwG/0tHbIqpRoBGc=
github.com/appleboy/gin-jwt/v2 v2.10.3/go.mod h1:LDUaQ8mF2W6LyXIbd5wqlV2SFebuyYs4RDwqMNgpsp8=
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/corona10/goimagehash v1.0.2 h1:pUfB0LnsJASMPGEZLj7tGY251vF+qLGqOgEP4rUs6kA=
github.com/corona10/goimagehash v1.0.2/go.mod h1:/l9umBhvcHQXVtQO1V6Gp1yD20STawkhRnnX0D1bvVI=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA=
github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc=
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4=
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-graphviz v0.0.9 h1:s/FMMJ1Joj6La3S5ApO3Jk2cwM4LpXECC2muFx3IPQQ=
github.com/goccy/go-graphviz v0.0.9/go.mod h1:wXVsXxmyMQU6TN3zGRttjNn3h+iCAS7xQFC6TlNvLhk=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20211122183932-1daafda22083 h1:c8EUapQFi+kjzedr4c6WqbwMdmB95+oDBWZ5XFHFYxY=
github.com/google/pprof v0.0.0-20211122183932-1daafda22083/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c=
github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 h1:7xsUJsB2NrdcttQPa7JLEaGzvdbk7KvfrjgHZXOQRo0=
github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69/go.mod h1:YLEMZOtU+AZ7dhN9T/IpGhXVGly2bvkJQ+zxj3WeVQo=
github.com/henrylee2cn/ameda v1.4.10 h1:JdvI2Ekq7tapdPsuhrc4CaFiqw6QXFvZIULWJgQyCAk=
github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d h1:uGg2frlt3IcT7kbV6LEp5ONv4vmoO2FW4qSO+my/aoM=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.3.0/go.mod h1:b0JqxHvPmljG+HQ5IsvQ0yqeSi4nGcDTVjFoiLDb0Ik=
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg=
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/joomcode/errorx v1.0.1 h1:CalpDWz14ZHd68fIqluJasJosAewpz2TFaJALrUxjrk=
github.com/joomcode/errorx v1.0.1/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
github.com/minio/sio v0.3.0 h1:syEFBewzOMOYVzSTFpp1MqpSZk8rUNbz8VIIc+PNzus=
github.com/minio/sio v0.3.0/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY=
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM=
github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60=
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ=
github.com/pingcap/check v0.0.0-20191216031241-8a5a85928f12 h1:rfD9v3+ppLPzoQBgZev0qYCpegrwyFx/BUpkApEiKdY=
github.com/pingcap/check v0.0.0-20191216031241-8a5a85928f12/go.mod h1:PYMCGwN0JHjoqGr3HrZoD+b8Tgx8bKnArhSq8YVzUMc=
github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pingcap/errors v0.11.5-0.20200917111840-a15ef68f753d h1:TH18wFO5Nq/zUQuWu9ms2urgZnLP69XJYiI2JZAkUGc=
github.com/pingcap/errors v0.11.5-0.20200917111840-a15ef68f753d/go.mod h1:g4vx//d6VakjJ0mk7iLBlKA8LFavV/sAVINT/1PFxeQ=
github.com/pingcap/kvproto v0.0.0-20200411081810-b85805c9476c h1:wO9VvZezAU4ZPZj8+P5uWfsT/ppuABjJPmHNrpCQnlc=
github.com/pingcap/kvproto v0.0.0-20200411081810-b85805c9476c/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI=
github.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=
github.com/pingcap/log v0.0.0-20200511115504-543df19646ad/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=
github.com/pingcap/log v0.0.0-20210906054005-afc726e70354 h1:SvWCbCPh1YeHd9yQLksvJYAgft6wLTY1aNG81tpyscQ=
github.com/pingcap/log v0.0.0-20210906054005-afc726e70354/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4=
github.com/pingcap/tipb v0.0.0-20220718022156-3e2483c20a9e h1:FBaTXU8C3xgt/drM58VHxojHo/QoG1oPsgWTGvaSpO4=
github.com/pingcap/tipb v0.0.0-20220718022156-3e2483c20a9e/go.mod h1:A7mrd7WHBl1o63LE2bIBGEJMTNWXqhgmYiOvMLxozfs=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shhdgit/testfixtures/v3 v3.6.2-0.20211219171712-c4f264d673d3 h1:qgLFG8/LS7dhYw6SF6yIx+Nfpf4Md9/oxtAYTjl9ayk=
github.com/shhdgit/testfixtures/v3 v3.6.2-0.20211219171712-c4f264d673d3/go.mod h1:Z0OLtuFJ7Y4yLsVijHK8uq95NjGFlYJy+I00ElAEtUQ=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0 h1:mj/nMDAwTBiaCqMEs4cYCqF7pO6Np7vhy1D1wcQGz+E=
github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM=
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/http-swagger v1.2.6 h1:ihTjChUoSRMpFMjWw+0AkL1Ti4r6v8pCgVYLmQVRlRw=
github.com/swaggo/http-swagger v1.2.6/go.mod h1:CcoICgY3yVDk2u1LQUCMHbAj0fjlxIX+873psXlIKNA=
github.com/swaggo/swag v1.7.9 h1:6vCG5mm43ebDzGlZPMGYrYI4zKFfOr5kicQX8qjeDwc=
github.com/swaggo/swag v1.7.9/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/dig v1.9.0 h1:pJTDXKEhRqBI8W7rU7kwT5EgyRZuSMVSFcZolOvKK9U=
go.uber.org/dig v1.9.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw=
go.uber.org/fx v1.12.0 h1:+1+3Cz9M0dFMPy9SW9XUIUHye8bnPUm7q7DroNGWYG4=
go.uber.org/fx v1.12.0/go.mod h1:egT3Kyg1JFYQkvKLZ3EsykxkNrZxgXS+gKoKo7abERY=
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE=
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191030062658-86caa796c7ab/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191107010934-f79515f33823/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201125231158-b5590deeca9b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.1.0 h1:EVp1Z28N4ACpYFK1nHboEIJGIFfjY7vLeieDk8jSHJA=
gorm.io/datatypes v1.1.0/go.mod h1:SH2K9R+2RMjuX1CkCONrPwoe9JzVv2hkQvEu4bXGojE=
gorm.io/driver/mysql v1.4.5 h1:u1lytId4+o9dDaNcPCFzNv7h6wvmc92UjNk3z8enSBU=
gorm.io/driver/mysql v1.4.5/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
moul.io/zapgorm2 v1.1.0 h1:qwAlMBYf+qJkJ7PAzJl4oCe6eS6QGiKAXUPeis0+RBE=
moul.io/zapgorm2 v1.1.0/go.mod h1:emRfKjNqSzVj5lcgasBdovIXY1jSOwFz2GQZn1Rddks=
================================================
FILE: pkg/apiserver/apiserver.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package apiserver
import (
"context"
"io"
"net/http"
"sync"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
cors "github.com/rs/cors/wrapper/gin"
"go.uber.org/fx"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/clusterinfo"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/configuration"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/conprof"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/deadlock"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/debugapi"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/diagnose"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/info"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/logsearch"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/metrics"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/profiling"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/queryeditor"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/topsql"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user/code"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user/code/codeauth"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user/sqlauth"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user/sso"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user/sso/ssoauth"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/visualplan"
"github.com/pingcap/tidb-dashboard/pkg/scheduling"
"github.com/pingcap/tidb-dashboard/pkg/ticdc"
"github.com/pingcap/tidb-dashboard/pkg/tiflash"
"github.com/pingcap/tidb-dashboard/pkg/tiproxy"
"github.com/pingcap/tidb-dashboard/pkg/tso"
"github.com/pingcap/tidb-dashboard/pkg/utils/version"
"github.com/pingcap/tidb-dashboard/util/client/httpclient"
"github.com/pingcap/tidb-dashboard/util/client/pdclient"
"github.com/pingcap/tidb-dashboard/util/client/schedulingclient"
"github.com/pingcap/tidb-dashboard/util/client/ticdcclient"
"github.com/pingcap/tidb-dashboard/util/client/tidbclient"
"github.com/pingcap/tidb-dashboard/util/client/tiflashclient"
"github.com/pingcap/tidb-dashboard/util/client/tikvclient"
"github.com/pingcap/tidb-dashboard/util/client/tiproxyclient"
"github.com/pingcap/tidb-dashboard/util/client/tsoclient"
"github.com/pingcap/tidb-dashboard/util/featureflag"
"github.com/pingcap/tidb-dashboard/util/rest"
// "github.com/pingcap/tidb-dashboard/pkg/apiserver/__APP_NAME__"
// NOTE: Don't remove above comment line, it is a placeholder for code generator.
resourcemanager "github.com/pingcap/tidb-dashboard/pkg/apiserver/resource_manager"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/slowquery"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/statement"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
apiutils "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap/tidb-dashboard/pkg/config"
"github.com/pingcap/tidb-dashboard/pkg/dbstore"
"github.com/pingcap/tidb-dashboard/pkg/httpc"
"github.com/pingcap/tidb-dashboard/pkg/keyvisual"
keyvisualregion "github.com/pingcap/tidb-dashboard/pkg/keyvisual/region"
"github.com/pingcap/tidb-dashboard/pkg/pd"
"github.com/pingcap/tidb-dashboard/pkg/tidb"
"github.com/pingcap/tidb-dashboard/pkg/tikv"
"github.com/pingcap/tidb-dashboard/pkg/utils"
)
func Handler(s *Service) http.Handler {
return s.NewStatusAwareHandler(http.HandlerFunc(s.handler), s.stoppedHandler)
}
var once sync.Once
type Service struct {
app *fx.App
status *utils.ServiceStatus
ctx context.Context
cancel context.CancelFunc
config *config.Config
customKeyVisualProvider *keyvisualregion.DataProvider
stoppedHandler http.Handler
uiAssetFS http.FileSystem
apiHandlerEngine *gin.Engine
}
func NewService(cfg *config.Config, stoppedHandler http.Handler, uiAssetFS http.FileSystem, customKeyVisualProvider *keyvisualregion.DataProvider) *Service {
once.Do(func() {
// These global modification will be effective only for the first invoke.
_ = godotenv.Load()
gin.SetMode(gin.ReleaseMode)
})
return &Service{
status: utils.NewServiceStatus(),
config: cfg,
customKeyVisualProvider: customKeyVisualProvider,
stoppedHandler: stoppedHandler,
uiAssetFS: uiAssetFS,
}
}
func (s *Service) IsRunning() bool {
return s.status.IsRunning()
}
var Modules = fx.Options(
fx.Provide(
newAPIHandlerEngine,
newClients,
dbstore.NewDBStore,
httpc.NewHTTPClient,
pd.NewEtcdClient,
pd.NewPDClient,
tso.NewTSOClient,
scheduling.NewSchedulingClient,
config.NewDynamicConfigManager,
tidb.NewTiDBClient,
tikv.NewTiKVClient,
tiflash.NewTiFlashClient,
ticdc.NewTiCDCClient,
tiproxy.NewTiProxyClient,
utils.ProvideSysSchema,
apiutils.NewNgmProxy,
info.NewService,
clusterinfo.NewService,
logsearch.NewService,
diagnose.NewService,
keyvisual.NewService,
metrics.NewService,
queryeditor.NewService,
configuration.NewService,
// __APP_NAME__.NewService,
// NOTE: Don't remove above comment line, it is a placeholder for code generator
),
user.Module,
codeauth.Module,
sqlauth.Module,
ssoauth.Module,
code.Module,
sso.Module,
profiling.Module,
conprof.Module,
statement.Module,
slowquery.Module,
debugapi.Module,
topsql.Module,
visualplan.Module,
deadlock.Module,
resourcemanager.Module,
)
func (s *Service) Start(ctx context.Context) error {
if s.IsRunning() {
return nil
}
s.ctx, s.cancel = context.WithCancel(ctx)
s.app = fx.New(
fx.Logger(utils.NewFxPrinter()),
fx.Supply(featureflag.NewRegistry(s.config.FeatureVersion)),
Modules,
fx.Provide(
s.provideLocals,
),
fx.Populate(&s.apiHandlerEngine),
fx.Invoke(
info.RegisterRouter,
clusterinfo.RegisterRouter,
profiling.RegisterRouter,
logsearch.RegisterRouter,
diagnose.RegisterRouter,
keyvisual.RegisterRouter,
metrics.RegisterRouter,
queryeditor.RegisterRouter,
configuration.RegisterRouter,
// __APP_NAME__.RegisterRouter,
// NOTE: Don't remove above comment line, it is a placeholder for code generator
// Must be at the end
s.status.Register,
),
)
if err := s.app.Start(s.ctx); err != nil {
s.cleanAfterError()
return err
}
version.Print()
return nil
}
// TODO: Find a better place to put these client bundles.
func newClients(lc fx.Lifecycle, config *config.Config) (
dbClient *tidbclient.StatusClient,
kvClient *tikvclient.StatusClient,
csClient *tiflashclient.StatusClient,
pdClient *pdclient.APIClient,
ticdcClient *ticdcclient.StatusClient,
tiproxyClient *tiproxyclient.StatusClient,
tsoClient *tsoclient.StatusClient,
schedulingClient *schedulingclient.StatusClient,
) {
httpConfig := httpclient.Config{
TLSConfig: config.ClusterTLSConfig,
}
dbClient = tidbclient.NewStatusClient(httpConfig)
kvClient = tikvclient.NewStatusClient(httpConfig)
csClient = tiflashclient.NewStatusClient(httpConfig)
pdClient = pdclient.NewAPIClient(httpConfig)
ticdcClient = ticdcclient.NewStatusClient(httpConfig)
tiproxyClient = tiproxyclient.NewStatusClient(httpConfig)
tsoClient = tsoclient.NewStatusClient(httpConfig)
schedulingClient = schedulingclient.NewStatusClient(httpConfig)
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
dbClient.SetDefaultCtx(ctx)
kvClient.SetDefaultCtx(ctx)
csClient.SetDefaultCtx(ctx)
pdClient.SetDefaultCtx(ctx)
return nil
},
})
return
}
func (s *Service) cleanAfterError() {
s.cancel()
// drop
s.app = nil
s.apiHandlerEngine = nil
s.ctx = nil
s.cancel = nil
}
func (s *Service) Stop(ctx context.Context) error {
if !s.IsRunning() || s.app == nil {
return nil
}
s.cancel()
err := s.app.Stop(ctx)
// drop
s.app = nil
s.apiHandlerEngine = nil
s.ctx = nil
s.cancel = nil
return err
}
func (s *Service) NewStatusAwareHandler(handler http.Handler, stoppedHandler http.Handler) http.Handler {
return s.status.NewStatusAwareHandler(handler, stoppedHandler)
}
func (s *Service) handler(w http.ResponseWriter, r *http.Request) {
s.apiHandlerEngine.ServeHTTP(w, r)
}
func (s *Service) provideLocals() (*config.Config, http.FileSystem, *keyvisualregion.DataProvider) {
return s.config, s.uiAssetFS, s.customKeyVisualProvider
}
func newAPIHandlerEngine() (apiHandlerEngine *gin.Engine, endpoint *gin.RouterGroup) {
apiHandlerEngine = gin.New()
apiHandlerEngine.Use(gin.Recovery())
apiHandlerEngine.Use(cors.AllowAll())
apiHandlerEngine.Use(gzip.Gzip(gzip.DefaultCompression))
apiHandlerEngine.Use(rest.ErrorHandlerFn())
endpoint = apiHandlerEngine.Group("/dashboard/api")
return
}
var StoppedHandler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = io.WriteString(w, "Dashboard is not started.\n")
})
================================================
FILE: pkg/apiserver/clusterinfo/host.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package clusterinfo
import (
"sort"
"strings"
"github.com/pingcap/log"
"github.com/samber/lo"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/clusterinfo/hostinfo"
"github.com/pingcap/tidb-dashboard/pkg/utils/topology"
)
// fetchAllInstanceHosts fetches all hosts in the cluster and return in ascending order.
func (s *Service) fetchAllInstanceHosts() ([]string, error) {
allHostsMap := make(map[string]struct{})
pdInfo, err := topology.FetchPDTopology(s.params.PDClient)
if err != nil {
return nil, err
}
for _, i := range pdInfo {
allHostsMap[i.IP] = struct{}{}
}
tikvInfo, tiFlashInfo, err := topology.FetchStoreTopology(s.params.PDClient)
if err != nil {
return nil, err
}
for _, i := range tikvInfo {
allHostsMap[i.IP] = struct{}{}
}
for _, i := range tiFlashInfo {
allHostsMap[i.IP] = struct{}{}
}
tidbInfo, err := topology.FetchTiDBTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
return nil, err
}
for _, i := range tidbInfo {
allHostsMap[i.IP] = struct{}{}
}
ticdcInfo, err := topology.FetchTiCDCTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
return nil, err
}
for _, i := range ticdcInfo {
allHostsMap[i.IP] = struct{}{}
}
tiproxyInfo, err := topology.FetchTiProxyTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
return nil, err
}
for _, i := range tiproxyInfo {
allHostsMap[i.IP] = struct{}{}
}
tsoInfo, err := topology.FetchTSOTopology(s.lifecycleCtx, s.params.PDClient)
if err != nil {
if strings.Contains(err.Error(), "status code 404") {
tsoInfo = []topology.TSOInfo{}
} else {
return nil, err
}
}
for _, i := range tsoInfo {
allHostsMap[i.IP] = struct{}{}
}
schedulingInfo, err := topology.FetchSchedulingTopology(s.lifecycleCtx, s.params.PDClient)
if err != nil {
if strings.Contains(err.Error(), "status code 404") {
schedulingInfo = []topology.SchedulingInfo{}
} else {
return nil, err
}
}
for _, i := range schedulingInfo {
allHostsMap[i.IP] = struct{}{}
}
allHosts := lo.Keys(allHostsMap)
sort.Strings(allHosts)
return allHosts, nil
}
// fetchAllHostsInfo fetches all hosts and their information.
// Note: The returned data and error may both exist.
func (s *Service) fetchAllHostsInfo(db *gorm.DB) ([]*hostinfo.Info, error) {
allHosts, err := s.fetchAllInstanceHosts()
if err != nil {
return nil, err
}
allHostsInfoMap := make(map[string]*hostinfo.Info)
if e := hostinfo.FillFromClusterLoadTable(db, allHostsInfoMap); e != nil {
log.Warn("Failed to read cluster_load table", zap.Error(e))
err = e
}
if e := hostinfo.FillFromClusterHardwareTable(db, allHostsInfoMap); e != nil && err == nil {
log.Warn("Failed to read cluster_hardware table", zap.Error(e))
err = e
}
if e := hostinfo.FillInstances(db, allHostsInfoMap); e != nil && err == nil {
log.Warn("Failed to fill instances for hosts", zap.Error(e))
err = e
}
r := make([]*hostinfo.Info, 0, len(allHosts))
for _, host := range allHosts {
if im, ok := allHostsInfoMap[host]; ok {
r = append(r, im)
} else {
// Missing item
r = append(r, hostinfo.NewHostInfo(host))
}
}
return r, err
}
================================================
FILE: pkg/apiserver/clusterinfo/hostinfo/cluster_config.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package hostinfo
import (
"encoding/json"
"strings"
"gorm.io/gorm"
"github.com/pingcap/tidb-dashboard/util/netutil"
)
type clusterConfigModel struct {
Type string `gorm:"column:TYPE"`
Instance string `gorm:"column:INSTANCE"`
Key string `gorm:"column:KEY"`
Value string `gorm:"column:VALUE"`
}
func FillInstances(db *gorm.DB, m InfoMap) error {
var rows []clusterConfigModel
if err := db.
Table("INFORMATION_SCHEMA.CLUSTER_CONFIG").
Where("(`TYPE` = 'tidb' AND `KEY` = 'log.file.filename') " +
"OR (`TYPE` = 'tikv' AND `KEY` = 'storage.data-dir') " +
"OR (`TYPE` = 'pd' AND `KEY` = 'data-dir') " +
"OR (`TYPE` = 'ticdc' AND `KEY` = 'data-dir')" +
"OR (`TYPE` = 'tiflash' AND (`KEY` = 'engine-store.path' " +
" OR `KEY` = 'engine-store.storage.main.dir' " +
" OR `KEY` = 'engine-store.storage.latest.dir'))").
Find(&rows).Error; err != nil {
return err
}
for _, row := range rows {
hostname, _, err := netutil.ParseHostAndPortFromAddress(row.Instance)
if err != nil {
continue
}
if _, ok := m[hostname]; !ok {
m[hostname] = NewHostInfo(hostname)
}
switch row.Type {
case "tiflash":
if ins, ok := m[hostname].Instances[row.Instance]; ok {
if ins.Type == row.Type && ins.PartitionPathL != "" {
continue
}
} else {
m[hostname].Instances[row.Instance] = &InstanceInfo{
Type: row.Type,
PartitionPathL: "",
}
}
var paths []string
switch row.Key {
case "engine-store.path":
items := strings.Split(row.Value, ",")
for _, path := range items {
paths = append(paths, strings.TrimSpace(path))
}
case "engine-store.storage.main.dir", "engine-store.storage.latest.dir":
if err := json.Unmarshal([]byte(row.Value), &paths); err != nil {
return err
}
default:
paths = []string{row.Value}
}
for _, path := range paths {
mountDir := locateInstanceMountPartition(path, m[hostname].Partitions)
if mountDir != "" {
m[hostname].Instances[row.Instance].PartitionPathL = strings.ToLower(mountDir)
break
}
}
default:
m[hostname].Instances[row.Instance] = &InstanceInfo{
Type: row.Type,
PartitionPathL: strings.ToLower(locateInstanceMountPartition(row.Value, m[hostname].Partitions)),
}
}
}
return nil
}
// Try to discover which partition this instance is running on.
// If discover failed, empty string will be returned.
func locateInstanceMountPartition(directoryOrFilePath string, partitions map[string]*PartitionInfo) string {
if len(directoryOrFilePath) == 0 {
return ""
}
maxMatchLen := 0
maxMatchPath := ""
directoryOrFilePathL := strings.ToLower(directoryOrFilePath)
for _, info := range partitions {
// FIXME: This may cause wrong result in case sensitive FS.
if !strings.HasPrefix(directoryOrFilePathL, strings.ToLower(info.Path)) {
continue
}
if len(info.Path) > maxMatchLen {
maxMatchLen = len(info.Path)
maxMatchPath = info.Path
}
}
return maxMatchPath
}
================================================
FILE: pkg/apiserver/clusterinfo/hostinfo/cluster_hardware.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package hostinfo
import (
"bytes"
"encoding/json"
"strings"
"gorm.io/gorm"
"github.com/pingcap/tidb-dashboard/util/netutil"
)
// Used to deserialize from JSON_VALUE.
type clusterHardwareCPUInfoModel struct {
Arch string `json:"cpu-arch"`
LogicalCores int `json:"cpu-logical-cores,string"`
PhysicalCores int `json:"cpu-physical-cores,string"`
}
// Used to deserialize from JSON_VALUE.
type clusterHardwareDiskModel struct {
Path string `json:"path"`
FSType string `json:"fstype"`
Free int `json:"free,string"`
Total int `json:"total,string"`
}
func FillFromClusterHardwareTable(db *gorm.DB, m InfoMap) error {
var rows []clusterTableModel
var sqlQuery bytes.Buffer
if err := clusterTableQueryTemplate.Execute(&sqlQuery, map[string]string{
"tableName": "INFORMATION_SCHEMA.CLUSTER_HARDWARE",
}); err != nil {
panic(err)
}
if err := db.
Raw(sqlQuery.String(), []string{"cpu", "disk"}).
Scan(&rows).Error; err != nil {
return err
}
for _, row := range rows {
hostname, _, err := netutil.ParseHostAndPortFromAddress(row.Instance)
if err != nil {
continue
}
if _, ok := m[hostname]; !ok {
m[hostname] = NewHostInfo(hostname)
}
switch {
case row.DeviceType == "cpu" && row.DeviceName == "cpu":
if m[hostname].CPUInfo != nil {
continue
}
var v clusterHardwareCPUInfoModel
err := json.Unmarshal([]byte(row.JSONValue), &v)
if err != nil {
continue
}
m[hostname].CPUInfo = &CPUInfo{
Arch: v.Arch,
LogicalCores: v.LogicalCores,
PhysicalCores: v.PhysicalCores,
}
case row.DeviceType == "disk":
if m[hostname].PartitionProviderType != "" && m[hostname].PartitionProviderType != row.Type {
// Another instance on the same host has already provided disk information, skip.
continue
}
var v clusterHardwareDiskModel
err := json.Unmarshal([]byte(row.JSONValue), &v)
if err != nil {
continue
}
if m[hostname].PartitionProviderType == "" {
m[hostname].PartitionProviderType = row.Type
}
m[hostname].Partitions[strings.ToLower(v.Path)] = &PartitionInfo{
Path: v.Path,
FSType: v.FSType,
Free: v.Free,
Total: v.Total,
}
}
}
return nil
}
================================================
FILE: pkg/apiserver/clusterinfo/hostinfo/cluster_load.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package hostinfo
import (
"bytes"
"encoding/json"
"gorm.io/gorm"
"github.com/pingcap/tidb-dashboard/util/netutil"
)
// Used to deserialize from JSON_VALUE.
type clusterLoadCPUUsageModel struct {
Idle float64 `json:"idle,string"`
System float64 `json:"system,string"`
}
// Used to deserialize from JSON_VALUE.
type clusterLoadMemoryVirtualModel struct {
Used int `json:"used,string"`
Total int `json:"total,string"`
}
func FillFromClusterLoadTable(db *gorm.DB, m InfoMap) error {
var rows []clusterTableModel
var sqlQuery bytes.Buffer
if err := clusterTableQueryTemplate.Execute(&sqlQuery, map[string]string{
"tableName": "INFORMATION_SCHEMA.CLUSTER_LOAD",
}); err != nil {
panic(err)
}
if err := db.
Raw(sqlQuery.String(), []string{"memory", "cpu"}).
Scan(&rows).Error; err != nil {
return err
}
for _, row := range rows {
hostname, _, err := netutil.ParseHostAndPortFromAddress(row.Instance)
if err != nil {
continue
}
if _, ok := m[hostname]; !ok {
m[hostname] = NewHostInfo(hostname)
}
switch {
case row.DeviceType == "memory" && row.DeviceName == "virtual":
if m[hostname].MemoryUsage != nil {
continue
}
var v clusterLoadMemoryVirtualModel
err := json.Unmarshal([]byte(row.JSONValue), &v)
if err != nil {
continue
}
m[hostname].MemoryUsage = &MemoryUsageInfo{
Used: v.Used,
Total: v.Total,
}
case row.DeviceType == "cpu" && row.DeviceName == "usage":
if m[hostname].CPUUsage != nil {
continue
}
var v clusterLoadCPUUsageModel
err := json.Unmarshal([]byte(row.JSONValue), &v)
if err != nil {
continue
}
m[hostname].CPUUsage = &CPUUsageInfo{
Idle: v.Idle,
System: v.System,
}
}
}
return nil
}
================================================
FILE: pkg/apiserver/clusterinfo/hostinfo/hostinfo.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package hostinfo
import "text/template"
type CPUUsageInfo struct {
Idle float64 `json:"idle"`
System float64 `json:"system"`
}
type MemoryUsageInfo struct {
Used int `json:"used"`
Total int `json:"total"`
}
type CPUInfo struct {
Arch string `json:"arch"`
LogicalCores int `json:"logical_cores"`
PhysicalCores int `json:"physical_cores"`
}
type PartitionInfo struct {
Path string `json:"path"`
FSType string `json:"fstype"`
Free int `json:"free"`
Total int `json:"total"`
}
type InstanceInfo struct {
Type string `json:"type"`
PartitionPathL string `json:"partition_path_lower"`
}
type Info struct {
Host string `json:"host"`
CPUInfo *CPUInfo `json:"cpu_info"`
CPUUsage *CPUUsageInfo `json:"cpu_usage"`
MemoryUsage *MemoryUsageInfo `json:"memory_usage"`
// Containing unused partitions. The key is path in lower case.
// Note: deviceName is not used as the key, since TiDB and TiKV may return different deviceName for the same device.
Partitions map[string]*PartitionInfo `json:"partitions"`
// The source instance type that provides the partition info.
PartitionProviderType string `json:"-"`
// Instances in the current host. The key is instance address
Instances map[string]*InstanceInfo `json:"instances"`
}
type InfoMap = map[string]*Info
var clusterTableQueryTemplate = template.Must(template.New("").Parse(`
SELECT
*,
FIELD(LOWER(A.TYPE), 'tiflash', 'tikv', 'pd', 'tidb', 'tiproxy', 'tso', 'scheduling') AS _ORDER
FROM (
SELECT
TYPE, INSTANCE, DEVICE_TYPE, DEVICE_NAME, JSON_OBJECTAGG(NAME, VALUE) AS JSON_VALUE
FROM
{{.tableName}}
WHERE
DEVICE_TYPE IN (?)
GROUP BY TYPE, INSTANCE, DEVICE_TYPE, DEVICE_NAME
) AS A
ORDER BY
_ORDER DESC, INSTANCE, DEVICE_TYPE, DEVICE_NAME
`))
type clusterTableModel struct {
Type string `gorm:"column:TYPE"` // Example: tidb, tikv
Instance string `gorm:"column:INSTANCE"` // Example: 127.0.0.1:4000
DeviceType string `gorm:"column:DEVICE_TYPE"` // Example: cpu
DeviceName string `gorm:"column:DEVICE_NAME"` // Example: usage
JSONValue string `gorm:"column:JSON_VALUE"` // Only exists by using `clusterTableQueryTemplate`.
}
func NewHostInfo(hostname string) *Info {
return &Info{
Host: hostname,
Partitions: make(map[string]*PartitionInfo),
Instances: make(map[string]*InstanceInfo),
}
}
================================================
FILE: pkg/apiserver/clusterinfo/service.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
// clusterinfo is a directory for ClusterInfoServer, which could load topology from pd
// using Etcd v3 interface and pd interface.
package clusterinfo
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
clientv3 "go.etcd.io/etcd/client/v3"
"go.uber.org/fx"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/clusterinfo/hostinfo"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap/tidb-dashboard/pkg/httpc"
"github.com/pingcap/tidb-dashboard/pkg/pd"
"github.com/pingcap/tidb-dashboard/pkg/tidb"
"github.com/pingcap/tidb-dashboard/pkg/utils/topology"
"github.com/pingcap/tidb-dashboard/util/rest"
)
type ServiceParams struct {
fx.In
PDClient *pd.Client
EtcdClient *clientv3.Client
HTTPClient *httpc.Client
TiDBClient *tidb.Client
}
type Service struct {
params ServiceParams
lifecycleCtx context.Context
}
func NewService(lc fx.Lifecycle, p ServiceParams) *Service {
s := &Service{params: p}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
s.lifecycleCtx = ctx
return nil
},
})
return s
}
func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/topology")
endpoint.Use(auth.MWAuthRequired())
endpoint.GET("/tidb", s.getTiDBTopology)
endpoint.GET("/ticdc", s.getTiCDCTopology)
endpoint.GET("/tiproxy", s.getTiProxyTopology)
endpoint.DELETE("/tidb/:address", s.deleteTiDBTopology)
endpoint.GET("/store", s.getStoreTopology)
endpoint.GET("/pd", s.getPDTopology)
endpoint.GET("/tso", s.getTSOTopology)
endpoint.GET("/scheduling", s.getSchedulingTopology)
endpoint.GET("/alertmanager", s.getAlertManagerTopology)
endpoint.GET("/alertmanager/:address/count", s.getAlertManagerCounts)
endpoint.GET("/grafana", s.getGrafanaTopology)
endpoint.GET("/store_location", s.getStoreLocationTopology)
endpoint = r.Group("/host")
endpoint.Use(auth.MWAuthRequired())
endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient))
endpoint.GET("/all", s.getHostsInfo)
endpoint.GET("/statistics", s.getStatistics)
}
// @Summary Hide a TiDB instance
// @Param address path string true "ip:port"
// @Success 200 "delete ok"
// @Failure 401 {object} rest.ErrorResponse
// @Security JwtAuth
// @Router /topology/tidb/{address} [delete]
func (s *Service) deleteTiDBTopology(c *gin.Context) {
address := c.Param("address")
errorChannel := make(chan error, 2)
ttlKey := fmt.Sprintf("/topology/tidb/%v/ttl", address)
nonTTLKey := fmt.Sprintf("/topology/tidb/%v/info", address)
ctx, cancel := context.WithTimeout(s.lifecycleCtx, time.Second*5)
defer cancel()
var wg sync.WaitGroup
for _, key := range []string{ttlKey, nonTTLKey} {
wg.Add(1)
go func(toDel string) {
defer wg.Done()
if _, err := s.params.EtcdClient.Delete(ctx, toDel); err != nil {
errorChannel <- err
}
}(key)
}
wg.Wait()
var err error
select {
case err = <-errorChannel:
default:
}
close(errorChannel)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, nil)
}
// @ID getTiDBTopology
// @Summary Get all TiDB instances
// @Success 200 {array} topology.TiDBInfo
// @Router /topology/tidb [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getTiDBTopology(c *gin.Context) {
instances, err := topology.FetchTiDBTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, instances)
}
// @ID getTiCDCTopology
// @Summary Get all TiCDC instances
// @Success 200 {array} topology.TiCDCInfo
// @Router /topology/ticdc [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getTiCDCTopology(c *gin.Context) {
instances, err := topology.FetchTiCDCTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, instances)
}
// @ID getTiProxyTopology
// @Summary Get all TiProxy instances
// @Success 200 {array} topology.TiProxyInfo
// @Router /topology/tiproxy [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getTiProxyTopology(c *gin.Context) {
instances, err := topology.FetchTiProxyTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, instances)
}
// @ID getTSOTopology
// @Summary Get all TSO instances
// @Success 200 {array} topology.TSOInfo
// @Router /topology/tso [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getTSOTopology(c *gin.Context) {
instances, err := topology.FetchTSOTopology(s.lifecycleCtx, s.params.PDClient)
if err != nil {
// TODO: refine later
if strings.Contains(err.Error(), "status code 404") {
rest.Error(c, rest.ErrNotFound.Wrap(err, "api not found"))
} else {
rest.Error(c, err)
}
return
}
c.JSON(http.StatusOK, instances)
}
// @ID getSchedulingTopology
// @Summary Get all Scheduling instances
// @Success 200 {array} topology.SchedulingInfo
// @Router /topology/scheduling [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getSchedulingTopology(c *gin.Context) {
instances, err := topology.FetchSchedulingTopology(s.lifecycleCtx, s.params.PDClient)
if err != nil {
// TODO: refine later
if strings.Contains(err.Error(), "status code 404") {
rest.Error(c, rest.ErrNotFound.Wrap(err, "api not found"))
} else {
rest.Error(c, err)
}
return
}
c.JSON(http.StatusOK, instances)
}
type StoreTopologyResponse struct {
TiKV []topology.StoreInfo `json:"tikv"`
TiFlash []topology.StoreInfo `json:"tiflash"`
}
// @ID getStoreTopology
// @Summary Get all TiKV / TiFlash instances
// @Success 200 {object} StoreTopologyResponse
// @Router /topology/store [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getStoreTopology(c *gin.Context) {
tikvInstances, tiFlashInstances, err := topology.FetchStoreTopology(s.params.PDClient)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, StoreTopologyResponse{
TiKV: tikvInstances,
TiFlash: tiFlashInstances,
})
}
// @ID getStoreLocationTopology
// @Summary Get location labels of all TiKV / TiFlash instances
// @Success 200 {object} topology.StoreLocation
// @Router /topology/store_location [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getStoreLocationTopology(c *gin.Context) {
storeLocation, err := topology.FetchStoreLocation(s.params.PDClient)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, storeLocation)
}
// @ID getPDTopology
// @Summary Get all PD instances
// @Success 200 {array} topology.PDInfo
// @Router /topology/pd [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getPDTopology(c *gin.Context) {
instances, err := topology.FetchPDTopology(s.params.PDClient)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, instances)
}
// @ID getAlertManagerTopology
// @Summary Get AlertManager instance
// @Success 200 {object} topology.AlertManagerInfo
// @Router /topology/alertmanager [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getAlertManagerTopology(c *gin.Context) {
instance, err := topology.FetchAlertManagerTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, instance)
}
// @ID getGrafanaTopology
// @Summary Get Grafana instance
// @Success 200 {object} topology.GrafanaInfo
// @Router /topology/grafana [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getGrafanaTopology(c *gin.Context) {
instance, err := topology.FetchGrafanaTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, instance)
}
// @ID getAlertManagerCounts
// @Summary Get current alert count from AlertManager
// @Success 200 {object} int
// @Param address path string true "ip:port"
// @Router /topology/alertmanager/{address}/count [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getAlertManagerCounts(c *gin.Context) {
address := c.Param("address")
if address == "" {
rest.Error(c, rest.ErrBadRequest.New("address is empty"))
return
}
info, err := topology.FetchAlertManagerTopology(c.Request.Context(), s.params.EtcdClient)
if err != nil {
rest.Error(c, err)
return
}
if info == nil {
rest.Error(c, rest.ErrBadRequest.New("alertmanager not found"))
return
}
if address != fmt.Sprintf("%s:%d", info.IP, info.Port) {
rest.Error(c, rest.ErrBadRequest.New("address not match"))
return
}
cnt, err := fetchAlertManagerCounts(s.lifecycleCtx, address, s.params.HTTPClient)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, cnt)
}
type GetHostsInfoResponse struct {
Hosts []*hostinfo.Info `json:"hosts"`
Warning rest.ErrorResponse `json:"warning"`
}
// @ID clusterInfoGetHostsInfo
// @Summary Get information of all hosts
// @Router /host/all [get]
// @Security JwtAuth
// @Success 200 {object} GetHostsInfoResponse
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getHostsInfo(c *gin.Context) {
db := utils.GetTiDBConnection(c)
info, err := s.fetchAllHostsInfo(db)
if err != nil && info == nil {
rest.Error(c, err)
return
}
var warning rest.ErrorResponse
if err != nil {
warning = rest.NewErrorResponse(err)
}
c.JSON(http.StatusOK, GetHostsInfoResponse{
Hosts: info,
Warning: warning,
})
}
// @ID clusterInfoGetStatistics
// @Summary Get cluster statistics
// @Router /host/statistics [get]
// @Security JwtAuth
// @Success 200 {object} ClusterStatistics
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getStatistics(c *gin.Context) {
db := utils.GetTiDBConnection(c)
stats, err := s.calculateStatistics(db)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, stats)
}
================================================
FILE: pkg/apiserver/clusterinfo/statistics.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package clusterinfo
import (
"net"
"sort"
"strconv"
"strings"
"github.com/samber/lo"
"gorm.io/gorm"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/clusterinfo/hostinfo"
"github.com/pingcap/tidb-dashboard/pkg/utils/topology"
)
type ClusterStatisticsPartial struct {
NumberOfHosts int `json:"number_of_hosts"`
NumberOfInstances int `json:"number_of_instances"`
TotalMemoryCapacityBytes int `json:"total_memory_capacity_bytes"`
TotalPhysicalCores int `json:"total_physical_cores"`
TotalLogicalCores int `json:"total_logical_cores"`
}
type ClusterStatistics struct {
ProbeFailureHosts int `json:"probe_failure_hosts"`
Versions []string `json:"versions"`
TotalStats *ClusterStatisticsPartial `json:"total_stats"`
StatsByInstanceKind map[string]*ClusterStatisticsPartial `json:"stats_by_instance_kind"`
}
type instanceKindHostImmediateInfo struct {
memoryCapacity int
physicalCores int
logicalCores int
}
type instanceKindImmediateInfo struct {
instances map[string]struct{}
hosts map[string]*instanceKindHostImmediateInfo
}
func newInstanceKindImmediateInfo() *instanceKindImmediateInfo {
return &instanceKindImmediateInfo{
instances: make(map[string]struct{}),
hosts: make(map[string]*instanceKindHostImmediateInfo),
}
}
func sumInt(array []int) int {
result := 0
for _, v := range array {
result += v
}
return result
}
func (info *instanceKindImmediateInfo) ToResult() *ClusterStatisticsPartial {
return &ClusterStatisticsPartial{
NumberOfHosts: len(lo.Keys(info.hosts)),
NumberOfInstances: len(lo.Keys(info.instances)),
TotalMemoryCapacityBytes: sumInt(lo.Map(lo.Values(info.hosts), func(x *instanceKindHostImmediateInfo, _ int) int { return x.memoryCapacity })),
TotalPhysicalCores: sumInt(lo.Map(lo.Values(info.hosts), func(x *instanceKindHostImmediateInfo, _ int) int { return x.physicalCores })),
TotalLogicalCores: sumInt(lo.Map(lo.Values(info.hosts), func(x *instanceKindHostImmediateInfo, _ int) int { return x.logicalCores })),
}
}
func (s *Service) calculateStatistics(db *gorm.DB) (*ClusterStatistics, error) {
globalHostsSet := make(map[string]struct{})
globalFailureHostsSet := make(map[string]struct{})
globalVersionsSet := make(map[string]struct{})
globalInfo := newInstanceKindImmediateInfo()
infoByIk := make(map[string]*instanceKindImmediateInfo)
infoByIk["pd"] = newInstanceKindImmediateInfo()
infoByIk["tidb"] = newInstanceKindImmediateInfo()
infoByIk["tikv"] = newInstanceKindImmediateInfo()
infoByIk["tiflash"] = newInstanceKindImmediateInfo()
infoByIk["ticdc"] = newInstanceKindImmediateInfo()
infoByIk["tiproxy"] = newInstanceKindImmediateInfo()
infoByIk["tso"] = newInstanceKindImmediateInfo()
infoByIk["scheduling"] = newInstanceKindImmediateInfo()
// Fill from topology info
pdInfo, err := topology.FetchPDTopology(s.params.PDClient)
if err != nil {
return nil, err
}
for _, i := range pdInfo {
globalHostsSet[i.IP] = struct{}{}
globalVersionsSet[i.Version] = struct{}{}
globalInfo.instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
infoByIk["pd"].instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
}
tikvInfo, tiFlashInfo, err := topology.FetchStoreTopology(s.params.PDClient)
if err != nil {
return nil, err
}
for _, i := range tikvInfo {
globalHostsSet[i.IP] = struct{}{}
globalVersionsSet[i.Version] = struct{}{}
globalInfo.instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
infoByIk["tikv"].instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
}
for _, i := range tiFlashInfo {
globalHostsSet[i.IP] = struct{}{}
globalVersionsSet[i.Version] = struct{}{}
globalInfo.instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
infoByIk["tiflash"].instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
}
tidbInfo, err := topology.FetchTiDBTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
return nil, err
}
for _, i := range tidbInfo {
globalHostsSet[i.IP] = struct{}{}
globalVersionsSet[i.Version] = struct{}{}
globalInfo.instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
infoByIk["tidb"].instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
}
ticdcInfo, err := topology.FetchTiCDCTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
return nil, err
}
for _, i := range ticdcInfo {
globalHostsSet[i.IP] = struct{}{}
globalVersionsSet[i.Version] = struct{}{}
globalInfo.instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
infoByIk["ticdc"].instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
}
tiproxyInfo, err := topology.FetchTiProxyTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
return nil, err
}
for _, i := range tiproxyInfo {
globalHostsSet[i.IP] = struct{}{}
globalVersionsSet[i.Version] = struct{}{}
globalInfo.instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
infoByIk["tiproxy"].instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
}
tsoInfo, err := topology.FetchTSOTopology(s.lifecycleCtx, s.params.PDClient)
if err != nil {
if strings.Contains(err.Error(), "status code 404") {
tsoInfo = []topology.TSOInfo{}
} else {
return nil, err
}
}
for _, i := range tsoInfo {
globalHostsSet[i.IP] = struct{}{}
globalVersionsSet[i.Version] = struct{}{}
globalInfo.instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
infoByIk["tso"].instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
}
schedulingInfo, err := topology.FetchSchedulingTopology(s.lifecycleCtx, s.params.PDClient)
if err != nil {
if strings.Contains(err.Error(), "status code 404") {
schedulingInfo = []topology.SchedulingInfo{}
} else {
return nil, err
}
}
for _, i := range schedulingInfo {
globalHostsSet[i.IP] = struct{}{}
globalVersionsSet[i.Version] = struct{}{}
globalInfo.instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
infoByIk["scheduling"].instances[net.JoinHostPort(i.IP, strconv.Itoa(int(i.Port)))] = struct{}{}
}
// Fill from hardware info
allHostsInfoMap := make(map[string]*hostinfo.Info)
if e := hostinfo.FillFromClusterLoadTable(db, allHostsInfoMap); e != nil {
return nil, err
}
if e := hostinfo.FillFromClusterHardwareTable(db, allHostsInfoMap); e != nil {
return nil, err
}
for host, hi := range allHostsInfoMap {
if hi.MemoryUsage.Total > 0 && hi.CPUInfo.PhysicalCores > 0 && hi.CPUInfo.LogicalCores > 0 {
// Put success host info into `globalInfo.hosts`.
globalInfo.hosts[host] = &instanceKindHostImmediateInfo{
memoryCapacity: hi.MemoryUsage.Total,
physicalCores: hi.CPUInfo.PhysicalCores,
logicalCores: hi.CPUInfo.LogicalCores,
}
}
}
// Fill hosts in each instance kind according to the global hosts info
for _, i := range pdInfo {
if v, ok := globalInfo.hosts[i.IP]; ok {
infoByIk["pd"].hosts[i.IP] = v
} else {
globalFailureHostsSet[i.IP] = struct{}{}
}
}
for _, i := range tikvInfo {
if v, ok := globalInfo.hosts[i.IP]; ok {
infoByIk["tikv"].hosts[i.IP] = v
} else {
globalFailureHostsSet[i.IP] = struct{}{}
}
}
for _, i := range tiFlashInfo {
if v, ok := globalInfo.hosts[i.IP]; ok {
infoByIk["tiflash"].hosts[i.IP] = v
} else {
globalFailureHostsSet[i.IP] = struct{}{}
}
}
for _, i := range tidbInfo {
if v, ok := globalInfo.hosts[i.IP]; ok {
infoByIk["tidb"].hosts[i.IP] = v
} else {
globalFailureHostsSet[i.IP] = struct{}{}
}
}
for _, i := range ticdcInfo {
if v, ok := globalInfo.hosts[i.IP]; ok {
infoByIk["ticdc"].hosts[i.IP] = v
} else {
globalFailureHostsSet[i.IP] = struct{}{}
}
}
for _, i := range tiproxyInfo {
if v, ok := globalInfo.hosts[i.IP]; ok {
infoByIk["tiproxy"].hosts[i.IP] = v
} else {
globalFailureHostsSet[i.IP] = struct{}{}
}
}
for _, i := range tsoInfo {
if v, ok := globalInfo.hosts[i.IP]; ok {
infoByIk["tso"].hosts[i.IP] = v
} else {
globalFailureHostsSet[i.IP] = struct{}{}
}
}
for _, i := range schedulingInfo {
if v, ok := globalInfo.hosts[i.IP]; ok {
infoByIk["scheduling"].hosts[i.IP] = v
} else {
globalFailureHostsSet[i.IP] = struct{}{}
}
}
// Generate result..
versions := lo.Keys(globalVersionsSet)
sort.Strings(versions)
statsByIk := make(map[string]*ClusterStatisticsPartial)
for ik, info := range infoByIk {
statsByIk[ik] = info.ToResult()
}
return &ClusterStatistics{
ProbeFailureHosts: len(lo.Keys(globalFailureHostsSet)),
Versions: versions,
TotalStats: globalInfo.ToResult(),
StatsByInstanceKind: statsByIk,
}, nil
}
================================================
FILE: pkg/apiserver/clusterinfo/topology.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package clusterinfo
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/pingcap/tidb-dashboard/pkg/httpc"
)
func fetchAlertManagerCounts(ctx context.Context, alertManagerAddr string, httpClient *httpc.Client) (int, error) {
// FIXME: Use httpClient.SendGetRequest
uri := fmt.Sprintf("http://%s/api/v2/alerts", alertManagerAddr)
req, err := http.NewRequestWithContext(ctx, "GET", uri, nil)
if err != nil {
return 0, err
}
resp, err := httpClient.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("alert manager API returns non success status code")
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return 0, err
}
var alerts []struct{}
err = json.Unmarshal(data, &alerts)
if err != nil {
return 0, err
}
return len(alerts), nil
}
================================================
FILE: pkg/apiserver/configuration/editable.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package configuration
import "strings"
// Hard coded items comes from https://docs.pingcap.com/tidb/stable/dynamic-config
var editableConfigItemsRaw = map[ItemKind]string{
ItemKindTiKVConfig: `
raftstore.sync-log
raftstore.raft-entry-max-size
raftstore.raft-log-gc-tick-interval
raftstore.raft-log-gc-threshold
raftstore.raft-log-gc-count-limit
raftstore.raft-log-gc-size-limit
raftstore.raft-entry-cache-life-time
raftstore.raft-reject-transfer-leader-duration
raftstore.split-region-check-tick-interval
raftstore.region-split-check-diff
raftstore.region-compact-check-interval
raftstore.region-compact-check-step
raftstore.region-compact-min-tombstones
raftstore.region-compact-tombstones-percent
raftstore.pd-heartbeat-tick-interval
raftstore.pd-store-heartbeat-tick-interval
raftstore.snap-mgr-gc-tick-interval
raftstore.snap-gc-timeout
raftstore.lock-cf-compact-interval
raftstore.lock-cf-compact-bytes-threshold
raftstore.messages-per-tick
raftstore.max-peer-down-duration
raftstore.max-leader-missing-duration
raftstore.abnormal-leader-missing-duration
raftstore.peer-stale-state-check-interval
raftstore.consistency-check-interval
raftstore.raft-store-max-leader-lease
raftstore.allow-remove-leader
raftstore.merge-check-tick-interval
raftstore.cleanup-import-sst-interval
raftstore.local-read-batch-size
raftstore.hibernate-timeout
coprocessor.split-region-on-table
coprocessor.batch-split-limit
coprocessor.region-max-size
coprocessor.region-split-size
coprocessor.region-max-keys
coprocessor.region-split-keys
pessimistic-txn.wait-for-lock-timeout
pessimistic-txn.wake-up-delay-duration
pessimistic-txn.pipelined
gc.ratio-threshold
gc.batch-keys
gc.max-write-bytes-per-sec
gc.enable-compaction-filter
gc.compaction-filter-skip-version-check
raftdb.defaultcf.block-cache-size
raftdb.defaultcf.write-buffer-size
raftdb.defaultcf.max-write-buffer-number
raftdb.defaultcf.max-bytes-for-level-base
raftdb.defaultcf.target-file-size-base
raftdb.defaultcf.level0-file-num-compaction-trigger
raftdb.defaultcf.level0-slowdown-writes-trigger
raftdb.defaultcf.level0-stop-writes-trigger
raftdb.defaultcf.max-compaction-bytes
raftdb.defaultcf.max-bytes-for-level-multiplier
raftdb.defaultcf.disable-auto-compactions
raftdb.defaultcf.soft-pending-compaction-bytes-limit
raftdb.defaultcf.hard-pending-compaction-bytes-limit
raftdb.defaultcf.titan.blob-run-mode
rocksdb.max-total-wal-size
rocksdb.max-background-jobs
rocksdb.max-open-files
rocksdb.compaction-readahead-size
rocksdb.bytes-per-sync
rocksdb.wal-bytes-per-sync
rocksdb.writable-file-max-buffer-size
rocksdb.raftcf.block-cache-size
rocksdb.raftcf.write-buffer-size
rocksdb.raftcf.max-write-buffer-number
rocksdb.raftcf.max-bytes-for-level-base
rocksdb.raftcf.target-file-size-base
rocksdb.raftcf.level0-file-num-compaction-trigger
rocksdb.raftcf.level0-slowdown-writes-trigger
rocksdb.raftcf.level0-stop-writes-trigger
rocksdb.raftcf.max-compaction-bytes
rocksdb.raftcf.max-bytes-for-level-multiplier
rocksdb.raftcf.disable-auto-compactions
rocksdb.raftcf.soft-pending-compaction-bytes-limit
rocksdb.raftcf.hard-pending-compaction-bytes-limit
rocksdb.raftcf.titan.blob-run-mode
rocksdb.defaultcf.block-cache-size
rocksdb.defaultcf.write-buffer-size
rocksdb.defaultcf.max-write-buffer-number
rocksdb.defaultcf.max-bytes-for-level-base
rocksdb.defaultcf.target-file-size-base
rocksdb.defaultcf.level0-file-num-compaction-trigger
rocksdb.defaultcf.level0-slowdown-writes-trigger
rocksdb.defaultcf.level0-stop-writes-trigger
rocksdb.defaultcf.max-compaction-bytes
rocksdb.defaultcf.max-bytes-for-level-multiplier
rocksdb.defaultcf.disable-auto-compactions
rocksdb.defaultcf.soft-pending-compaction-bytes-limit
rocksdb.defaultcf.hard-pending-compaction-bytes-limit
rocksdb.defaultcf.titan.blob-run-mode
rocksdb.lockcf.block-cache-size
rocksdb.lockcf.write-buffer-size
rocksdb.lockcf.max-write-buffer-number
rocksdb.lockcf.max-bytes-for-level-base
rocksdb.lockcf.target-file-size-base
rocksdb.lockcf.level0-file-num-compaction-trigger
rocksdb.lockcf.level0-slowdown-writes-trigger
rocksdb.lockcf.level0-stop-writes-trigger
rocksdb.lockcf.max-compaction-bytes
rocksdb.lockcf.max-bytes-for-level-multiplier
rocksdb.lockcf.disable-auto-compactions
rocksdb.lockcf.soft-pending-compaction-bytes-limit
rocksdb.lockcf.hard-pending-compaction-bytes-limit
rocksdb.lockcf.titan.blob-run-mode
storage.block-cache.capacity
backup.num-threads
split.qps-threshold
split.split-balance-score
split.split-contained-score
`,
ItemKindPDConfig: `
log.level
cluster-version
schedule.max-merge-region-size
schedule.max-merge-region-keys
schedule.patrol-region-interval
schedule.split-merge-interval
schedule.max-snapshot-count
schedule.max-pending-peer-count
schedule.max-store-down-time
schedule.leader-schedule-policy
schedule.leader-schedule-limit
schedule.region-schedule-limit
schedule.replica-schedule-limit
schedule.merge-schedule-limit
schedule.hot-region-schedule-limit
schedule.hot-region-cache-hits-threshold
schedule.high-space-ratio
schedule.low-space-ratio
schedule.tolerant-size-ratio
schedule.enable-remove-down-replica
schedule.enable-replace-offline-replica
schedule.enable-make-up-replica
schedule.enable-remove-extra-replica
schedule.enable-location-replacement
schedule.enable-cross-table-merge
schedule.enable-one-way-merge
replication.max-replicas
replication.location-labels
replication.enable-placement-rules
replication.strictly-match-label
pd-server.use-region-storage
pd-server.max-gap-reset-ts
pd-server.key-type
pd-server.metric-storage
pd-server.dashboard-address
replication-mode.replication-mode
`,
// Mark all global variables in TiDB being editable.
// Due to https://github.com/pingcap/tidb/issues/18517 we have to hard code all global variables for now.
// TODO: We'd better provide a Editable system variable table as well.
ItemKindTiDBVariable: `
gtid_mode
flush_time
low_priority_updates
session_track_gtids
ndbinfo_max_rows
ndb_index_stat_option
old_passwords
max_connections
big_tables
slave_pending_jobs_size_max
validate_password_check_user_name
validate_password_number_count
sql_select_limit
ndb_show_foreign_key_mock_tables
default_week_format
binlog_error_action
slave_transaction_retries
default_storage_engine
max_connect_errors
sync_binlog
innodb_fast_shutdown
log_backward_compatible_user_definitions
ft_boolean_syntax
table_definition_cache
sql_mode
server_id
innodb_flushing_avg_loops
tmp_table_size
innodb_max_purge_lag
preload_buffer_size
slave_checkpoint_period
check_proxy_users
innodb_flush_log_at_timeout
innodb_max_undo_log_size
range_alloc_block_size
connect_timeout
max_execution_time
collation_server
innodb_old_blocks_pct
innodb_file_format
innodb_compression_failure_threshold_pct
innodb_checksum_algorithm
relay_log_info_repository
sql_log_bin
super_read_only
max_delayed_threads
new
myisam_sort_buffer_size
optimizer_trace_offset
innodb_buffer_pool_dump_at_shutdown
sql_notes
innodb_cmp_per_index_enabled
innodb_ft_server_stopword_table
binlog_group_commit_sync_delay
binlog_group_commit_sync_no_delay_count
innodb_log_write_ahead_size
general_log
validate_password_dictionary_file
binlog_order_commits
master_verify_checksum
key_cache_division_limit
rpl_semi_sync_master_trace_level
max_insert_delayed_threads
time_zone
innodb_max_dirty_pages_pct
innodb_file_per_table
innodb_log_compressed_pages
master_info_repository
rpl_stop_slave_timeout
innodb_monitor_reset
innodb_print_all_deadlocks
slave_net_timeout
key_buffer_size
foreign_key_checks
host_cache_size
delay_key_write
innodb_file_format_max
debug
log_warnings
offline_mode
innodb_strict_mode
innodb_rollback_segments
join_buffer_size
max_binlog_size
sync_master_info
concurrent_insert
innodb_adaptive_hash_index
innodb_ft_enable_stopword
general_log_file
innodb_support_xa
innodb_compression_level
init_slave
block_encryption_mode
max_length_for_sort_data
interactive_timeout
innodb_optimize_fulltext_only
query_cache_type
query_alloc_block_size
slave_compressed_protocol
init_connect
rpl_semi_sync_slave_trace_level
query_prealloc_size
max_user_connections
innodb_api_trx_level
expire_logs_days
binlog_rows_query_log_events
default_password_lifetime
innodb_status_output_locks
max_error_count
max_write_lock_count
innodb_stats_persistent_sample_pages
show_compatibility_56
log_slow_slave_statements
innodb_spin_wait_delay
thread_cache_size
log_slow_admin_statements
auto_increment_offset
innodb_max_dirty_pages_pct_lwm
log_queries_not_using_indexes
query_cache_wlock_invalidate
sql_buffer_result
character_set_filesystem
collation_database
auto_increment_increment
auto_increment_offset
max_heap_table_size
div_precision_increment
innodb_lru_scan_depth
innodb_purge_rseg_truncate_frequency
sql_auto_is_null
innodb_ft_user_stopword_table
innodb_log_checksum_algorithm
sort_buffer_size
innodb_flush_neighbors
innodb_purge_batch_size
slave_checkpoint_group
character_set_client
innodb_buffer_pool_dump_now
relay_log_purge
ndb_distribution
myisam_data_pointer_size
ndb_optimization_delay
innodb_ft_num_word_optimize
max_join_size
max_seeks_for_key
delayed_insert_timeout
max_relay_log_size
max_sort_length
ndb_eventbuffer_free_percent
binlog_max_flush_queue_time
innodb_fill_factor
log_syslog_facility
transaction_write_set_extraction
ndb_blob_write_batch_bytes
automatic_sp_privileges
innodb_flush_sync
innodb_monitor_disable
slave_parallel_type
innodb_adaptive_flushing_lwm
innodb_buffer_pool_load_now
profiling
sha256_password_proxy_users
sql_quote_show_create
binlogging_impossible_mode
query_cache_size
innodb_stats_transient_sample_pages
innodb_stats_on_metadata
ndb_force_send
log_timestamps
slave_parallel_workers
event_scheduler
ndb_deferred_constraints
log_syslog_include_pid
innodb_disable_sort_file_cache
log_error_verbosity
innodb_replication_delay
slow_query_log
innodb_stats_auto_recalc
lc_messages
bulk_insert_buffer_size
binlog_direct_non_transactional_updates
innodb_change_buffering
sql_big_selects
character_set_results
innodb_max_purge_lag_delay
session_track_schema
innodb_io_capacity_max
innodb_autoextend_increment
binlog_format
optimizer_trace
read_rnd_buffer_size
net_write_timeout
innodb_buffer_pool_load_abort
tx_isolation
transaction_isolation
collation_connection
rpl_semi_sync_master_timeout
transaction_prealloc_size
sync_relay_log
innodb_ft_result_cache_limit
innodb_ft_enable_diag_print
stored_program_cache
innodb_adaptive_max_sleep_delay
session_track_system_variables
innodb_change_buffer_max_size
log_bin_trust_function_creators
mysql_native_password_proxy_users
read_only
innodb_stats_persistent
session_track_state_change
delayed_queue_size
log_syslog
transaction_alloc_block_size
sql_slave_skip_counter
innodb_large_prefix
innodb_io_capacity
max_binlog_cache_size
ndb_index_stat_enable
executed_gtids_compression_period
old_alter_table
long_query_time
log_throttle_queries_not_using_indexes
binlog_cache_size
innodb_compression_pad_pct_max
innodb_commit_concurrency
enforce_gtid_consistency
secure_auth
innodb_random_read_ahead
unique_checks
internal_tmp_disk_storage_engine
myisam_repair_threads
ndb_eventbuffer_max_alloc
innodb_read_ahead_threshold
key_cache_block_size
rpl_semi_sync_slave_enabled
gtid_purged
max_binlog_stmt_cache_size
lock_wait_timeout
read_buffer_size
max_sp_recursion_depth
rpl_semi_sync_master_enabled
slow_query_log_file
innodb_thread_sleep_delay
innodb_ft_aux_table
sql_warnings
keep_files_on_create
slave_preserve_commit_order
slave_exec_mode
binlog_stmt_cache_size
table_open_cache
autocommit
default_tmp_storage_engine
optimizer_search_depth
max_points_in_geometry
innodb_stats_sample_pages
profiling_history_size
character_set_database
storage_engine
sql_log_off
log_syslog_tag
tx_read_only
transaction_read_only
rpl_semi_sync_master_wait_point
innodb_undo_log_truncate
gtid_executed_compression_period
ndb_log_empty_epochs
max_prepared_stmt_count
optimizer_trace_max_mem_size
net_retry_count
optimizer_trace_features
innodb_flush_log_at_trx_commit
rewriter_enabled
query_cache_min_res_unit
updatable_views_with_limit
optimizer_prune_level
slave_sql_verify_checksum
completion_type
binlog_checksum
show_old_temporals
query_cache_limit
innodb_buffer_pool_size
innodb_adaptive_flushing
wait_timeout
innodb_monitor_enable
innodb_buffer_pool_filename
slow_launch_time
slave_max_allowed_packet
ndb_use_transactions
innodb_concurrency_tickets
innodb_monitor_reset_all
ndb_log_updated_only
innodb_old_blocks_time
innodb_stats_method
innodb_lock_wait_timeout
local_infile
myisam_stats_method
innodb_table_locks
net_buffer_length
rpl_semi_sync_master_wait_for_slave_count
binlog_row_image
myisam_max_sort_file_size
rpl_semi_sync_master_wait_no_slave
group_concat_max_len
rewriter_verbose
innodb_undo_logs
delayed_insert_limit
flush
eq_range_index_dive_limit
character_set_connection
myisam_use_mmap
ndb_join_pushdown
character_set_server
validate_password_special_char_count
slave_rows_search_algorithms
ndbinfo_show_hidden
net_read_timeout
max_allowed_packet
sync_relay_log_info
optimizer_trace_limit
validate_password_length
ndb_log_binlog_index
innodb_api_bk_commit_interval
innodb_sync_spin_loops
sql_safe_updates
innodb_thread_concurrency
slave_allow_batching
innodb_buffer_pool_dump_pct
lc_time_names
max_statement_time
end_markers_in_json
avoid_temporal_upgrade
key_cache_age_threshold
innodb_status_output
min_examined_row_limit
sync_frm
innodb_online_alter_log_max_size
information_schema_stats_expiry
thread_pool_size
windowing_use_high_precision
tidb_opt_broadcast_join
tidb_build_stats_concurrency
tidb_auto_analyze_ratio
tidb_auto_analyze_start_time
tidb_auto_analyze_end_time
tidb_executor_concurrency
tidb_distsql_scan_concurrency
tidb_opt_insubq_to_join_and_agg
tidb_opt_correlation_threshold
tidb_opt_correlation_exp_factor
tidb_opt_cpu_factor
tidb_opt_tiflash_concurrency_factor
tidb_opt_copcpu_factor
tidb_opt_network_factor
tidb_opt_scan_factor
tidb_opt_desc_factor
tidb_opt_seek_factor
tidb_opt_memory_factor
tidb_opt_disk_factor
tidb_opt_concurrency_factor
tidb_index_join_batch_size
tidb_index_lookup_size
tidb_index_lookup_concurrency
tidb_index_lookup_join_concurrency
tidb_index_serial_scan_concurrency
tidb_skip_utf8_check
tidb_skip_ascii_check
tidb_max_chunk_size
tidb_allow_batch_cop
tidb_init_chunk_size
tidb_enable_cascades_planner
tidb_enable_index_merge
tidb_enable_table_partition
tidb_hash_join_concurrency
tidb_projection_concurrency
tidb_hashagg_partial_concurrency
tidb_hashagg_final_concurrency
tidb_window_concurrency
tidb_enable_parallel_apply
tidb_backoff_lock_fast
tidb_backoff_weight
tidb_retry_limit
tidb_disable_txn_auto_retry
tidb_constraint_check_in_place
tidb_txn_mode
tidb_row_format_version
tidb_enable_window_function
tidb_enable_vectorized_expression
tidb_enable_fast_analyze
tidb_skip_isolation_level_check
tidb_ddl_reorg_worker_cnt
tidb_ddl_reorg_batch_size
tidb_ddl_error_count_limit
tidb_max_delta_schema_count
tidb_opt_join_reorder_threshold
tidb_scatter_region
tidb_enable_noop_functions
tidb_enable_stmt_summary
tidb_stmt_summary_internal_query
tidb_stmt_summary_refresh_interval
tidb_stmt_summary_history_size
tidb_stmt_summary_max_stmt_count
tidb_stmt_summary_max_sql_length
tidb_capture_plan_baselines
tidb_use_plan_baselines
tidb_evolve_plan_baselines
tidb_evolve_plan_task_max_time
tidb_evolve_plan_task_start_time
tidb_evolve_plan_task_end_time
tidb_store_limit
allow_auto_random_explicit_insert
tidb_enable_clustered_index
tidb_slow_log_masking
tidb_log_desensitization
tidb_shard_allocate_step
tidb_enable_telemetry
`,
}
var editableConfigItems = map[ItemKind]map[string]struct{}{}
func init() {
for kind, str := range editableConfigItemsRaw {
editableConfigItems[kind] = make(map[string]struct{})
configItems := strings.SplitSeq(strings.TrimSpace(str), "\n")
for key := range configItems {
editableConfigItems[kind][key] = struct{}{}
}
}
}
func isConfigItemEditable(kind ItemKind, key string) bool {
if _, ok := editableConfigItems[kind]; !ok {
return false
}
if _, ok := editableConfigItems[kind][key]; !ok {
return false
}
return true
}
================================================
FILE: pkg/apiserver/configuration/flatten.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package configuration
import (
"encoding/json"
"github.com/pingcap/log"
"go.uber.org/zap"
)
func flattenRecursive(nestedConfig map[string]interface{}) map[string]interface{} {
flatMap := make(map[string]interface{})
flatten(flatMap, nestedConfig, "")
return flatMap
}
func flatten(flatMap map[string]interface{}, nested interface{}, prefix string) {
switch n := nested.(type) {
case map[string]interface{}:
for k, v := range n {
path := k
if prefix != "" {
path = prefix + "." + k
}
flatten(flatMap, v, path)
}
case []interface{}:
// For array, serialize as json string directly
j, err := json.Marshal(n)
if err != nil {
log.Warn("Failed to serialize config value", zap.Any("value", n), zap.Error(err))
flatMap[prefix] = nil
} else {
flatMap[prefix] = string(j)
}
case nil:
flatMap[prefix] = ""
default: // don't flatten arrays
flatMap[prefix] = nested
}
}
================================================
FILE: pkg/apiserver/configuration/router.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package configuration
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap/tidb-dashboard/util/rest"
)
func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/configuration")
endpoint.Use(auth.MWAuthRequired())
endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient))
endpoint.Use(utils.MWForbidByExperimentalFlag(s.params.Config.EnableExperimental))
endpoint.GET("/all", s.getHandler)
endpoint.POST("/edit", auth.MWRequireWritePriv(), s.editHandler)
}
// @ID configurationGetAll
// @Summary Get all configurations
// @Success 200 {object} AllConfigItems
// @Router /configuration/all [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
// @Failure 403 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) getHandler(c *gin.Context) {
db := utils.GetTiDBConnection(c)
r, err := s.getAllConfigItems(db)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, r)
}
type EditRequest struct {
Kind ItemKind `json:"kind"`
ID string `json:"id"`
NewValue interface{} `json:"new_value"`
}
type EditResponse struct {
Warnings []rest.ErrorResponse `json:"warnings"`
}
// @ID configurationEdit
// @Summary Edit a configuration
// @Param request body EditRequest true "Request body"
// @Success 200 {object} EditResponse
// @Router /configuration/edit [post]
// @Security JwtAuth
// @Failure 400 {object} rest.ErrorResponse
// @Failure 401 {object} rest.ErrorResponse
// @Failure 403 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) editHandler(c *gin.Context) {
var req EditRequest
if err := c.ShouldBindJSON(&req); err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
db := utils.GetTiDBConnection(c)
warnings, err := s.editConfig(db, req.Kind, req.ID, req.NewValue)
if err != nil {
rest.Error(c, err)
return
}
var resp EditResponse
resp.Warnings = warnings
c.JSON(http.StatusOK, resp)
}
================================================
FILE: pkg/apiserver/configuration/service.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package configuration
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"sort"
"strconv"
"github.com/joomcode/errorx"
clientv3 "go.etcd.io/etcd/client/v3"
"go.uber.org/fx"
"gorm.io/gorm"
"github.com/pingcap/tidb-dashboard/pkg/config"
"github.com/pingcap/tidb-dashboard/pkg/pd"
"github.com/pingcap/tidb-dashboard/pkg/tidb"
"github.com/pingcap/tidb-dashboard/pkg/tikv"
"github.com/pingcap/tidb-dashboard/pkg/utils/topology"
"github.com/pingcap/tidb-dashboard/util/distro"
"github.com/pingcap/tidb-dashboard/util/rest"
)
var (
ErrNS = errorx.NewNamespace("error.api.config")
ErrListTopologyFailed = ErrNS.NewType("list_topology_failed")
ErrListConfigItemsFailed = ErrNS.NewType("list_config_items_failed")
ErrNotEditable = ErrNS.NewType("not_editable")
ErrEditFailed = ErrNS.NewType("edit_failed")
)
type ServiceParams struct {
fx.In
Config *config.Config
PDClient *pd.Client
EtcdClient *clientv3.Client
TiDBClient *tidb.Client
TiKVClient *tikv.Client
}
type Service struct {
params ServiceParams
lifecycleCtx context.Context
}
func NewService(lc fx.Lifecycle, p ServiceParams) *Service {
service := &Service{params: p}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
service.lifecycleCtx = ctx
return nil
},
})
return service
}
type ItemKind string
const (
ItemKindTiKVConfig ItemKind = "tikv_config"
ItemKindPDConfig ItemKind = "pd_config"
ItemKindTiDBConfig ItemKind = "tidb_config"
ItemKindTiDBVariable ItemKind = "tidb_variable"
)
type channelItem struct {
Err error
SourceDisplayAddress string
SourceKind ItemKind
Values map[string]interface{}
}
func processNestedConfigAPIResponse(data []byte) (map[string]interface{}, error) {
nestedConfig := make(map[string]interface{})
if err := json.Unmarshal(data, &nestedConfig); err != nil {
return nil, err
}
plainConfig := flattenRecursive(nestedConfig)
return plainConfig, nil
}
func (s *Service) getConfigItemsFromPDToChannel(ch chan<- channelItem) {
r, err := s.getConfigItemsFromPD()
if err != nil {
ch <- channelItem{Err: ErrListConfigItemsFailed.Wrap(err, "Failed to list PD config items")}
return
}
ch <- channelItem{
Err: nil,
SourceKind: ItemKindPDConfig,
Values: r,
}
}
func (s *Service) getConfigItemsFromPD() (map[string]interface{}, error) {
data, err := s.params.PDClient.SendGetRequest("/config")
if err != nil {
return nil, err
}
return processNestedConfigAPIResponse(data)
}
func (s *Service) getConfigItemsFromTiDBToChannel(tidb *topology.TiDBInfo, ch chan<- channelItem) {
displayAddress := net.JoinHostPort(tidb.IP, strconv.Itoa(int(tidb.Port)))
r, err := s.getConfigItemsFromTiDB(tidb.IP, int(tidb.StatusPort))
if err != nil {
ch <- channelItem{Err: ErrListConfigItemsFailed.Wrap(err, "Failed to list %s config items of %s", distro.R().TiDB, displayAddress)}
return
}
ch <- channelItem{
Err: nil,
SourceDisplayAddress: displayAddress,
SourceKind: ItemKindTiDBConfig,
Values: r,
}
}
func (s *Service) getConfigItemsFromTiDB(host string, statusPort int) (map[string]interface{}, error) {
data, err := s.params.TiDBClient.WithStatusAPIAddress(host, statusPort).SendGetRequest("/config")
if err != nil {
return nil, err
}
return processNestedConfigAPIResponse(data)
}
func (s *Service) getConfigItemsFromTiKVToChannel(tikv *topology.StoreInfo, ch chan<- channelItem) {
displayAddress := net.JoinHostPort(tikv.IP, strconv.Itoa(int(tikv.Port)))
r, err := s.getConfigItemsFromTiKV(tikv.IP, int(tikv.StatusPort))
if err != nil {
ch <- channelItem{Err: ErrListConfigItemsFailed.Wrap(err, "Failed to list TiKV config items of %s", displayAddress)}
return
}
ch <- channelItem{
Err: nil,
SourceDisplayAddress: displayAddress,
SourceKind: ItemKindTiKVConfig,
Values: r,
}
}
func (s *Service) getConfigItemsFromTiKV(host string, statusPort int) (map[string]interface{}, error) {
data, err := s.params.TiKVClient.SendGetRequest(host, statusPort, "/config")
if err != nil {
return nil, err
}
return processNestedConfigAPIResponse(data)
}
type ShowVariableItem struct {
Name string `gorm:"column:Variable_name"`
Value string `gorm:"column:Value"`
}
func (s *Service) getGlobalVariablesFromTiDBToChannel(db *gorm.DB, ch chan<- channelItem) {
r, err := s.getGlobalVariablesFromTiDB(db)
if err != nil {
ch <- channelItem{Err: ErrListConfigItemsFailed.Wrap(err, "Failed to list %s variables", distro.R().TiDB)}
return
}
ch <- channelItem{
Err: nil,
SourceKind: ItemKindTiDBVariable,
Values: r,
}
}
func (s *Service) getGlobalVariablesFromTiDB(db *gorm.DB) (map[string]interface{}, error) {
var rows []ShowVariableItem
if err := db.Raw("SHOW GLOBAL VARIABLES").Find(&rows).Error; err != nil {
return nil, err
}
result := make(map[string]interface{})
for _, r := range rows {
result[r.Name] = r.Value
}
return result, nil
}
type Item struct {
ID string `json:"id"`
IsEditable bool `json:"is_editable"`
IsMultiValue bool `json:"is_multi_value"` // TODO: Support per-instance config
Value interface{} `json:"value"` // When multi value present, this contains one of the value
}
type AllConfigItems struct {
Errors []rest.ErrorResponse `json:"errors"`
Items map[ItemKind][]Item `json:"items"`
}
func (s *Service) getAllConfigItems(db *gorm.DB) (*AllConfigItems, error) {
tikvInfo, _, err := topology.FetchStoreTopology(s.params.PDClient)
if err != nil {
return nil, ErrListTopologyFailed.Wrap(err, "Failed to list TiKV stores")
}
tidbInfo, err := topology.FetchTiDBTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
return nil, ErrListTopologyFailed.Wrap(err, "Failed to list %s instances", distro.R().TiDB)
}
ch := make(chan channelItem)
waitItems := 0
{
waitItems++
go s.getConfigItemsFromPDToChannel(ch)
}
{
waitItems++
go s.getGlobalVariablesFromTiDBToChannel(db, ch)
}
for _, item := range tikvInfo {
// TODO: What about tombstone stores?
waitItems++
item2 := item
go s.getConfigItemsFromTiKVToChannel(&item2, ch)
}
for _, item := range tidbInfo {
waitItems++
item2 := item
go s.getConfigItemsFromTiDBToChannel(&item2, ch)
}
errors := make([]rest.ErrorResponse, 0)
successItems := make([]channelItem, 0)
for i := 0; i < waitItems; i++ {
item := <-ch
if item.Err != nil {
errors = append(errors, rest.NewErrorResponse(item.Err))
continue
}
successItems = append(successItems, item)
}
close(ch)
// The first occurred value of each config item
valuesMap := make(map[ItemKind]map[string]interface{})
// Number of config item key occurred to detect missing config items
occurTimesMap := make(map[ItemKind]map[string]int)
// Whether each config item has different values
identicalMap := make(map[ItemKind]map[string]bool)
// The expected number of occur times
expectedOccurTimes := make(map[ItemKind]int)
for _, item := range successItems {
if _, ok := expectedOccurTimes[item.SourceKind]; !ok {
expectedOccurTimes[item.SourceKind] = 1
} else {
expectedOccurTimes[item.SourceKind]++
}
if _, ok := valuesMap[item.SourceKind]; !ok {
valuesMap[item.SourceKind] = make(map[string]interface{})
occurTimesMap[item.SourceKind] = make(map[string]int)
identicalMap[item.SourceKind] = make(map[string]bool)
}
for key, value := range item.Values {
if _, ok := valuesMap[item.SourceKind][key]; !ok {
valuesMap[item.SourceKind][key] = value
occurTimesMap[item.SourceKind][key] = 1
identicalMap[item.SourceKind][key] = true
} else {
occurTimesMap[item.SourceKind][key]++
if value != valuesMap[item.SourceKind][key] {
identicalMap[item.SourceKind][key] = false
}
}
}
}
result := make(map[ItemKind][]Item)
for kind, v := range valuesMap {
result[kind] = make([]Item, 0)
for configKey, configValue := range v {
// There are two cases when a config item has multiple values:
// 1. Values are not equal
// 2. Value is missing
isMultiValue := !identicalMap[kind][configKey]
value := configValue
if !isMultiValue && occurTimesMap[kind][configKey] < expectedOccurTimes[kind] {
isMultiValue = false
}
result[kind] = append(result[kind], Item{
ID: configKey,
IsEditable: isConfigItemEditable(kind, configKey),
IsMultiValue: isMultiValue,
Value: value,
})
}
s := result[kind]
sort.Slice(s, func(i, j int) bool {
return s[i].ID < s[j].ID
})
}
return &AllConfigItems{
Errors: errors,
Items: result,
}, nil
}
func (s *Service) editConfig(db *gorm.DB, kind ItemKind, id string, newValue interface{}) ([]rest.ErrorResponse, error) {
if !isConfigItemEditable(kind, id) {
return nil, ErrNotEditable.New("Configuration `%s` is not editable", id)
}
body := make(map[string]interface{})
body[id] = newValue
bodyJSON, err := json.Marshal(&body)
if err != nil {
return nil, ErrEditFailed.WrapWithNoMessage(err)
}
switch kind {
case ItemKindPDConfig:
_, err := s.params.PDClient.SendPostRequest("/config", bytes.NewBuffer(bodyJSON))
if err != nil {
return nil, ErrEditFailed.WrapWithNoMessage(err)
}
case ItemKindTiKVConfig:
tikvInfo, _, err := topology.FetchStoreTopology(s.params.PDClient)
if err != nil {
return nil, ErrEditFailed.WrapWithNoMessage(ErrListTopologyFailed.WrapWithNoMessage(err))
}
failures := make([]error, 0)
for _, kvStore := range tikvInfo {
// TODO: What about tombstone stores?
_, err := s.params.TiKVClient.SendPostRequest(kvStore.IP, int(kvStore.StatusPort), "/config", bytes.NewBuffer(bodyJSON))
if err != nil {
failures = append(failures, ErrEditFailed.Wrap(err, "Failed to edit config for TiKV instance `%s`", net.JoinHostPort(kvStore.IP, strconv.Itoa(int(kvStore.Port)))))
}
}
if len(failures) == len(tikvInfo) {
if len(failures) > 0 {
return nil, failures[0]
}
return nil, nil
}
warnings := make([]rest.ErrorResponse, 0)
for _, err := range failures {
warnings = append(warnings, rest.NewErrorResponse(err))
}
return warnings, nil
case ItemKindTiDBVariable:
// We have checked the correctness of id, so no need to worry about injections
if err := db.Exec(fmt.Sprintf("SET GLOBAL %s = ?", id), newValue).Error; err != nil {
return nil, ErrEditFailed.WrapWithNoMessage(err)
}
default:
return nil, ErrEditFailed.New("Edit failed, not implemented")
}
return nil, nil
}
================================================
FILE: pkg/apiserver/conprof/module.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package conprof
import (
"go.uber.org/fx"
)
var Module = fx.Options(
fx.Provide(newService),
fx.Invoke(registerRouter),
)
================================================
FILE: pkg/apiserver/conprof/service.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
// conprof is short for continuous profiling
package conprof
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
clientv3 "go.etcd.io/etcd/client/v3"
"go.uber.org/fx"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap/tidb-dashboard/pkg/config"
"github.com/pingcap/tidb-dashboard/util/featureflag"
"github.com/pingcap/tidb-dashboard/util/rest"
)
type ServiceParams struct {
fx.In
EtcdClient *clientv3.Client
Config *config.Config
NgmProxy *utils.NgmProxy
FeatureFlags *featureflag.Registry
}
type Service struct {
FeatureFlagConprof *featureflag.FeatureFlag
params ServiceParams
lifecycleCtx context.Context
}
func newService(lc fx.Lifecycle, p ServiceParams) *Service {
s := &Service{
FeatureFlagConprof: p.FeatureFlags.Register("conprof", ">= 5.3.0"),
params: p,
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
s.lifecycleCtx = ctx
return nil
},
})
return s
}
// Register register the handlers to the service.
func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/continuous_profiling")
endpoint.Use(s.FeatureFlagConprof.VersionGuard())
{
endpoint.GET("/config", auth.MWAuthRequired(), s.params.NgmProxy.Route("/config"))
endpoint.POST("/config", auth.MWAuthRequired(), auth.MWRequireWritePriv(), s.params.NgmProxy.Route("/config"))
endpoint.GET("/components", auth.MWAuthRequired(), s.params.NgmProxy.Route("/continuous_profiling/components"))
endpoint.GET("/estimate_size", auth.MWAuthRequired(), s.params.NgmProxy.Route("/continuous_profiling/estimate_size"))
endpoint.GET("/group_profiles", auth.MWAuthRequired(), s.params.NgmProxy.Route("/continuous_profiling/group_profiles"))
endpoint.GET("/group_profile/detail", auth.MWAuthRequired(), s.params.NgmProxy.Route("/continuous_profiling/group_profile/detail"))
endpoint.GET("/action_token", auth.MWAuthRequired(), s.GenConprofActionToken)
endpoint.GET("/download", s.parseJWTToken, s.params.NgmProxy.Route("/continuous_profiling/download"))
endpoint.GET("/single_profile/view", s.parseJWTToken, s.params.NgmProxy.Route("/continuous_profiling/single_profile/view"))
}
}
type ContinuousProfilingConfig struct {
Enable bool `json:"enable"`
ProfileSeconds int `json:"profile_seconds"`
IntervalSeconds int `json:"interval_seconds"`
TimeoutSeconds int `json:"timeout_seconds"`
DataRetentionSeconds int `json:"data_retention_seconds"`
}
type NgMonitoringConfig struct {
ContinuousProfiling ContinuousProfilingConfig `json:"continuous_profiling"`
}
// @Summary Get Continuous Profiling Config
// @Success 200 {object} NgMonitoringConfig
// @Router /continuous_profiling/config [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) ConprofConfig(_ *gin.Context) {
// dummy, for generate openapi
}
// @Summary Update Continuous Profiling Config
// @Router /continuous_profiling/config [post]
// @Param request body NgMonitoringConfig true "Request body"
// @Security JwtAuth
// @Success 200 {string} string "ok"
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) UpdateConprofConfig(_ *gin.Context) {
// dummy, for generate openapi
}
type Component struct {
Name string `json:"name"`
IP string `json:"ip"`
Port uint `json:"port"`
StatusPort uint `json:"status_port"`
}
// @Summary Get current scraping components
// @Success 200 {array} Component
// @Router /continuous_profiling/components [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) ConprofComponents(_ *gin.Context) {
// dummy, for generate openapi
}
type EstimateSizeRes struct {
InstanceCount int `json:"instance_count"`
ProfileSize int `json:"profile_size"`
}
// @Summary Get Estimate Size
// @Router /continuous_profiling/estimate_size [get]
// @Security JwtAuth
// @Success 200 {object} EstimateSizeRes
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) EstimateSize(_ *gin.Context) {
// dummy, for generate openapi
}
type GetGroupProfileReq struct {
BeginTime int `json:"begin_time"`
EndTime int `json:"end_time"`
}
type ComponentNum struct {
TiDB int `json:"tidb"`
PD int `json:"pd"`
TiKV int `json:"tikv"`
TiFlash int `json:"tiflash"`
TiCDC int `json:"ticdc"`
}
type GroupProfiles struct {
Ts int64 `json:"ts"`
ProfileSecs int `json:"profile_duration_secs"`
State string `json:"state"`
CompNum ComponentNum `json:"component_num"`
}
type GroupProfileDetail struct {
Ts int64 `json:"ts"`
ProfileSecs int `json:"profile_duration_secs"`
State string `json:"state"`
TargetProfiles []ProfileDetail `json:"target_profiles"`
}
type ProfileDetail struct {
State string `json:"state"`
Error string `json:"error"`
Type string `json:"profile_type"`
Target Target `json:"target"`
}
type Target struct {
Component string `json:"component"`
Address string `json:"address"`
}
// @Summary Get Group Profiles
// @Router /continuous_profiling/group_profiles [get]
// @Param q query GetGroupProfileReq true "Query"
// @Security JwtAuth
// @Success 200 {array} GroupProfiles
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) ConprofGroupProfiles(_ *gin.Context) {
// dummy, for generate openapi
}
// @Summary Get Group Profile Detail
// @Router /continuous_profiling/group_profile/detail [get]
// @Param ts query number true "timestamp"
// @Security JwtAuth
// @Success 200 {object} GroupProfileDetail
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) ConprofGroupProfileDetail(_ *gin.Context) {
// dummy, for generate openapi
}
// @Summary Get action token for download or view profile
// @Router /continuous_profiling/action_token [get]
// @Param q query string true "target query string"
// @Security JwtAuth
// @Success 200 {string} string
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) GenConprofActionToken(c *gin.Context) {
q := c.Query("q")
token, err := utils.NewJWTString("conprof", q)
if err != nil {
rest.Error(c, err)
return
}
c.String(http.StatusOK, token)
}
// @Summary Download Group Profile files
// @Router /continuous_profiling/download [get]
// @Param ts query number true "timestamp"
// @Security JwtAuth
// @Produce application/x-gzip
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) ConprofDownload(_ *gin.Context) {
// dummy, for generate openapi
}
func (s *Service) parseJWTToken(c *gin.Context) {
token := c.Query("token")
queryStr, err := utils.ParseJWTString("conprof", token)
if err != nil {
rest.Error(c, rest.ErrBadRequest.WrapWithNoMessage(err))
c.Abort()
return
}
c.Request.URL.RawQuery = queryStr
}
type ViewSingleProfileReq struct {
Ts int `json:"ts"`
ProfileType string `json:"profile_type"`
Component string `json:"component"`
Address string `json:"address"`
}
// @Summary View Single Profile files
// @Router /continuous_profiling/single_profile/view [get]
// @Param q query ViewSingleProfileReq true "Query"
// @Security JwtAuth
// @Produce html
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
func (s *Service) ConprofViewProfile(_ *gin.Context) {
// dummy, for generate openapi
}
================================================
FILE: pkg/apiserver/deadlock/model.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package deadlock
import "time"
type Model struct {
Instance string `gorm:"column:INSTANCE" json:"instance"`
DeadlockID uint64 `gorm:"column:DEADLOCK_ID" json:"id"`
OccurTime time.Time `gorm:"column:OCCUR_TIME" json:"occur_time"`
Retryable bool `gorm:"column:RETRYABLE" json:"retryable"`
TryLockTrxID uint64 `gorm:"column:TRY_LOCK_TRX_ID" json:"try_lock_trx_id"`
TryHoldingLock uint64 `gorm:"column:TRX_HOLDING_LOCK" json:"trx_holding_lock"`
CurrentSQL string `gorm:"column:CURRENT_SQL_DIGEST_TEXT" json:"current_sql"`
Key string `gorm:"column:KEY" json:"key"`
KeyInfo string `gorm:"column:KEY_INFO" json:"key_info"`
}
================================================
FILE: pkg/apiserver/deadlock/module.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package deadlock
import "go.uber.org/fx"
var Module = fx.Options(
fx.Provide(newService),
fx.Invoke(registerRouter),
)
================================================
FILE: pkg/apiserver/deadlock/service.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package deadlock
import (
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/fx"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap/tidb-dashboard/pkg/tidb"
commonUtils "github.com/pingcap/tidb-dashboard/pkg/utils"
"github.com/pingcap/tidb-dashboard/util/rest"
)
const (
DeadlockTable = "INFORMATION_SCHEMA.CLUSTER_DEADLOCKS"
)
type ServiceParams struct {
fx.In
TiDBClient *tidb.Client
SysSchema *commonUtils.SysSchema
}
type Service struct {
params ServiceParams
}
func newService(p ServiceParams) *Service {
return &Service{params: p}
}
func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/deadlock")
endpoint.Use(
auth.MWAuthRequired(),
utils.MWConnectTiDB(s.params.TiDBClient),
)
{
endpoint.GET("/list", s.getList)
}
}
// @Summary List all deadlock records
// @Success 200 {array} Model
// @Router /deadlock/list [get]
// @Security JwtAuth
// @Failure 400 {object} rest.ErrorResponse
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) getList(c *gin.Context) {
db := utils.GetTiDBConnection(c)
var results []Model
err := db.Table(DeadlockTable).Find(&results).Error
if err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
c.JSON(http.StatusOK, results)
}
================================================
FILE: pkg/apiserver/debugapi/apis.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package debugapi
import (
"github.com/go-resty/resty/v2"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/debugapi/endpoint"
"github.com/pingcap/tidb-dashboard/util/client/httpclient"
"github.com/pingcap/tidb-dashboard/util/topo"
)
var commonParamPprofKinds = endpoint.APIParamEnum("kind", true, []endpoint.EnumItemDefinition{
{Value: "allocs"},
{Value: "block"},
{Value: "cmdline"},
{Value: "goroutine"},
{Value: "heap"},
{Value: "mutex"},
{Value: "profile"},
{Value: "threadcreate"},
{Value: "trace"},
})
var commonParamPprofSeconds = endpoint.APIParamEnum("seconds", false, []endpoint.EnumItemDefinition{
{Value: "10", DisplayAs: "10s"},
{Value: "30", DisplayAs: "30s"},
{Value: "60", DisplayAs: "60s"},
})
var commonParamPprofDebug = endpoint.APIParamEnum("debug", false, []endpoint.EnumItemDefinition{
{Value: "0", DisplayAs: "Raw Format"},
{Value: "1", DisplayAs: "Legacy Text Format"},
{Value: "2", DisplayAs: "Text Format"},
})
var commonParamConfigFormat = endpoint.APIParamEnum("format", false, []endpoint.EnumItemDefinition{
{Value: "toml"},
{Value: "json"},
})
var apiEndpoints = []endpoint.APIDefinition{
// TiDB Endpoints
{
ID: "tidb_stats_by_table",
Component: topo.KindTiDB,
Path: "/stats/dump/{db}/{table}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamDBName("db", true),
endpoint.APIParamTableName("table", true),
},
},
{
ID: "tidb_stats_by_table_timestamp",
Component: topo.KindTiDB,
Path: "/stats/dump/{db}/{table}/{yyyyMMddHHmmss}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamDBName("db", true),
endpoint.APIParamTableName("table", true),
endpoint.APIParamText("yyyyMMddHHmmss", true),
},
},
{
ID: "tidb_settings",
Component: topo.KindTiDB,
Path: "/settings",
Method: resty.MethodGet,
},
{
ID: "tidb_schema",
Component: topo.KindTiDB,
Path: "/schema",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
endpoint.APIParamTableID("table_id", false),
},
},
{
ID: "tidb_schema_by_db",
Component: topo.KindTiDB,
Path: "/schema/{db}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamDBName("db", true),
},
},
{
ID: "tidb_schema_by_table",
Component: topo.KindTiDB,
Path: "/schema/{db}/{table}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamDBName("db", true),
endpoint.APIParamTableName("table", true),
},
},
{
ID: "tidb_schema_by_table_id",
Component: topo.KindTiDB,
Path: "/db-table/{tableID}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamTableID("tableID", true),
},
},
{
ID: "tidb_ddl_history",
Component: topo.KindTiDB,
Path: "/ddl/history",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("start_job_id", false),
endpoint.APIParamIntWithDefaultVal("limit", false, "10"),
},
},
{
ID: "tidb_server_info",
Component: topo.KindTiDB,
Path: "/info",
Method: resty.MethodGet,
},
{
ID: "tidb_all_servers_info",
Component: topo.KindTiDB,
Path: "/info/all",
Method: resty.MethodGet,
},
{
ID: "tidb_all_regions_meta",
Component: topo.KindTiDB,
Path: "/regions/meta",
Method: resty.MethodGet,
},
{
ID: "tidb_region_meta_by_id",
Component: topo.KindTiDB,
Path: "/regions/{regionID}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("regionID", true),
},
},
{
ID: "tidb_table_regions",
Component: topo.KindTiDB,
Path: "/tables/{db}/{table}/regions",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamDBName("db", true),
endpoint.APIParamTableName("table", true),
},
},
{
ID: "tidb_hot_regions",
Component: topo.KindTiDB,
Path: "/regions/hot",
Method: resty.MethodGet,
},
{
ID: "tidb_pprof",
Component: topo.KindTiDB,
Path: "/debug/pprof/{kind}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
commonParamPprofKinds,
},
QueryParams: []endpoint.APIParamDefinition{
commonParamPprofSeconds,
commonParamPprofDebug,
},
},
// PD Endpoints
{
ID: "pd_cluster_info",
Component: topo.KindPD,
Path: "/pd/api/v1/cluster",
Method: resty.MethodGet,
},
{
ID: "pd_cluster_status",
Component: topo.KindPD,
Path: "/pd/api/v1/cluster/status",
Method: resty.MethodGet,
},
{
ID: "pd_configs_all",
Component: topo.KindPD,
Path: "/pd/api/v1/config",
Method: resty.MethodGet,
},
{
ID: "pd_health",
Component: topo.KindPD,
Path: "/pd/api/v1/health",
Method: resty.MethodGet,
},
{
ID: "pd_hot_read",
Component: topo.KindPD,
Path: "/pd/api/v1/hotspot/regions/read",
Method: resty.MethodGet,
},
{
ID: "pd_hot_write",
Component: topo.KindPD,
Path: "/pd/api/v1/hotspot/regions/write",
Method: resty.MethodGet,
},
{
ID: "pd_hot_stores",
Component: topo.KindPD,
Path: "/pd/api/v1/hotspot/stores",
Method: resty.MethodGet,
},
{
ID: "pd_labels_all",
Component: topo.KindPD,
Path: "/pd/api/v1/labels",
Method: resty.MethodGet,
},
{
ID: "pd_members_all",
Component: topo.KindPD,
Path: "/pd/api/v1/members",
Method: resty.MethodGet,
},
{
ID: "pd_leader",
Component: topo.KindPD,
Path: "/pd/api/v1/leader",
Method: resty.MethodGet,
},
{
ID: "pd_operators",
Component: topo.KindPD,
Path: "/pd/api/v1/operators",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
endpoint.APIParamEnum("kind", false, []endpoint.EnumItemDefinition{
{Value: "admin"},
{Value: "leader"},
{Value: "region"},
}),
},
},
{
ID: "pd_regions_all",
Component: topo.KindPD,
Path: "/pd/api/v1/regions",
Method: resty.MethodGet,
},
{
ID: "pd_region_by_id",
Component: topo.KindPD,
Path: "/pd/api/v1/region/id/{regionID}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("regionID", true),
},
},
{
ID: "pd_region_by_key",
Component: topo.KindPD,
Path: "/pd/api/v1/region/key/{regionKey}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamPDKey("regionKey", true),
},
},
{
ID: "pd_regions_sibling_by_id",
Component: topo.KindPD,
Path: "/pd/api/v1/regions/sibling/{regionID}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("regionID", true),
},
},
{
ID: "pd_regions_store",
Component: topo.KindPD,
Path: "/pd/api/v1/regions/store/{storeID}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("storeID", true),
},
},
{
ID: "pd_regions_by_top_read",
Component: topo.KindPD,
Path: "/pd/api/v1/regions/readflow",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("limit", false),
},
},
{
ID: "pd_regions_by_top_write",
Component: topo.KindPD,
Path: "/pd/api/v1/regions/writeflow",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("limit", false),
},
},
{
ID: "pd_regions_by_top_conf_ver",
Component: topo.KindPD,
Path: "/pd/api/v1/regions/confver",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("limit", false),
},
},
{
ID: "pd_regions_by_top_version",
Component: topo.KindPD,
Path: "/pd/api/v1/regions/version",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("limit", false),
},
},
{
ID: "pd_regions_by_top_size",
Component: topo.KindPD,
Path: "/pd/api/v1/regions/size",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("limit", false),
},
},
{
ID: "pd_regions_by_state",
Component: topo.KindPD,
Path: "/pd/api/v1/regions/check/{state}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamEnum("state", true, []endpoint.EnumItemDefinition{
{Value: "miss-peer", DisplayAs: "Regions that miss peer"},
{Value: "extra-peer", DisplayAs: "Regions that has extra peer"},
{Value: "down-peer", DisplayAs: "Regions that has down peer"},
{Value: "pending-peer", DisplayAs: "Regions that has pending peer"},
{Value: "offline-peer", DisplayAs: "Regions that has offline peer"},
{Value: "empty-region", DisplayAs: "Empty regions"},
}),
},
},
{
ID: "pd_schedulers_all",
Component: topo.KindPD,
Path: "/pd/api/v1/schedulers",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
endpoint.APIParamEnum("status", false, []endpoint.EnumItemDefinition{
{Value: "paused"},
{Value: "disabled"},
}),
},
},
{
ID: "pd_stores_all",
Component: topo.KindPD,
Path: "/pd/api/v1/stores",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
// TODO: Actually it accepts multiple values.
endpoint.APIParamEnum("state", false, []endpoint.EnumItemDefinition{
{Value: "0", DisplayAs: "Up"},
{Value: "1", DisplayAs: "Offline"},
{Value: "2", DisplayAs: "Tombstone"},
}),
},
},
{
ID: "pd_stores_by_label",
Component: topo.KindPD,
Path: "/pd/api/v1/labels/stores",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
endpoint.APIParamText("name", true),
endpoint.APIParamText("value", true),
},
},
{
ID: "pd_store_by_id",
Component: topo.KindPD,
Path: "/pd/api/v1/store/{storeID}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
endpoint.APIParamInt("storeID", true),
},
},
{
ID: "pd_pprof",
Component: topo.KindPD,
Path: "/debug/pprof/{kind}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
commonParamPprofKinds,
},
QueryParams: []endpoint.APIParamDefinition{
commonParamPprofSeconds,
commonParamPprofDebug,
},
},
// TiKV Endpoints
{
ID: "tikv_config",
Component: topo.KindTiKV,
Path: "/config",
Method: resty.MethodGet,
},
{
ID: "tikv_pprof_profile",
Component: topo.KindTiKV,
Path: "/debug/pprof/profile",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
commonParamPprofSeconds,
},
BeforeSendRequest: func(req *httpclient.LazyRequest) {
req.SetHeader("Content-Type", "application/protobuf")
},
},
// TiFlash Endpoints
{
ID: "tiflash_config",
Component: topo.KindTiFlash,
Path: "/config",
Method: resty.MethodGet,
},
{
ID: "tiflash_pprof_profile",
Component: topo.KindTiFlash,
Path: "/debug/pprof/profile",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
commonParamPprofSeconds,
},
BeforeSendRequest: func(req *httpclient.LazyRequest) {
req.SetHeader("Content-Type", "application/protobuf")
},
},
// TiProxy Endpoints
{
ID: "tiproxy_config",
Component: topo.KindTiProxy,
Path: "/api/admin/config",
Method: resty.MethodGet,
QueryParams: []endpoint.APIParamDefinition{
commonParamConfigFormat,
},
},
{
ID: "tiproxy_pprof",
Component: topo.KindTiProxy,
Path: "/debug/pprof/{kind}",
Method: resty.MethodGet,
PathParams: []endpoint.APIParamDefinition{
commonParamPprofKinds,
},
QueryParams: []endpoint.APIParamDefinition{
commonParamPprofSeconds,
commonParamPprofDebug,
},
},
}
================================================
FILE: pkg/apiserver/debugapi/endpoint/1_main_test.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package endpoint
import (
"testing"
"github.com/pingcap/tidb-dashboard/util/testutil/testdefault"
)
func TestMain(m *testing.M) {
testdefault.TestMain(m)
}
================================================
FILE: pkg/apiserver/debugapi/endpoint/errors.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package endpoint
import (
"github.com/joomcode/errorx"
)
var (
ErrNS = errorx.NewNamespace("debug_api.endpoint")
ErrUnknownComponent = ErrNS.NewType("unknown_component")
ErrInvalidEndpoint = ErrNS.NewType("invalid_endpoint")
)
================================================
FILE: pkg/apiserver/debugapi/endpoint/models.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package endpoint
import (
"encoding/hex"
"fmt"
"strconv"
"github.com/pingcap/tidb-dashboard/util/client/httpclient"
"github.com/pingcap/tidb-dashboard/util/topo"
)
// APIDefinition defines what an API endpoints accepts.
// APIDefinition can be "resolved" to become a request when its parameter values are given via RequestPayload.
type APIDefinition struct {
ID string `json:"id"`
Component topo.Kind `json:"component"`
Path string `json:"path"`
Method string `json:"method"`
PathParams []APIParamDefinition `json:"path_params"` // e.g. /stats/dump/{db}/{table} -> db, table
QueryParams []APIParamDefinition `json:"query_params"` // e.g. /debug/pprof?seconds=1 -> seconds
BeforeSendRequest func(req *httpclient.LazyRequest) `json:"-"`
}
type APIParamResolveFn func(value string) ([]string, error)
// APIParamDefinition defines what an API endpoint parameter accepts and how it should look like in the UI.
// Usually this struct doesn't need to be manually constructed. Use APIParamXxx() helpers.
type APIParamDefinition struct {
Name string `json:"name"`
Required bool `json:"required"`
UIComponentKind string `json:"ui_kind"`
UIComponentProps interface{} `json:"ui_props"` // varies by different ui kinds
OnResolve APIParamResolveFn `json:"-"`
}
func (d *APIParamDefinition) Resolve(value string) ([]string, error) {
if d.OnResolve == nil {
return []string{value}, nil
}
return d.OnResolve(value)
}
// UIComponentTextProps is the type of UIComponentProps when UIComponentKind is "text".
type UIComponentTextProps struct {
Placeholder string `json:"placeholder"`
DefaultVal string `json:"default_val"`
}
func APIParamText(name string, required bool) APIParamDefinition {
return APIParamDefinition{
Name: name,
Required: required,
UIComponentKind: "text",
}
}
func APIParamInt(name string, required bool) APIParamDefinition {
return APIParamIntWithDefaultVal(name, required, "")
}
func APIParamIntWithDefaultVal(name string, required bool, defVal string) APIParamDefinition {
placeHolder := "(int)"
if defVal != "" {
placeHolder = fmt.Sprintf("(int, default: %s)", defVal)
}
return APIParamDefinition{
Name: name,
Required: required,
UIComponentKind: "text",
UIComponentProps: UIComponentTextProps{
Placeholder: placeHolder,
DefaultVal: defVal,
},
OnResolve: func(value string) ([]string, error) {
if _, err := strconv.Atoi(value); err != nil {
return nil, fmt.Errorf("'%s' is not a int", value)
}
return []string{value}, nil
},
}
}
func APIParamDBName(name string, required bool) APIParamDefinition {
return APIParamDefinition{
Name: name,
Required: required,
UIComponentKind: "db_dropdown",
}
}
func APIParamTableName(name string, required bool) APIParamDefinition {
return APIParamDefinition{
Name: name,
Required: required,
UIComponentKind: "table_dropdown",
}
}
func APIParamTableID(name string, required bool) APIParamDefinition {
return APIParamDefinition{
Name: name,
Required: required,
UIComponentKind: "table_id_dropdown",
}
}
// UIComponentDropdownProps is the type of UIComponentProps when UIComponentKind is "dropdown".
type UIComponentDropdownProps struct {
Items []EnumItemDefinition `json:"items"`
}
type EnumItemDefinition struct {
Value string `json:"value"`
DisplayAs string `json:"display_as"` // Optional
}
func APIParamEnum(name string, required bool, items []EnumItemDefinition) APIParamDefinition {
return APIParamDefinition{
Name: name,
Required: required,
UIComponentKind: "dropdown",
UIComponentProps: UIComponentDropdownProps{Items: items},
OnResolve: func(value string) ([]string, error) {
for _, item := range items {
if item.Value == value {
return []string{value}, nil
}
}
return nil, fmt.Errorf("'%s' is not a valid enum value", value)
},
}
}
// Below are some special API param kinds.
func APIParamPDKey(name string, required bool) APIParamDefinition {
return APIParamDefinition{
Name: name,
Required: required,
UIComponentKind: "text",
UIComponentProps: UIComponentTextProps{
Placeholder: "(hex key, e.g. 748000...)",
},
OnResolve: func(value string) ([]string, error) {
keyBinary, err := hex.DecodeString(value)
if err != nil {
return nil, fmt.Errorf("'%s' is not a valid hex key", value)
}
return []string{string(keyBinary)}, nil
},
}
}
================================================
FILE: pkg/apiserver/debugapi/endpoint/models_test.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package endpoint
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestAPIParamPDKey(t *testing.T) {
p := APIParamPDKey("foo", true)
require.Equal(t, p.Name, "foo")
require.True(t, p.Required)
v, err := p.Resolve("fooo")
require.Nil(t, v)
require.NotNil(t, err)
require.Contains(t, err.Error(), "'fooo' is not a valid hex key")
v, err = p.Resolve("0x0011")
require.Nil(t, v)
require.NotNil(t, err)
require.Contains(t, err.Error(), "'0x0011' is not a valid hex key")
v, err = p.Resolve("0011")
require.Equal(t, []string{"\x00\x11"}, v)
require.Nil(t, err)
}
func TestAPIParamEnum(t *testing.T) {
p := APIParamEnum("bar", false, []EnumItemDefinition{
{Value: "v1"},
{Value: "v2", DisplayAs: "d1"},
})
require.Equal(t, p.Name, "bar")
require.False(t, p.Required)
v, err := p.Resolve("x")
require.Nil(t, v)
require.NotNil(t, err)
require.Contains(t, err.Error(), "'x' is not a valid enum value")
v, err = p.Resolve("")
require.Nil(t, v)
require.NotNil(t, err)
require.Contains(t, err.Error(), "'' is not a valid enum value")
v, err = p.Resolve("v1")
require.Equal(t, []string{"v1"}, v)
require.Nil(t, err)
v, err = p.Resolve("d1")
require.Nil(t, v)
require.NotNil(t, err)
require.Contains(t, err.Error(), "'d1' is not a valid enum value")
}
func TestAPIParamInt(t *testing.T) {
p := APIParamInt("ix", true)
require.Equal(t, p.Name, "ix")
require.True(t, p.Required)
v, err := p.Resolve("ab")
require.Nil(t, v)
require.NotNil(t, err)
require.Contains(t, err.Error(), "'ab' is not a int")
v, err = p.Resolve("123.4")
require.Nil(t, v)
require.NotNil(t, err)
require.Contains(t, err.Error(), "'123.4' is not a int")
v, err = p.Resolve("123")
require.Equal(t, []string{"123"}, v)
require.Nil(t, err)
}
================================================
FILE: pkg/apiserver/debugapi/endpoint/payload.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package endpoint
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
clientv3 "go.etcd.io/etcd/client/v3"
"github.com/pingcap/tidb-dashboard/pkg/pd"
"github.com/pingcap/tidb-dashboard/pkg/utils/topology"
"github.com/pingcap/tidb-dashboard/util/client/httpclient"
"github.com/pingcap/tidb-dashboard/util/client/pdclient"
"github.com/pingcap/tidb-dashboard/util/client/schedulingclient"
"github.com/pingcap/tidb-dashboard/util/client/ticdcclient"
"github.com/pingcap/tidb-dashboard/util/client/tidbclient"
"github.com/pingcap/tidb-dashboard/util/client/tiflashclient"
"github.com/pingcap/tidb-dashboard/util/client/tikvclient"
"github.com/pingcap/tidb-dashboard/util/client/tiproxyclient"
"github.com/pingcap/tidb-dashboard/util/client/tsoclient"
"github.com/pingcap/tidb-dashboard/util/rest"
"github.com/pingcap/tidb-dashboard/util/topo"
)
// RequestPayload describes how a server-side request should be sent, by describing the API endpoint to send
// and its parameter values. The content of this struct is specified by the user so that it should be carefully
// checked.
type RequestPayload struct {
API string `json:"api_id"`
Host string `json:"host"`
Port int `json:"port"`
ParamValues map[string]string `json:"param_values"`
}
type HTTPClients struct {
PDAPIClient *pdclient.APIClient
TiDBStatusClient *tidbclient.StatusClient
TiKVStatusClient *tikvclient.StatusClient
TiFlashStatusClient *tiflashclient.StatusClient
TiCDCStatusClient *ticdcclient.StatusClient
TiProxyStatusClient *tiproxyclient.StatusClient
TSOStatusClient *tsoclient.StatusClient
SchedulingStatusClient *schedulingclient.StatusClient
}
func (c HTTPClients) GetHTTPClientByNodeKind(kind topo.Kind) *httpclient.Client {
switch kind {
case topo.KindPD:
if c.PDAPIClient == nil {
return nil
}
return c.PDAPIClient.Client
case topo.KindTiDB:
if c.TiDBStatusClient == nil {
return nil
}
return c.TiDBStatusClient.Client
case topo.KindTiKV:
if c.TiKVStatusClient == nil {
return nil
}
return c.TiKVStatusClient.Client
case topo.KindTiFlash:
if c.TiFlashStatusClient == nil {
return nil
}
return c.TiFlashStatusClient.Client
case topo.KindTiCDC:
if c.TiCDCStatusClient == nil {
return nil
}
return c.TiCDCStatusClient.Client
case topo.KindTiProxy:
if c.TiProxyStatusClient == nil {
return nil
}
return c.TiProxyStatusClient.Client
default:
return nil
}
}
// RequestPayloadResolver resolves the request payload using specified API definitions.
//
// The relationship is below:
//
// RequestPayload ---(RequestPayloadResolver.ResolvePayload)---> ResolvedRequestPayload
type RequestPayloadResolver struct {
apis []APIDefinition
apiMapByID map[string]*APIDefinition
}
func NewRequestPayloadResolver(apis []APIDefinition, acceptedClients HTTPClients) *RequestPayloadResolver {
// Filter APIs by accepted clients
filteredAPIs := make([]APIDefinition, 0, len(apis))
for _, api := range apis {
httpClient := acceptedClients.GetHTTPClientByNodeKind(api.Component)
if httpClient != nil {
filteredAPIs = append(filteredAPIs, api)
}
}
apiMapByID := make(map[string]*APIDefinition)
for idx := range filteredAPIs {
api := &filteredAPIs[idx]
apiMapByID[api.ID] = api
}
return &RequestPayloadResolver{
apis: filteredAPIs,
apiMapByID: apiMapByID,
}
}
func (r *RequestPayloadResolver) ListAPIs() []APIDefinition {
return r.apis
}
var pathReplaceRegexp = regexp.MustCompile(`\{(\w+)\}`)
func (r *RequestPayloadResolver) ResolvePayload(payload RequestPayload) (*ResolvedRequestPayload, error) {
if payload.ParamValues == nil {
// let's make life easier
payload.ParamValues = make(map[string]string)
}
api, ok := r.apiMapByID[payload.API]
if !ok {
return nil, rest.ErrBadRequest.New("Unknown API endpoint '%s'", payload.API)
}
resolvedPayload := &ResolvedRequestPayload{
api: api,
host: payload.Host,
port: payload.Port,
path: "", // will be filled later
queryValues: url.Values{},
}
// Resolve path
pathValues := map[string]string{}
for _, pathParam := range api.PathParams {
// path param should always be required
if payload.ParamValues[pathParam.Name] == "" {
return nil, rest.ErrBadRequest.New("parameter '%s' is required", pathParam.Name)
}
resolvedValue, err := pathParam.Resolve(payload.ParamValues[pathParam.Name])
if err != nil {
return nil, rest.ErrBadRequest.Wrap(err, "parameter '%s' is invalid", pathParam.Name)
}
pathValues[pathParam.Name] = resolvedValue[0]
}
resolvedPayload.path = pathReplaceRegexp.ReplaceAllStringFunc(api.Path, func(s string) string {
key := pathReplaceRegexp.ReplaceAllString(s, "${1}")
val := url.PathEscape(pathValues[key])
return val
})
// Resolve query
for _, queryParam := range api.QueryParams {
if payload.ParamValues[queryParam.Name] == "" {
if queryParam.Required {
return nil, rest.ErrBadRequest.New("parameter '%s' is required", queryParam.Name)
}
continue
}
resolvedValue, err := queryParam.Resolve(payload.ParamValues[queryParam.Name])
if err != nil {
return nil, rest.ErrBadRequest.Wrap(err, "parameter '%s' is invalid", queryParam.Name)
}
resolvedPayload.queryValues[queryParam.Name] = resolvedValue
}
return resolvedPayload, nil
}
// ResolvedRequestPayload describes the final request to send by the server.
// It is constructed by from the RequestPayload and the corresponding APIDefinition.
type ResolvedRequestPayload struct {
api *APIDefinition
host string
port int
path string
queryValues url.Values
}
func (p *ResolvedRequestPayload) SendRequestAndPipe(
ctx context.Context,
clientsToUse HTTPClients,
etcdClient *clientv3.Client,
pdClient *pd.Client,
w io.Writer,
) (respNoBody *http.Response, err error) {
if etcdClient != nil && pdClient != nil { // It can only be false in tests.
if err := p.verifyEndpoint(ctx, etcdClient, pdClient); err != nil {
return nil, err
}
}
httpClient := clientsToUse.GetHTTPClientByNodeKind(p.api.Component)
if httpClient == nil {
return nil, ErrUnknownComponent.New("Unknown component '%s'", p.api.Component)
}
req := httpClient.LR().
SetDebugTag("origin:debug_api").
SetTLSAwareBaseURL(fmt.Sprintf("http://%s", net.JoinHostPort(p.host, strconv.Itoa(p.port)))).
SetMethod(p.api.Method).
SetURL(p.path).
SetQueryParamsFromValues(p.queryValues)
if p.api.BeforeSendRequest != nil {
p.api.BeforeSendRequest(req)
}
resp := req.Send()
_, respNoBody, err = resp.PipeBody(w)
return
}
func (p *ResolvedRequestPayload) verifyEndpoint(ctx context.Context, etcdClient *clientv3.Client, pdClient *pd.Client) error {
switch p.api.Component {
case topo.KindTiDB:
infos, err := topology.FetchTiDBTopology(ctx, etcdClient)
if err != nil {
return ErrInvalidEndpoint.Wrap(err, "failed to fetch tidb topology")
}
matched := false
for _, info := range infos {
if info.IP == p.host && info.StatusPort == uint(p.port) {
matched = true
break
}
}
if !matched {
return ErrInvalidEndpoint.New("invalid endpoint '%s:%d'", p.host, p.port)
}
case topo.KindTiKV, topo.KindTiFlash:
tikvInfos, tiflashInfos, err := topology.FetchStoreTopology(pdClient)
if err != nil {
return ErrInvalidEndpoint.Wrap(err, "failed to fetch store topology")
}
matched := false
if p.api.Component == topo.KindTiKV {
for _, info := range tikvInfos {
if info.IP == p.host && info.StatusPort == uint(p.port) {
matched = true
break
}
}
} else {
for _, info := range tiflashInfos {
if info.IP == p.host && info.StatusPort == uint(p.port) {
matched = true
break
}
}
}
if !matched {
return ErrInvalidEndpoint.New("invalid endpoint '%s:%d'", p.host, p.port)
}
case topo.KindPD:
infos, err := topology.FetchPDTopology(pdClient)
if err != nil {
return ErrInvalidEndpoint.Wrap(err, "failed to fetch pd topology")
}
matched := false
for _, info := range infos {
if info.IP == p.host && info.Port == uint(p.port) {
matched = true
break
}
}
if !matched {
return ErrInvalidEndpoint.New("invalid endpoint '%s:%d'", p.host, p.port)
}
case topo.KindTiCDC:
infos, err := topology.FetchTiCDCTopology(ctx, etcdClient)
if err != nil {
return ErrInvalidEndpoint.Wrap(err, "failed to fetch ticdc topology")
}
matched := false
for _, info := range infos {
if info.IP == p.host && info.Port == uint(p.port) {
matched = true
break
}
}
if !matched {
return ErrInvalidEndpoint.New("invalid endpoint '%s:%d'", p.host, p.port)
}
case topo.KindTiProxy:
infos, err := topology.FetchTiProxyTopology(ctx, etcdClient)
if err != nil {
return ErrInvalidEndpoint.Wrap(err, "failed to fetch tiproxy topology")
}
matched := false
for _, info := range infos {
if info.IP == p.host && info.StatusPort == uint(p.port) {
matched = true
break
}
}
if !matched {
return ErrInvalidEndpoint.New("invalid endpoint '%s:%d'", p.host, p.port)
}
case topo.KindTSO:
infos, err := topology.FetchTSOTopology(ctx, pdClient)
if err != nil {
return ErrInvalidEndpoint.Wrap(err, "failed to fetch tso topology")
}
matched := false
for _, info := range infos {
if info.IP == p.host && info.Port == uint(p.port) {
matched = true
break
}
}
if !matched {
return ErrInvalidEndpoint.New("invalid endpoint '%s:%d'", p.host, p.port)
}
case topo.KindScheduling:
infos, err := topology.FetchSchedulingTopology(ctx, pdClient)
if err != nil {
return ErrInvalidEndpoint.Wrap(err, "failed to fetch scheduling topology")
}
matched := false
for _, info := range infos {
if info.IP == p.host && info.Port == uint(p.port) {
matched = true
break
}
}
if !matched {
return ErrInvalidEndpoint.New("invalid endpoint '%s:%d'", p.host, p.port)
}
default:
return ErrUnknownComponent.New("Unknown component '%s'", p.api.Component)
}
return nil
}
================================================
FILE: pkg/apiserver/debugapi/endpoint/payload_test.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package endpoint
import (
"bytes"
"context"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/go-resty/resty/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pingcap/tidb-dashboard/util/client/httpclient"
"github.com/pingcap/tidb-dashboard/util/client/tidbclient"
"github.com/pingcap/tidb-dashboard/util/topo"
)
func TestRequestPayloadResolver(t *testing.T) {
clients := HTTPClients{
TiDBStatusClient: tidbclient.NewStatusClient(httpclient.Config{}),
}
apis := []APIDefinition{
{
ID: "one_pd_api",
Component: topo.KindPD,
Path: "/foo",
Method: resty.MethodGet,
},
{
ID: "one_tidb_api",
Component: topo.KindTiDB,
Path: "/test/{pathParam}",
Method: resty.MethodGet,
PathParams: []APIParamDefinition{
{
Name: "pathParam",
Required: true,
},
},
QueryParams: []APIParamDefinition{
{
Name: "queryParam",
Required: true,
},
{
Name: "queryParam2",
Required: false,
},
},
},
{
ID: "another_tidb_api",
Component: topo.KindTiDB,
Path: "/foo/{regionID}",
Method: resty.MethodGet,
PathParams: []APIParamDefinition{
{
Name: "regionID",
Required: false,
},
},
QueryParams: []APIParamDefinition{
{
Name: "state",
Required: false,
OnResolve: func(value string) ([]string, error) {
if value == "__INVALID__" {
return nil, fmt.Errorf("invalid")
}
return []string{"a" + value + "b"}, nil
},
},
},
},
{
ID: "one_tiflash_api",
Component: topo.KindTiFlash,
Path: "/bar",
Method: resty.MethodGet,
},
}
resolver := NewRequestPayloadResolver(apis, clients)
// APIs without an accepted client will be ignored.
{
apis := resolver.ListAPIs()
require.Len(t, apis, 2)
require.Equal(t, "one_tidb_api", apis[0].ID)
require.Equal(t, "another_tidb_api", apis[1].ID)
}
// Resolve
resolved, err := resolver.ResolvePayload(RequestPayload{
API: "one_tidb_api",
Host: "tidb-1.internal",
Port: 12345,
ParamValues: map[string]string{
"pathParam": "p1",
"queryParam": "q1",
},
})
require.Nil(t, err)
require.Equal(t, resolved.api, &apis[1])
require.Equal(t, "tidb-1.internal", resolved.host)
require.Equal(t, 12345, resolved.port)
require.Equal(t, "/test/p1", resolved.path)
require.Equal(t, url.Values{"queryParam": []string{"q1"}}, resolved.queryValues)
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "another_tidb_api",
Host: "tidb-1.internal",
Port: 12345,
ParamValues: map[string]string{
"regionID": "35",
},
})
require.Nil(t, err)
require.Equal(t, resolved.api, &apis[2])
require.Equal(t, "tidb-1.internal", resolved.host)
require.Equal(t, 12345, resolved.port)
require.Equal(t, "/foo/35", resolved.path)
require.Equal(t, url.Values{}, resolved.queryValues)
// Resolve unknown API endpoint
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "foo",
})
require.NotNil(t, err)
require.Contains(t, err.Error(), "Unknown API endpoint 'foo'")
require.Nil(t, resolved)
// Resolve filtered API endpoint
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "one_pd_api",
})
require.NotNil(t, err)
require.Contains(t, err.Error(), "Unknown API endpoint 'one_pd_api'")
require.Nil(t, resolved)
// Resolve without specifying the path param
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "one_tidb_api",
Host: "tidb-1.internal",
Port: 12345,
ParamValues: map[string]string{
"queryParam": "q1",
},
})
require.NotNil(t, err)
require.Contains(t, err.Error(), "parameter 'pathParam' is required")
require.Nil(t, resolved)
// Resolve without specifying the path param (even if path param is not set to required)
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "another_tidb_api",
Host: "tidb-1.internal",
Port: 12345,
})
require.NotNil(t, err)
require.Contains(t, err.Error(), "parameter 'regionID' is required")
require.Nil(t, resolved)
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "another_tidb_api",
Host: "tidb-1.internal",
Port: 12345,
ParamValues: map[string]string{
"regionID": "",
},
})
require.NotNil(t, err)
require.Contains(t, err.Error(), "parameter 'regionID' is required")
require.Nil(t, resolved)
// Resolve without specifying the required query param
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "one_tidb_api",
Host: "tidb-1.internal",
Port: 12345,
ParamValues: map[string]string{
"pathParam": "p1",
},
})
require.NotNil(t, err)
require.Contains(t, err.Error(), "parameter 'queryParam' is required")
require.Nil(t, resolved)
// Resolve with optional query param
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "one_tidb_api",
Host: "tidb-x.internal",
Port: 5431,
ParamValues: map[string]string{
"pathParam": "abc/def?q=x",
"queryParam": "q",
"queryParam2": "q?foo",
},
})
require.Nil(t, err)
require.Equal(t, resolved.api, &apis[1])
require.Equal(t, "tidb-x.internal", resolved.host)
require.Equal(t, 5431, resolved.port)
require.Equal(t, "/test/abc%2Fdef%3Fq=x", resolved.path)
require.Equal(t, url.Values{"queryParam": []string{"q"}, "queryParam2": []string{"q?foo"}}, resolved.queryValues)
// Resolve empty optional query param
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "another_tidb_api",
Host: "tidb-1.internal",
Port: 12345,
ParamValues: map[string]string{
"regionID": "35",
"state": "",
},
})
require.Nil(t, err)
require.Equal(t, resolved.api, &apis[2])
require.Equal(t, "tidb-1.internal", resolved.host)
require.Equal(t, 12345, resolved.port)
require.Equal(t, "/foo/35", resolved.path)
require.Equal(t, url.Values{}, resolved.queryValues)
// Resolve with invalid query param (OnResolve returns error)
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "another_tidb_api",
Host: "tidb-1.internal",
Port: 12345,
ParamValues: map[string]string{
"regionID": "123",
"state": "__INVALID__",
},
})
require.NotNil(t, err)
require.Contains(t, err.Error(), "parameter 'state' is invalid, cause: invalid")
require.Nil(t, resolved)
// Resolve param with OnResolve returns something
resolved, err = resolver.ResolvePayload(RequestPayload{
API: "another_tidb_api",
Host: "tidb-1.internal",
Port: 12345,
ParamValues: map[string]string{
"regionID": "35",
"state": "v",
},
})
require.Nil(t, err)
require.Equal(t, resolved.api, &apis[2])
require.Equal(t, "tidb-1.internal", resolved.host)
require.Equal(t, 12345, resolved.port)
require.Equal(t, "/foo/35", resolved.path)
require.Equal(t, url.Values{"state": []string{"avb"}}, resolved.queryValues)
}
func TestResolvedRequestPayload(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintln(w, r.URL.String())
_, _ = fmt.Fprintln(w, r.Header.Get("x-test-header"))
}))
defer ts.Close()
addr := ts.Listener.Addr().(*net.TCPAddr)
rp := ResolvedRequestPayload{
api: &APIDefinition{
ID: "api_id",
Component: topo.KindTiDB,
Path: "/does_not_matter",
Method: resty.MethodGet,
BeforeSendRequest: func(req *httpclient.LazyRequest) {
req.SetHeader("x-test-header", "hello")
},
},
host: addr.IP.String(),
port: addr.Port,
path: "/abc",
queryValues: nil,
}
client := tidbclient.NewStatusClient(httpclient.Config{})
clients := HTTPClients{
TiDBStatusClient: client,
}
buf := bytes.Buffer{}
_, err := rp.SendRequestAndPipe(context.Background(), clients, nil, nil, &buf)
assert.Nil(t, err)
assert.Equal(t, "/abc\nhello\n", buf.String())
}
================================================
FILE: pkg/apiserver/debugapi/module.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package debugapi
import "go.uber.org/fx"
var Module = fx.Options(
fx.Provide(newService),
fx.Invoke(registerRouter),
)
================================================
FILE: pkg/apiserver/debugapi/service.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package debugapi
import (
"fmt"
"mime"
"net/http"
"time"
"github.com/gin-gonic/gin"
clientv3 "go.etcd.io/etcd/client/v3"
"go.uber.org/fx"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/debugapi/endpoint"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/pd"
"github.com/pingcap/tidb-dashboard/util/client/pdclient"
"github.com/pingcap/tidb-dashboard/util/client/schedulingclient"
"github.com/pingcap/tidb-dashboard/util/client/ticdcclient"
"github.com/pingcap/tidb-dashboard/util/client/tidbclient"
"github.com/pingcap/tidb-dashboard/util/client/tiflashclient"
"github.com/pingcap/tidb-dashboard/util/client/tikvclient"
"github.com/pingcap/tidb-dashboard/util/client/tiproxyclient"
"github.com/pingcap/tidb-dashboard/util/client/tsoclient"
"github.com/pingcap/tidb-dashboard/util/rest"
"github.com/pingcap/tidb-dashboard/util/rest/fileswap"
)
func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
ep := r.Group("/debug_api")
ep.GET("/download", s.Download)
{
ep.Use(auth.MWAuthRequired())
ep.GET("/endpoints", s.GetEndpoints)
ep.POST("/endpoint", s.RequestEndpoint)
}
}
type ServiceParams struct {
fx.In
PDAPIClient *pdclient.APIClient
TiDBStatusClient *tidbclient.StatusClient
TiKVStatusClient *tikvclient.StatusClient
TiFlashStatusClient *tiflashclient.StatusClient
TiCDCStatusClient *ticdcclient.StatusClient
TiProxyStatusClient *tiproxyclient.StatusClient
EtcdClient *clientv3.Client
PDClient *pd.Client
TSOStatusClient *tsoclient.StatusClient
SchedulingStatusClient *schedulingclient.StatusClient
}
type Service struct {
httpClients endpoint.HTTPClients
etcdClient *clientv3.Client
pdClient *pd.Client
resolver *endpoint.RequestPayloadResolver
fSwap *fileswap.Handler
}
func newService(p ServiceParams) *Service {
httpClients := endpoint.HTTPClients{
PDAPIClient: p.PDAPIClient,
TiDBStatusClient: p.TiDBStatusClient,
TiKVStatusClient: p.TiKVStatusClient,
TiFlashStatusClient: p.TiFlashStatusClient,
TiCDCStatusClient: p.TiCDCStatusClient,
TiProxyStatusClient: p.TiProxyStatusClient,
TSOStatusClient: p.TSOStatusClient,
SchedulingStatusClient: p.SchedulingStatusClient,
}
return &Service{
httpClients: httpClients,
etcdClient: p.EtcdClient,
pdClient: p.PDClient,
resolver: endpoint.NewRequestPayloadResolver(apiEndpoints, httpClients),
fSwap: fileswap.New(),
}
}
func getExtFromContentTypeHeader(contentType string) string {
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil || len(mediaType) == 0 {
return ".bin"
}
// Some explicit overrides
if mediaType == "text/plain" {
return ".txt"
}
if mediaType == "application/toml" {
return ".toml"
}
exts, err := mime.ExtensionsByType(mediaType)
if err == nil && len(exts) > 0 {
// Note: the first element might not be the most common one
return exts[0]
}
return ".bin"
}
// @Summary Send request remote endpoint and return a token for downloading results
// @Security JwtAuth
// @ID debugAPIRequestEndpoint
// @Param req body endpoint.RequestPayload true "request payload"
// @Success 200 {object} string
// @Failure 400 {object} rest.ErrorResponse
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
// @Router /debug_api/endpoint [post]
func (s *Service) RequestEndpoint(c *gin.Context) {
var req endpoint.RequestPayload
if err := c.ShouldBindJSON(&req); err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
resolved, err := s.resolver.ResolvePayload(req)
if err != nil {
rest.Error(c, err)
return
}
writer, err := s.fSwap.NewFileWriter("debug_api")
if err != nil {
rest.Error(c, err)
return
}
defer func() {
_ = writer.Close()
}()
resp, err := resolved.SendRequestAndPipe(c.Request.Context(), s.httpClients, s.etcdClient, s.pdClient, writer)
if err != nil {
rest.Error(c, err)
return
}
ext := getExtFromContentTypeHeader(resp.Header.Get("Content-Type"))
fileName := fmt.Sprintf("%s_%d%s", req.API, time.Now().Unix(), ext)
downloadToken, err := writer.GetDownloadToken(fileName, time.Minute*5)
if err != nil {
// This shall never happen
rest.Error(c, err)
return
}
c.String(http.StatusOK, downloadToken)
}
// @Summary Download a finished request result
// @Param token query string true "download token"
// @Success 200 {object} string
// @Failure 400 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
// @Router /debug_api/download [get]
func (s *Service) Download(c *gin.Context) {
s.fSwap.HandleDownloadRequest(c)
}
// @Summary Get all endpoints
// @ID debugAPIGetEndpoints
// @Security JwtAuth
// @Success 200 {array} endpoint.APIDefinition
// @Failure 401 {object} rest.ErrorResponse
// @Router /debug_api/endpoints [get]
func (s *Service) GetEndpoints(c *gin.Context) {
c.JSON(http.StatusOK, s.resolver.ListAPIs())
}
================================================
FILE: pkg/apiserver/diagnose/compare.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package diagnose
import (
"container/heap"
"fmt"
"math"
"slices"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"github.com/pingcap/errors"
"gorm.io/gorm"
"github.com/pingcap/tidb-dashboard/pkg/dbstore"
)
func GetCompareReportTablesForDisplay(startTime1, endTime1, startTime2, endTime2 string, db *gorm.DB, sqliteDB *dbstore.DB, reportID string) []*TableDef {
errRows := checkBeforeReport(db)
if len(errRows) > 0 {
return []*TableDef{GenerateReportError(errRows)}
}
var resultTables []*TableDef
resultTables = append(resultTables, GetCompareHeaderTimeTable(startTime1, endTime1, startTime2, endTime2))
var tables0, tables1, tables2, tables3, tables4 []*TableDef
var errRows0, errRows1, errRows2, errRows3, errRows4 []TableRowDef
var compareDiagnoseTable, abnormalSlowQuery *TableDef
var wg sync.WaitGroup
wg.Add(7)
var progress, totalTableCount int32
go func() {
// Get Header tables.
tables0, errRows0 = GetReportHeaderTables(startTime2, endTime2, db, sqliteDB, reportID, &progress, &totalTableCount)
errRows = append(errRows, errRows0...)
wg.Done()
}()
go func() {
// Get tables in 2 ranges
tables1, errRows1 = GetReportTablesIn2Range(startTime1, endTime1, startTime2, endTime2, db, sqliteDB, reportID, &progress, &totalTableCount)
errRows = append(errRows, errRows1...)
wg.Done()
}()
go func() {
// Get compare refer tables
tables2, errRows2 = getCompareTables(startTime1, endTime1, db, sqliteDB, reportID, &progress, &totalTableCount)
errRows = append(errRows, errRows2...)
wg.Done()
}()
go func() {
// Get compare tables
tables3, errRows3 = getCompareTables(startTime2, endTime2, db.Session(&gorm.Session{NewDB: true}), sqliteDB, reportID, &progress, &totalTableCount)
errRows = append(errRows, errRows3...)
wg.Done()
}()
go func() {
tbl, errRow := CompareDiagnose(startTime1, endTime1, startTime2, endTime2, db)
if errRow != nil {
errRows = append(errRows, *errRow)
} else {
compareDiagnoseTable = &tbl
}
wg.Done()
}()
go func() {
tbl, errRow := getTiDBAbnormalSlowQueryOnly(startTime1, endTime1, startTime2, endTime2, db)
if errRow != nil {
errRows = append(errRows, *errRow)
} else {
abnormalSlowQuery = &tbl
}
wg.Done()
}()
go func() {
// Get end tables
tables4, errRows4 = GetReportEndTables(startTime2, endTime2, db, sqliteDB, reportID, &progress, &totalTableCount)
errRows = append(errRows, errRows4...)
wg.Done()
}()
wg.Wait()
tables, errs := CompareTables(tables2, tables3)
errRows = append(errRows, errs...)
resultTables = append(resultTables, tables0...)
if abnormalSlowQuery != nil {
resultTables = append(resultTables, abnormalSlowQuery)
}
resultTables = append(resultTables, tables1...)
if compareDiagnoseTable != nil {
resultTables = append(resultTables, compareDiagnoseTable)
}
resultTables = append(resultTables, tables...)
resultTables = append(resultTables, tables4...)
if len(errRows) > 0 {
resultTables = append(resultTables, GenerateReportError(errRows))
}
return resultTables
}
func CompareTables(tables1, tables2 []*TableDef) ([]*TableDef, []TableRowDef) {
var errRows []TableRowDef
dr := &diffRows{}
resultTables := make([]*TableDef, 1, len(tables1))
for _, tbl1 := range tables1 {
for _, tbl2 := range tables2 {
if strings.Join(tbl1.Category, ",") == strings.Join(tbl2.Category, ",") &&
tbl1.Title == tbl2.Title {
table, err := compareTable(tbl1, tbl2, dr)
if err != nil {
errRows = appendErrorRow(*tbl1, err, errRows)
} else if table != nil {
resultTables = append(resultTables, table)
}
}
}
}
resultTables[0] = GenerateDiffTable(*dr)
return resultTables, errRows
}
func GenerateDiffTable(dr diffRows) *TableDef {
l := dr.Len()
sort.Slice(dr, func(i, j int) bool {
abs1 := math.Abs(math.Round(dr[i].ratio*100) / 100)
abs2 := math.Abs(math.Round(dr[j].ratio*100) / 100)
if abs1 != abs2 {
return abs1 > abs2
}
vi1, err1 := parseFloat(dr[i].v1)
vi2, err2 := parseFloat(dr[i].v2)
vj1, err3 := parseFloat(dr[j].v1)
vj2, err4 := parseFloat(dr[j].v2)
if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
// should never be error herr.
return false
}
return math.Abs(vi2-vi1) > math.Abs(vj1-vj2)
})
type groupValue struct {
name string
ratioIdx int
}
rows := make([]TableRowDef, 0, l)
rowMap := make(map[groupValue]int, l)
for i := range dr {
row := dr[i]
name := ""
if labels := strings.Split(row.label, ","); len(labels) > 0 {
name = labels[0]
}
if len(name) == 0 {
continue
}
label := ""
if len(name) < len(row.label) {
label = row.label[len(name)+1:]
}
vs := []string{
row.table,
name,
label,
fmt.Sprintf("%.2f", row.ratio),
row.v1,
row.v2,
row.colName,
}
if idx, ok := rowMap[groupValue{name: name, ratioIdx: row.ratioIdx}]; ok {
var lastValue []string
if len(rows[idx].SubValues) == 0 {
lastValue = rows[idx].Values
} else {
lastIdx := len(rows[idx].SubValues) - 1
lastValue = rows[idx].SubValues[lastIdx]
}
equal := true
for i := 3; i <= 5; i++ {
if vs[i] != lastValue[i] {
equal = false
break
}
}
if !equal {
rows[idx].SubValues = append(rows[idx].SubValues, vs)
}
continue
}
rowMap[groupValue{name: name, ratioIdx: row.ratioIdx}] = len(rows)
rows = append(rows, TableRowDef{
Values: vs,
Comment: row.comment,
})
}
return &TableDef{
Category: []string{CategoryOverview},
Title: "max_diff_item",
Comment: "",
Column: []string{"TABLE", "METRIC_NAME", "LABEL", "MAX_DIFF", "t1.VALUE", "t2.VALUE", "VALUE_TYPE"},
Rows: rows,
}
}
func compareTable(table1, table2 *TableDef, dr *diffRows) (_ *TableDef, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.Errorf("compare table %s ,%s panic", table1.Category, table1.Title)
}
}()
if strings.Contains(strings.ToLower(table1.Title), "config") {
return compareTableWithNonUniqueKey(table1, table2, &diffRows{})
}
labelsMap1, err := getTableLablesMap(table1)
if err != nil {
return nil, err
}
labelsMap2, err := getTableLablesMap(table2)
if err != nil {
return nil, err
}
resultRows := make([]TableRowDef, 0, len(table1.Rows))
for i := range table1.Rows {
label1 := genRowLabel(table1.Rows[i].Values, table1.joinColumns)
row2, ok := labelsMap2[label1]
if !ok {
row2 = &TableRowDef{}
}
newRow, err := joinRow(&table1.Rows[i], row2, table1, dr)
if err != nil {
return nil, err
}
resultRows = append(resultRows, *newRow)
}
for i := range table2.Rows {
label2 := genRowLabel(table2.Rows[i].Values, table2.joinColumns)
_, ok := labelsMap1[label2]
if ok {
continue
}
row1 := &TableRowDef{}
newRow, err := joinRow(row1, &table2.Rows[i], table1, dr)
if err != nil {
return nil, err
}
resultRows = append(resultRows, *newRow)
}
resultTable := &TableDef{
Category: table1.Category,
Title: table1.Title,
Comment: table1.Comment,
joinColumns: nil,
compareColumns: nil,
}
columns := make([]string, 0, len(table1.Column)*2-len(table1.joinColumns))
for i := range table1.Column {
if checkIn(i, table1.joinColumns) {
columns = append(columns, table1.Column[i])
} else {
columns = append(columns, "t1."+table1.Column[i])
}
}
for i := range table2.Column {
if !checkIn(i, table2.joinColumns) {
columns = append(columns, "t2."+table2.Column[i])
}
}
for _, idx := range table1.compareColumns {
columns = append(columns, table1.Column[idx]+"_DIFF_RATIO")
}
sort.Slice(resultRows, func(i, j int) bool {
return math.Abs(resultRows[i].ratio) > math.Abs(resultRows[j].ratio)
})
if len(table1.compareColumns) > 0 {
for _, idx := range table1.compareColumns {
comment := table1.Column[idx] + "_DIFF_RATIO=" + fmt.Sprintf("if t2.%[1]s > t1.%[1]s => { t2.%[1]s / t1.%[1]s - 1 } else => { 1 - t1.%[1]s / t2.%[1]s }", table1.Column[idx])
if len(resultTable.Comment) > 0 {
resultTable.Comment += ", \n"
}
resultTable.Comment += comment
}
}
resultTable.Column = columns
resultTable.Rows = resultRows
return resultTable, nil
}
func compareTableWithNonUniqueKey(table1, table2 *TableDef, dr *diffRows) (_ *TableDef, err error) {
defer func() {
defer func() {
if v := recover(); v != nil {
err = errors.Errorf("join table error %v,%v", table1.Category, table1.Title)
}
}()
}()
labelsMap1, err := getTableLablesMapWithNonUniqueJoinKey(table1)
if err != nil {
return nil, err
}
labelsMap2, err := getTableLablesMapWithNonUniqueJoinKey(table2)
if err != nil {
return nil, err
}
resultRows := make([]TableRowDef, 0, len(table1.Rows))
for i := range table1.Rows {
label1 := genRowLabel(table1.Rows[i].Values, table1.joinColumns)
var row2 *TableRowDef
if row2s, ok := labelsMap2[label1]; ok && len(row2s) > 0 {
row2 = row2s[0]
if len(row2s) == 1 {
delete(labelsMap2, label1)
} else {
labelsMap2[label1] = row2s[1:]
}
} else {
delete(labelsMap2, label1)
row2 = &TableRowDef{}
}
if row1s, ok := labelsMap1[label1]; ok {
if len(row1s) <= 1 {
delete(labelsMap1, label1)
} else {
labelsMap1[label1] = row1s[1:]
}
}
newRow, err := joinRow(&table1.Rows[i], row2, table1, dr)
if err != nil {
return nil, err
}
resultRows = append(resultRows, *newRow)
}
for len(labelsMap2) > 0 {
for label, row2s := range labelsMap2 {
if len(row2s) == 0 {
delete(labelsMap2, label)
continue
}
row2 := row2s[0]
if len(row2s) == 1 {
delete(labelsMap2, label)
} else {
labelsMap2[label] = row2s[1:]
}
var row1 *TableRowDef
if row1s, ok := labelsMap1[label]; ok && len(row1s) > 0 {
row1 = row1s[0]
if len(row1s) == 0 {
delete(labelsMap1, label)
} else {
labelsMap1[label] = row1s[1:]
}
} else {
delete(labelsMap1, label)
row1 = &TableRowDef{}
}
newRow, err := joinRow(row1, row2, table1, dr)
if err != nil {
return nil, err
}
resultRows = append(resultRows, *newRow)
}
}
resultTable := &TableDef{
Category: table1.Category,
Title: table1.Title,
Comment: table1.Comment,
joinColumns: nil,
compareColumns: nil,
}
columns := make([]string, 0, len(table1.Column)*2-len(table1.joinColumns))
for i := range table1.Column {
if checkIn(i, table1.joinColumns) {
columns = append(columns, table1.Column[i])
} else {
columns = append(columns, "t1."+table1.Column[i])
}
}
for i := range table2.Column {
if !checkIn(i, table2.joinColumns) {
columns = append(columns, "t2."+table2.Column[i])
}
}
for _, idx := range table1.compareColumns {
columns = append(columns, table1.Column[idx]+"_DIFF_RATIO")
}
sort.Slice(resultRows, func(i, j int) bool {
if len(table1.joinColumns) > 0 {
idx := table1.joinColumns[0]
if len(resultRows[i].Values) > (idx+1) &&
len(resultRows[j].Values) > (idx+1) {
if resultRows[i].Values[idx] != resultRows[j].Values[idx] {
return resultRows[i].Values[idx] < resultRows[j].Values[idx]
}
return resultRows[i].Values[0] < resultRows[j].Values[0]
}
}
return false
})
for _, idx := range table1.compareColumns {
comment := table1.Column[idx] + "_DIFF_RATIO=" + fmt.Sprintf("(t2.%[1]s-t1.%[1]s)/max(t2.%[1]s, t1.%[1]s)", table1.Column[idx])
if len(resultTable.Comment) > 0 {
resultTable.Comment += ", \n"
}
resultTable.Comment += comment
}
resultTable.Column = columns
resultTable.Rows = resultRows
return resultTable, nil
}
func getTableLablesMapWithNonUniqueJoinKey(table *TableDef) (map[string][]*TableRowDef, error) {
if len(table.joinColumns) == 0 {
return nil, errors.Errorf("category %v,table %v doesn't have join columns", strings.Join(table.Category, ","), table.Title)
}
labelsMap := make(map[string][]*TableRowDef, len(table.Rows))
for i := range table.Rows {
label := genRowLabel(table.Rows[i].Values, table.joinColumns)
labelsMap[label] = append(labelsMap[label], &table.Rows[i])
}
return labelsMap, nil
}
func joinRow(row1, row2 *TableRowDef, table *TableDef, dr *diffRows) (*TableRowDef, error) {
rowsMap1, err := genRowsLablesMap(table, row1.SubValues)
if err != nil {
return nil, err
}
rowsMap2, err := genRowsLablesMap(table, row2.SubValues)
if err != nil {
return nil, err
}
subJoinRows := make([]*newJoinRow, 0, len(row1.SubValues))
for _, subRow1 := range row1.SubValues {
label := genRowLabel(subRow1, table.joinColumns)
subRow2 := rowsMap2[label]
ratio, ratios, idx, err := calculateDiffRatio(subRow1, subRow2, table)
if err != nil {
return nil, errors.Errorf("category %v,table %v, calculate diff ratio error: %v, %v,%v", strings.Join(table.Category, ","), table.Title, err.Error(), subRow1, subRow2)
}
subJoinRows = append(subJoinRows, &newJoinRow{
row1: subRow1,
row2: subRow2,
ratio: ratio,
ratios: ratios,
})
dr.addRow(table, label, ratio, subRow1, subRow2, idx, row1.Comment)
}
for _, subRow2 := range row2.SubValues {
label := genRowLabel(subRow2, table.joinColumns)
subRow1, ok := rowsMap1[label]
if ok {
continue
}
ratio, ratios, idx, err := calculateDiffRatio(subRow1, subRow2, table)
if err != nil {
return nil, errors.Errorf("category %v,table %v, calculate diff ratio error: %v, %v,%v", strings.Join(table.Category, ","), table.Title, err.Error(), subRow1, subRow2)
}
subJoinRows = append(subJoinRows, &newJoinRow{
row1: subRow1,
row2: subRow2,
ratio: ratio,
ratios: ratios,
})
dr.addRow(table, label, ratio, subRow1, subRow2, idx, row2.Comment)
}
sort.Slice(subJoinRows, func(i, j int) bool {
return math.Abs(subJoinRows[i].ratio) > math.Abs(subJoinRows[j].ratio)
})
totalRatio := float64(0)
var totalRatios []float64
resultSubRows := make([][]string, 0, len(row1.SubValues))
for _, r := range subJoinRows {
resultSubRows = append(resultSubRows, r.genNewRow(table))
}
totalRatioIdx := -1
if len(row1.Values) != len(row2.Values) {
totalRatio = 1
totalRatios = nil
} else {
totalRatio, totalRatios, totalRatioIdx, err = calculateDiffRatio(row1.Values, row2.Values, table)
if err != nil {
return nil, errors.Errorf("category %v,table %v, calculate diff ratio error: %v, %v,%v", strings.Join(table.Category, ","), table.Title, err.Error(), row1.Values, row2.Values)
}
}
if len(row1.SubValues) == 0 && len(row2.SubValues) == 0 {
label := ""
if len(row1.Values) >= len(table.Column) {
label = genRowLabel(row1.Values, table.joinColumns)
} else if len(row2.Values) >= len(table.Column) {
label = genRowLabel(row2.Values, table.joinColumns)
}
dr.addRow(table, label, totalRatio, row1.Values, row2.Values, totalRatioIdx, row1.Comment)
}
resultJoinRow := newJoinRow{
row1: row1.Values,
row2: row2.Values,
ratio: totalRatio,
ratios: totalRatios,
}
resultRow := &TableRowDef{
Values: resultJoinRow.genNewRow(table),
SubValues: resultSubRows,
ratio: totalRatio,
Comment: row1.Comment,
}
return resultRow, nil
}
type diffRow struct {
table string
label string
ratio float64
ratioIdx int
colName string
v1 string
v2 string
comment string
}
type diffRows []diffRow
func (r diffRows) Len() int { return len(r) }
func (r diffRows) Less(i, j int) bool { return math.Abs(r[i].ratio) < math.Abs(r[j].ratio) }
func (r diffRows) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r *diffRows) Push(x interface{}) {
*r = append(*r, x.(diffRow))
}
func (r *diffRows) Pop() interface{} {
old := *r
n := len(old)
x := old[n-1]
*r = old[0 : n-1]
return x
}
func (r *diffRows) addRow(table *TableDef, label string, ratio float64, vs1, vs2 []string, idx int, comment string) {
if ratio == 0 {
return
}
tableName := strings.Join(table.Category, "-") + ", " + table.Title
colName := ""
if idx > 0 && idx < len(table.Column) {
comment = comment + ", the value is " + table.Column[idx]
colName = table.Column[idx]
}
v1 := ""
v2 := ""
if idx >= 0 {
if idx < len(vs1) {
v1 = vs1[idx]
}
if idx < len(vs2) {
v2 = vs2[idx]
}
}
r.appendRow(diffRow{
table: tableName,
label: label,
ratio: ratio,
ratioIdx: idx,
colName: colName,
v1: v1,
v2: v2,
comment: comment,
})
}
func (r *diffRows) appendRow(row diffRow) {
heap.Push(r, row)
if r.Len() > 500 {
heap.Pop(r)
}
}
type newJoinRow struct {
row1 []string
row2 []string
ratio float64
ratios []float64
}
func (r *newJoinRow) genNewRow(table *TableDef) []string {
newRow := make([]string, 0, len(r.row1)+len(r.row2))
if len(r.row1) == 0 {
newRow = append(newRow, make([]string, len(r.row2))...)
for i := range r.row2 {
if checkIn(i, table.joinColumns) {
newRow[i] = r.row2[i]
} else {
newRow = append(newRow, r.row2[i])
}
}
for i := range table.compareColumns {
if len(r.ratios) > i {
newRow = append(newRow, convertFloatToString(r.ratios[i]))
} else {
newRow = append(newRow, convertFloatToString(r.ratio))
}
}
return newRow
}
newRow = append(newRow, r.row1...)
if len(r.row2) == 0 {
newRow = append(newRow, make([]string, len(r.row1)-len(table.joinColumns))...)
for i := range table.compareColumns {
if len(r.ratios) > i {
newRow = append(newRow, convertFloatToString(r.ratios[i]))
} else {
newRow = append(newRow, convertFloatToString(r.ratio))
}
}
return newRow
}
for i := range r.row2 {
if !checkIn(i, table.joinColumns) {
newRow = append(newRow, r.row2[i])
}
}
for i := range table.compareColumns {
if len(r.ratios) > i {
newRow = append(newRow, convertFloatToString(r.ratios[i]))
} else {
newRow = append(newRow, convertFloatToString(r.ratio))
}
}
return newRow
}
func calculateDiffRatio(row1, row2 []string, table *TableDef) (float64, []float64, int, error) {
if len(table.compareColumns) == 0 {
return 0, nil, -1, nil
}
if len(row1) == 0 && len(row2) == 0 {
return 0, nil, -1, nil
}
ratios := make([]float64, 0, len(table.compareColumns))
maxRatio := float64(0)
needBetter := false
maxIdx := -1
for _, idx := range table.compareColumns {
var f1, f2 float64
var err error
if idx < len(row1) {
f1, err = parseFloat(row1[idx])
if err != nil {
return 0, nil, -1, err
}
}
if idx < len(row2) {
f2, err = parseFloat(row2[idx])
if err != nil {
return 0, nil, -1, err
}
}
if f1 == f2 {
ratios = append(ratios, 0)
continue
}
ratio := float64(0)
if f1 == 0 {
ratio = f2
} else if f2 == 0 {
ratio = 0 - f1
} else if f2 > f1 {
ratio = f2/f1 - 1
} else {
ratio = 1 - f1/f2
}
ratios = append(ratios, ratio)
if (f1 == 0 || f2 == 0) && maxRatio != 0 && !needBetter {
continue
}
if math.Abs(ratio) > math.Abs(maxRatio) || (needBetter && f1 != 0 && f2 != 0) {
maxRatio = ratio
needBetter = f1 == 0 || f2 == 0
maxIdx = idx
}
}
return maxRatio, ratios, maxIdx, nil
}
func parseFloat(s string) (float64, error) {
if len(s) == 0 {
return float64(0), nil
}
cases := []struct {
suffix string
ratio float64
}{
{" GB", float64(1024 * 1024 * 1024)},
{" MB", float64(1024 * 1024)},
{" KB", float64(1024)},
{"%", float64(1)},
{" s", float64(1)},
{" ms", float64(1) / float64(1000)},
{" us", float64(1) / float64(10e5)},
}
ratio := float64(1)
for _, c := range cases {
if strings.HasSuffix(s, c.suffix) {
ratio = c.ratio
s = s[:len(s)-len(c.suffix)]
break
}
}
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, err
}
return f * ratio, nil
}
func checkIn(idx int, idxs []int) bool {
return slices.Contains(idxs, idx)
}
func genRowLabel(row []string, joinColumns []int) string {
var label strings.Builder
for i, idx := range joinColumns {
if i > 0 {
label.WriteString(",")
}
label.WriteString(row[idx])
}
return label.String()
}
func genRowsLablesMap(table *TableDef, rows [][]string) (map[string][]string, error) {
labelsMap := make(map[string][]string, len(rows))
for i := range rows {
label := genRowLabel(rows[i], table.joinColumns)
_, ok := labelsMap[label]
if ok {
return nil, errors.Errorf("category %v,table %v has duplicate join label: %v", strings.Join(table.Category, ","), table.Title, label)
}
labelsMap[label] = rows[i]
}
return labelsMap, nil
}
func getTableLablesMap(table *TableDef) (map[string]*TableRowDef, error) {
if len(table.joinColumns) == 0 {
return nil, errors.Errorf("category %v,table %v doesn't have join columns", strings.Join(table.Category, ","), table.Title)
}
labelsMap := make(map[string]*TableRowDef, len(table.Rows))
for i := range table.Rows {
label := genRowLabel(table.Rows[i].Values, table.joinColumns)
_, ok := labelsMap[label]
if ok {
return nil, errors.Errorf("category %v,table %v has duplicate join label: %v", strings.Join(table.Category, ","), table.Title, label)
}
labelsMap[label] = &table.Rows[i]
}
return labelsMap, nil
}
func getCompareTables(startTime, endTime string, db *gorm.DB, sqliteDB *dbstore.DB, reportID string, progress, totalTableCount *int32) ([]*TableDef, []TableRowDef) {
funcs := []getTableFunc{
// Node
GetLoadTable,
GetCPUUsageTable,
GetTiKVThreadCPUTable,
GetGoroutinesCountTable,
GetProcessMemUsageTable,
// Overview
GetTotalTimeConsumeTable,
GetTotalErrorTable,
// TiDB
GetTiDBTimeConsumeTable,
GetTiDBConnectionCountTable,
GetTiDBTxnTableData,
GetTiDBStatisticsInfo,
GetTiDBDDLOwner,
// PD
GetPDTimeConsumeTable,
GetPDSchedulerInfo,
GetPDClusterStatusTable,
GetStoreStatusTable,
GetPDEtcdStatusTable,
// TiKV
GetTiKVTotalTimeConsumeTable,
GetTiKVRocksDBTimeConsumeTable,
GetTiKVErrorTable,
GetTiKVStoreInfo,
GetTiKVRegionSizeInfo,
GetTiKVCopInfo,
GetTiKVSchedulerInfo,
GetTiKVRaftInfo,
GetTiKVSnapshotInfo,
GetTiKVGCInfo,
GetTiKVTaskInfo,
GetTiKVCacheHitTable,
// Config
GetPDConfigInfo,
GetPDConfigChangeInfo,
GetTiDBGCConfigInfo,
GetTiDBGCConfigChangeInfo,
GetTiKVRocksDBConfigInfo,
GetTiKVRocksDBConfigChangeInfo,
GetTiKVRaftStoreConfigInfo,
GetTiKVRaftStoreConfigChangeInfo,
}
atomic.AddInt32(totalTableCount, int32(len(funcs)))
return getTablesParallel(startTime, endTime, db, funcs, sqliteDB, reportID, progress, totalTableCount)
}
func GetReportHeaderTables(startTime, endTime string, db *gorm.DB, sqliteDB *dbstore.DB, reportID string, progress, totalTableCount *int32) ([]*TableDef, []TableRowDef) {
funcs := []func(string, string, *gorm.DB) (TableDef, error){
// Header
GetClusterHardwareInfoTable,
GetClusterInfoTable,
}
atomic.AddInt32(totalTableCount, int32(len(funcs)))
return getTablesParallel(startTime, endTime, db, funcs, sqliteDB, reportID, progress, totalTableCount)
}
func GetReportEndTables(startTime, endTime string, db *gorm.DB, sqliteDB *dbstore.DB, reportID string, progress, totalTableCount *int32) ([]*TableDef, []TableRowDef) {
funcs := []func(string, string, *gorm.DB) (TableDef, error){
GetTiDBCurrentConfig,
GetPDCurrentConfig,
GetTiKVCurrentConfig,
}
atomic.AddInt32(totalTableCount, int32(len(funcs)))
return getTablesParallel(startTime, endTime, db, funcs, sqliteDB, reportID, progress, totalTableCount)
}
func GetCompareHeaderTimeTable(startTime1, endTime1, startTime2, endTime2 string) *TableDef {
return &TableDef{
Category: []string{CategoryHeader},
Title: "compare_report_time_range",
Comment: "",
Column: []string{"T1.START_TIME", "T1.END_TIME", "T2.START_TIME", "T2.END_TIME"},
Rows: []TableRowDef{
{Values: []string{startTime1, endTime1, startTime2, endTime2}},
},
}
}
func GetReportTablesIn2Range(startTime1, endTime1, startTime2, endTime2 string, db *gorm.DB, sqliteDB *dbstore.DB, reportID string, progress, totalTableCount *int32) ([]*TableDef, []TableRowDef) {
funcs := []func(string, string, *gorm.DB) (TableDef, error){
// TiDB
GetTiDBTopNSlowQuery,
GetTiDBTopNSlowQueryGroupByDigest,
GetTiDBSlowQueryWithDiffPlan,
// Diagnose
GetAllDiagnoseReport,
}
atomic.AddInt32(totalTableCount, int32(len(funcs)*2))
tables := make([]*TableDef, 0, len(funcs))
var errRows []TableRowDef
var tables1, tables2 []*TableDef
var errRows1, errRows2 []TableRowDef
var wg sync.WaitGroup
wg.Add(2)
go func() {
tables1, errRows1 = getTablesParallel(startTime1, endTime1, db, funcs, sqliteDB, reportID, progress, totalTableCount)
errRows = append(errRows, errRows1...)
for _, tbl := range tables1 {
if tbl.Rows != nil {
tbl.Title += "_in_time_range_t1"
}
}
wg.Done()
}()
go func() {
tables2, errRows2 = getTablesParallel(startTime2, endTime2, db, funcs, sqliteDB, reportID, progress, totalTableCount)
errRows = append(errRows, errRows2...)
for _, tbl := range tables2 {
if tbl.Rows != nil {
tbl.Title += "_in_time_range_t2"
}
}
wg.Done()
}()
wg.Wait()
for len(tables1) > 0 && len(tables2) > 0 {
tables = append(tables, tables1[0])
tables = append(tables, tables2[0])
tables1 = tables1[1:]
tables2 = tables2[1:]
}
tables = append(tables, tables1...)
tables = append(tables, tables2...)
return tables, errRows
}
func appendErrorRow(tbl TableDef, err error, errRows []TableRowDef) []TableRowDef {
if err == nil {
return errRows
}
category := ""
if tbl.Category != nil {
category = strings.Join(tbl.Category, ",")
}
errRows = append(errRows, TableRowDef{Values: []string{category, tbl.Title, err.Error()}})
return errRows
}
func getTiDBAbnormalSlowQueryOnly(startTime1, endTime1, startTime2, endTime2 string, db *gorm.DB) (TableDef, *TableRowDef) {
sql := fmt.Sprintf(`select * from
(select /*+ agg_to_cop(), hash_agg() */ count(*),
min(time),
sum(query_time) as sum_query_time,
sum(process_time) as sum_process_time,
sum(wait_time) as sum_wait_time,
sum(commit_time),
sum(request_count),
sum(process_keys),
sum(write_keys),
max(cop_proc_max),
min(query),min(prev_stmt),
digest
from information_schema.cluster_slow_query
where time >= '%s'
and time < '%s'
and is_internal = false
group by digest) as t1
where t1.digest not in
(select /*+ agg_to_cop(), hash_agg() */ digest
from information_schema.cluster_slow_query
where time >= '%s'
and time < '%s'
group by digest)
order by t1.sum_query_time desc limit 10`, startTime2, endTime2, startTime1, endTime1)
table := TableDef{
Category: []string{CategoryTiDB},
Title: "slow_query_t2",
Comment: sql,
Column: []string{"count(*)", "min(time)", "sum(query_time)", "sum(Process_time)", "sum(Wait_time)", "sum(Commit_time)", "sum(Request_count)", "sum(process_keys)", "sum(Write_keys)", "max(Cop_proc_max)", "min(query)", "min(prev_stmt)", "digest"},
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, &TableRowDef{Values: []string{strings.Join(table.Category, ","), table.Title, err.Error()}}
}
table.Rows = useSubRowForLongColumnValue(rows, len(table.Column)-3)
return table, nil
}
func useSubRowForLongColumnValue(rows []TableRowDef, colIdx int) []TableRowDef {
maxLen := 100
for i := range rows {
row := rows[i]
if len(row.Values) <= colIdx {
continue
}
if len(row.Values[colIdx]) > maxLen {
subRow := make([]string, len(row.Values))
subRow[colIdx] = row.Values[colIdx]
rows[i].Values[colIdx] = row.Values[colIdx][:100]
rows[i].SubValues = append(rows[i].SubValues, subRow)
}
}
return rows
}
================================================
FILE: pkg/apiserver/diagnose/diagnose.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package diagnose
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/goccy/go-graphviz"
"github.com/pingcap/log"
"go.uber.org/zap"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap/tidb-dashboard/pkg/config"
"github.com/pingcap/tidb-dashboard/pkg/dbstore"
"github.com/pingcap/tidb-dashboard/pkg/tidb"
"github.com/pingcap/tidb-dashboard/pkg/uiserver"
"github.com/pingcap/tidb-dashboard/util/rest"
)
const (
timeLayout = "2006-01-02 15:04:05"
)
var graphvizMutex sync.Mutex
type Service struct {
// FIXME: Use fx.In
config *config.Config
db *dbstore.DB
tidbClient *tidb.Client
fileServer http.Handler
}
func NewService(config *config.Config, tidbClient *tidb.Client, db *dbstore.DB, uiAssetFS http.FileSystem) *Service {
err := autoMigrate(db)
if err != nil {
log.Fatal("Failed to initialize database", zap.Error(err))
}
return &Service{
config: config,
db: db,
tidbClient: tidbClient,
fileServer: uiserver.Handler(uiAssetFS),
}
}
func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/diagnose")
endpoint.GET("/reports",
auth.MWAuthRequired(),
s.reportsHandler)
endpoint.POST("/reports",
auth.MWAuthRequired(),
utils.MWConnectTiDB(s.tidbClient),
s.genReportHandler)
endpoint.GET("/reports/:id/detail", s.reportHTMLHandler)
endpoint.GET("/reports/:id/data.js", s.reportDataHandler)
endpoint.GET("/reports/:id/status",
auth.MWAuthRequired(),
s.reportStatusHandler)
endpoint.POST("/metrics_relation/generate", auth.MWAuthRequired(), s.metricsRelationHandler)
endpoint.GET("/metrics_relation/view", s.metricsRelationViewHandler)
endpoint.POST("/diagnosis",
auth.MWAuthRequired(),
utils.MWConnectTiDB((s.tidbClient)),
s.genDiagnosisHandler)
}
func (s *Service) generateMetricsRelation(startTime, endTime time.Time, graphType string) (string, error) {
params := url.Values{}
params.Add("start", startTime.Format(time.RFC3339))
params.Add("end", endTime.Format(time.RFC3339))
params.Add("type", graphType)
encodedParams := params.Encode()
data, err := s.tidbClient.SendGetRequest("/metrics/profile?" + encodedParams)
if err != nil {
return "", err
}
file, err := os.CreateTemp("", "metrics*.svg")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %v", err)
}
_ = file.Close()
g := graphviz.New()
// FIXME: should share a global mutex for profiling.
graphvizMutex.Lock()
defer graphvizMutex.Unlock()
graph, err := graphviz.ParseBytes(data)
if err != nil {
_ = os.Remove(file.Name())
return "", fmt.Errorf("failed to parse DOT file: %v", err)
}
if err := g.RenderFilename(graph, graphviz.SVG, file.Name()); err != nil {
_ = os.Remove(file.Name())
return "", fmt.Errorf("failed to render SVG: %v", err)
}
return file.Name(), nil
}
type GenerateMetricsRelationRequest struct {
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
Type string `json:"type"`
}
// @Id diagnoseGenerateMetricsRelationship
// @Summary Generate metrics relationship graph.
// @Param request body GenerateMetricsRelationRequest true "Request body"
// @Router /diagnose/metrics_relation/generate [post]
// @Success 200 {string} string
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) metricsRelationHandler(c *gin.Context) {
var req GenerateMetricsRelationRequest
if err := c.ShouldBindJSON(&req); err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
startTime := time.Unix(req.StartTime, 0)
endTime := time.Unix(req.EndTime, 0)
path, err := s.generateMetricsRelation(startTime, endTime, req.Type)
if err != nil {
rest.Error(c, err)
return
}
token, err := utils.NewJWTString("diagnose/metrics", path)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, token)
}
// @Summary View metrics relationship graph.
// @Produce image/svg
// @Param token query string true "token"
// @Failure 400 {object} rest.ErrorResponse
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
// @Router /diagnose/metrics_relation/view [get]
func (s *Service) metricsRelationViewHandler(c *gin.Context) {
token := c.Query("token")
path, err := utils.ParseJWTString("diagnose/metrics", token)
if err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
data, err := os.ReadFile(filepath.Clean(path))
if err != nil {
rest.Error(c, err)
return
}
// Do not remove it? Otherwise the link will just expire..
// _ = os.Remove(path)
c.Data(http.StatusOK, "image/svg+xml", data)
}
type GenerateReportRequest struct {
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
CompareStartTime int64 `json:"compare_start_time"`
CompareEndTime int64 `json:"compare_end_time"`
}
// @Summary SQL diagnosis reports history
// @Description Get sql diagnosis reports history
// @Success 200 {array} Report
// @Router /diagnose/reports [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) reportsHandler(c *gin.Context) {
reports, err := GetReports(s.db)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, reports)
}
// @Summary SQL diagnosis report
// @Description Generate sql diagnosis report
// @Param request body GenerateReportRequest true "Request body"
// @Success 200 {object} int
// @Router /diagnose/reports [post]
// @Security JwtAuth
// @Failure 400 {object} rest.ErrorResponse
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) genReportHandler(c *gin.Context) {
var req GenerateReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
startTime := time.Unix(req.StartTime, 0)
endTime := time.Unix(req.EndTime, 0)
var compareStartTime, compareEndTime *time.Time
if req.CompareStartTime != 0 && req.CompareEndTime != 0 {
compareStartTime = new(time.Time)
compareEndTime = new(time.Time)
*compareStartTime = time.Unix(req.CompareStartTime, 0)
*compareEndTime = time.Unix(req.CompareEndTime, 0)
}
reportID, err := NewReport(s.db, startTime, endTime, compareStartTime, compareEndTime)
if err != nil {
rest.Error(c, err)
return
}
db := utils.TakeTiDBConnection(c)
go func() {
defer utils.CloseTiDBConnection(db) //nolint:errcheck
var tables []*TableDef
if compareStartTime == nil || compareEndTime == nil {
tables = GetReportTablesForDisplay(startTime.Format(timeLayout), endTime.Format(timeLayout), db, s.db, reportID)
} else {
tables = GetCompareReportTablesForDisplay(
compareStartTime.Format(timeLayout), compareEndTime.Format(timeLayout),
startTime.Format(timeLayout), endTime.Format(timeLayout),
db, s.db, reportID)
}
_ = UpdateReportProgress(s.db, reportID, 100)
content, err := json.Marshal(tables)
if err == nil {
_ = SaveReportContent(s.db, reportID, string(content))
}
}()
c.JSON(http.StatusOK, reportID)
}
// @Summary Diagnosis report status
// @Description Get diagnosis report status
// @Param id path string true "report id"
// @Success 200 {object} Report
// @Router /diagnose/reports/{id}/status [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) reportStatusHandler(c *gin.Context) {
id := c.Param("id")
report, err := GetReport(s.db, id)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, &report)
}
// @Summary SQL diagnosis report
// @Description Get sql diagnosis report HTML
// @Produce html
// @Param id path string true "report id"
// @Success 200 {string} string
// @Router /diagnose/reports/{id}/detail [get]
func (s *Service) reportHTMLHandler(c *gin.Context) {
defer func(old string) {
c.Request.URL.Path = old
}(c.Request.URL.Path)
c.Request.URL.Path = "diagnoseReport.html"
s.fileServer.ServeHTTP(c.Writer, c.Request)
}
// @Summary SQL diagnosis report data
// @Description Get sql diagnosis report data
// @Produce text/javascript
// @Param id path string true "report id"
// @Success 200 {string} string
// @Router /diagnose/reports/{id}/data.js [get]
func (s *Service) reportDataHandler(c *gin.Context) {
id := c.Param("id")
report, err := GetReport(s.db, id)
if err != nil {
rest.Error(c, err)
return
}
data := "window.__diagnosis_data__ = " + report.Content
c.Data(http.StatusOK, "text/javascript", []byte(data))
}
type GenDiagnosisReportRequest struct {
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
Kind string `json:"kind"` // values: config, error, performance
}
// @Summary SQL diagnosis report
// @Description Generate sql diagnosis report
// @Produce json
// @Param request body GenDiagnosisReportRequest true "Request body"
// @Success 200 {object} TableDef
// @Router /diagnose/diagnosis [post]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) genDiagnosisHandler(c *gin.Context) {
var req GenDiagnosisReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
rest.Error(c, rest.ErrBadRequest.WrapWithNoMessage(err))
return
}
startTime := time.Unix(req.StartTime, 0)
endTime := time.Unix(req.EndTime, 0)
var rules []string
switch req.Kind {
case "config":
rules = []string{"config", "version"}
case "error":
rules = []string{"critical-error"}
case "performance":
rules = []string{"node-load", "threshold-check"}
}
db := utils.TakeTiDBConnection(c)
defer utils.CloseTiDBConnection(db) //nolint:errcheck
table, err := GetDiagnoseReport(startTime.Format(timeLayout), endTime.Format(timeLayout), db, rules)
if err != nil {
tableErr := TableRowDef{Values: []string{CategoryDiagnose, "diagnose", err.Error()}}
table = *GenerateReportError([]TableRowDef{tableErr})
}
c.JSON(http.StatusOK, table)
}
================================================
FILE: pkg/apiserver/diagnose/inspection.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package diagnose
import (
"fmt"
"math"
"strconv"
"strings"
"gorm.io/gorm"
)
type clusterInspection struct {
referStartTime string
referEndTime string
startTime string
endTime string
db *gorm.DB
}
func CompareDiagnose(referStartTime, referEndTime, startTime, endTime string, db *gorm.DB) (TableDef, *TableRowDef) {
c := &clusterInspection{
referStartTime: referStartTime,
referEndTime: referEndTime,
startTime: startTime,
endTime: endTime,
db: db,
}
table := TableDef{
Category: []string{CategoryDiagnose},
Title: "compare_diagnose",
Comment: "",
Column: []string{"RULE", "DETAIL"},
}
details, err := c.inspectForAffectByBigQuery()
if err != nil {
return table, &TableRowDef{Values: []string{strings.Join(table.Category, ","), table.Title, err.Error()}}
}
if len(details) > 0 {
subRows := make([][]string, 0, len(details))
for i := range details {
subRows = append(subRows, []string{"", details[i]})
}
row := TableRowDef{
Values: []string{
"big-query",
"may have big query in diagnose time range",
},
SubValues: subRows,
Comment: "diagnose for big query/write that affect the qps or duration",
}
table.Rows = []TableRowDef{row}
}
return table, nil
}
func (c *clusterInspection) inspectForAffectByBigQuery() ([]string, error) {
checks := []struct {
query metricQuery
ct compareType
threshold float64
}{
{
query: &queryQPS{
baseQuery: baseQuery{
table: "tidb_qps",
labels: []string{"instance"},
},
},
ct: compareLT,
threshold: 0.95,
},
{
query: &queryQuantile{
baseQuery: baseQuery{
table: "tidb_query_duration",
labels: []string{"instance"},
condition: "value is not null and quantile=0.999",
},
},
ct: compareGT,
threshold: 1.2,
},
}
otherInfoChecks := []struct {
query metricQuery
ct compareType
threshold float64
}{
{
query: &queryQuantile{
baseQuery: baseQuery{
table: "tidb_cop_duration",
labels: []string{"instance"},
condition: "value is not null and quantile=0.999",
},
},
ct: compareGT,
threshold: 2,
},
{
// Check for big write transaction
query: &queryQuantile{
baseQuery: baseQuery{
table: "tidb_kv_write_num",
labels: []string{"instance"},
condition: "value is not null and quantile=0.999",
},
},
ct: compareGT,
threshold: 2,
},
{
query: &queryTotal{
baseQuery: baseQuery{
table: "tikv_cop_scan_keys_total_num",
labels: []string{"instance"},
},
},
ct: compareGT,
threshold: 2.0,
},
{
// Check for tikv storage handle time
query: &queryQuantile{
baseQuery: baseQuery{
table: "tikv_storage_async_request_duration",
labels: []string{"instance", "type"},
condition: "value is not null and quantile=0.999",
},
},
ct: compareGT,
threshold: 2,
},
{
query: &queryTotal{
baseQuery: baseQuery{
table: "pd_operator_step_finish_total_count",
labels: []string{"type"},
},
},
ct: compareGT,
threshold: 1.0,
},
}
totalDiffs := make([]metricDiff, 0, len(checks))
for _, ck := range checks {
err := c.compareMetric(ck.query)
if err != nil {
return nil, err
}
partDiffs := ck.query.compare()
partDiffs = checkDiffs(partDiffs, ck.ct, ck.threshold)
totalDiffs = append(totalDiffs, partDiffs...)
}
// If both qps and query latency was not become worse, return.
if len(totalDiffs) == 0 {
return nil, nil
}
// Only for get more information
for _, ck := range otherInfoChecks {
err := c.compareMetric(ck.query)
if err != nil {
continue
}
partDiffs := ck.query.compare()
partDiffs = checkDiffs(partDiffs, ck.ct, ck.threshold)
totalDiffs = append(totalDiffs, partDiffs...)
}
var detailSQL string
var err error
detailSQL, err = c.queryBigQueryInSlowLog()
if err != nil {
return nil, err
}
if len(detailSQL) == 0 {
detailSQL, err = c.queryExpensiveQueryInTiDBLog()
if err != nil {
return nil, err
}
}
details := genMetricDiffsString(totalDiffs)
if len(detailSQL) > 0 {
details = append(details, "try to check the slow query only appear in diagnose time range with sql: \n"+detailSQL)
}
return details, nil
}
func (c *clusterInspection) compareMetric(query metricQuery) error {
arg := &queryArg{
startTime: c.referStartTime,
endTime: c.referEndTime,
}
err := queryMetric(query, arg, c.db)
if err != nil {
return err
}
query.setRefer()
arg.startTime = c.startTime
arg.endTime = c.endTime
err = queryMetric(query, arg, c.db)
if err != nil {
return err
}
query.setCurrent()
return nil
}
type metricQuery interface {
init()
setRefer()
setCurrent()
compare() []metricDiff
generateSQL(arg *queryArg) string
appendRow(row []string) error
}
type baseQuery struct {
table string
labels []string
condition string
}
func (b *baseQuery) genCondition(arg *queryArg) string {
condition := fmt.Sprintf("where time >= '%s' and time < '%s' ", arg.startTime, arg.endTime)
if len(b.condition) > 0 {
condition = condition + "and " + b.condition
}
return condition
}
type avgMaxMin struct {
avg int
max int
min int
}
type queryQPS struct {
baseQuery
result map[string]avgMaxMin
refer map[string]avgMaxMin
current map[string]avgMaxMin
}
func (s *queryQPS) init() {
s.result = make(map[string]avgMaxMin)
}
func (s *queryQPS) setRefer() {
s.refer = s.result
s.result = nil
}
func (s *queryQPS) setCurrent() {
s.current = s.result
s.result = nil
}
func (s *queryQPS) compare() []metricDiff {
diffs := make([]metricDiff, 0, len(s.current))
for label, v := range s.current {
rv := s.refer[label]
diff := newMetricDiff(s.table, label, float64(rv.avg), float64(v.avg))
diffs = append(diffs, diff)
}
return diffs
}
func (s *queryQPS) generateSQL(arg *queryArg) string {
field := ""
for i, label := range s.labels {
if i > 0 {
field += ","
}
field = field + "t1.`" + label + "`"
}
condition := s.genCondition(arg)
sql := fmt.Sprintf("select %[4]s, avg(value),max(value),min(value) from (select `%[3]v`, sum(value) as value from metrics_schema.%[1]s %[2]s group by `%[3]s`,time) as t1 group by %[4]s having avg(value)>0",
s.table, condition, strings.Join(s.labels, "`,`"), field)
prepareSQL := "set @@tidb_metric_query_step=30;set @@tidb_metric_query_range_duration=30;"
sql = prepareSQL + sql
return sql
}
func (s *queryQPS) appendRow(row []string) error {
label := strings.Join(row[:len(s.labels)], ",")
values, err := batchAtoi(row[len(s.labels):])
if err != nil {
return err
}
s.result[label] = avgMaxMin{
avg: values[0],
max: values[1],
min: values[2],
}
return nil
}
func queryMetric(query metricQuery, arg *queryArg, db *gorm.DB) error {
query.init()
sql := query.generateSQL(arg)
rows, err := querySQL(db, sql)
if err != nil {
return err
}
for _, row := range rows {
err = query.appendRow(row)
if err != nil {
return err
}
}
return nil
}
type queryQuantile struct {
baseQuery
result map[string]durationValue
refer map[string]durationValue
current map[string]durationValue
}
type durationValue struct {
avg float64
max float64
}
func (s *queryQuantile) init() {
s.result = make(map[string]durationValue)
}
func (s *queryQuantile) setRefer() {
s.refer = s.result
s.result = nil
}
func (s *queryQuantile) setCurrent() {
s.current = s.result
s.result = nil
}
func (s *queryQuantile) compare() []metricDiff {
diffs := make([]metricDiff, 0, len(s.current))
for label, v := range s.current {
rv := s.refer[label]
diff := newMetricDiff(s.table, label, rv.avg, v.avg)
diffs = append(diffs, diff)
}
return diffs
}
func (s *queryQuantile) generateSQL(arg *queryArg) string {
prepareSQL := "set @@tidb_metric_query_step=30;set @@tidb_metric_query_range_duration=30;"
sql := fmt.Sprintf("select `%[1]s`, avg(value),max(value) from metrics_schema.%[2]s %[3]s group by `%[1]s`",
strings.Join(s.labels, "`,`"), s.table, s.genCondition(arg))
sql = prepareSQL + sql
return sql
}
func (s *queryQuantile) appendRow(row []string) error {
label := strings.Join(row[:len(s.labels)], ",")
values, err := batchAtof(row[len(s.labels):])
if err != nil {
return err
}
s.result[label] = durationValue{
avg: values[0],
max: values[1],
}
return nil
}
type queryTotal struct {
baseQuery
result map[string]float64
refer map[string]float64
current map[string]float64
}
func (s *queryTotal) init() {
s.result = make(map[string]float64)
}
func (s *queryTotal) setRefer() {
s.refer = s.result
s.result = nil
}
func (s *queryTotal) setCurrent() {
s.current = s.result
s.result = nil
}
func (s *queryTotal) compare() []metricDiff {
diffs := make([]metricDiff, 0, len(s.current))
for label, v := range s.current {
rv := s.refer[label]
diff := newMetricDiff(s.table, label, rv, v)
diffs = append(diffs, diff)
}
return diffs
}
func (s *queryTotal) generateSQL(arg *queryArg) string {
prepareSQL := "set @@tidb_metric_query_step=60;set @@tidb_metric_query_range_duration=60;"
sql := fmt.Sprintf("select `%[1]s`, sum(value) as total from metrics_schema.%[2]s %[3]s group by `%[1]s` having total > 0",
strings.Join(s.labels, "`,`"), s.table, s.genCondition(arg))
sql = prepareSQL + sql
return sql
}
func (s *queryTotal) appendRow(row []string) error {
label := strings.Join(row[:len(s.labels)], ",")
values, err := batchAtof(row[len(s.labels):])
if err != nil {
return err
}
s.result[label] = values[0]
return nil
}
func (c *clusterInspection) queryBigQueryInSlowLog() (string, error) {
sql := fmt.Sprintf(`select count(*) from
(select sum(Process_time) as sum_process_time,
digest
from information_schema.CLUSTER_SLOW_QUERY
where time >= '%s'
AND time < '%s'
AND Is_internal = false
group by digest) AS t1
where t1.digest NOT IN
(select digest
from information_schema.CLUSTER_SLOW_QUERY
where time >= '%s'
and time < '%s'
group by digest);`, c.startTime, c.endTime, c.referStartTime, c.referEndTime)
rows, err := querySQL(c.db, sql)
if err != nil {
return "", err
}
if len(rows) == 0 || len(rows[0]) == 0 {
return "", nil
}
count, err := strconv.Atoi(rows[0][0])
if err != nil {
return "", err
}
if count == 0 {
return "", nil
}
return fmt.Sprintf(`select * from
(select count(*),
min(time),
sum(query_time) AS sum_query_time,
sum(Process_time) AS sum_process_time,
sum(Wait_time) AS sum_wait_time,
sum(Commit_time),
sum(Request_count),
sum(process_keys),
sum(Write_keys),
max(Cop_proc_max),
min(query),min(prev_stmt),
digest
from information_schema.CLUSTER_SLOW_QUERY
where time >= '%s'
and time < '%s'
and Is_internal = false
group by digest) AS t1
where t1.digest NOT IN
(select digest
from information_schema.CLUSTER_SLOW_QUERY
where time >= '%s'
AND time < '%s'
group by digest)
order by t1.sum_query_time desc limit 10;`, c.startTime, c.endTime, c.referStartTime, c.referEndTime), nil
}
func (c *clusterInspection) queryExpensiveQueryInTiDBLog() (string, error) {
sql := fmt.Sprintf(`select count(*) from information_schema.cluster_log where type='tidb' and time >= '%s' and time < '%s' and level = 'warn' and message LIKE '%s'`,
c.startTime, c.endTime, "%expensive_query%")
rows, err := querySQL(c.db, sql)
if err != nil {
return "", err
}
if len(rows) == 0 || len(rows[0]) == 0 {
return "", nil
}
count, err := strconv.Atoi(rows[0][0])
if err != nil {
return "", err
}
if count == 0 {
return "", nil
}
sql = strings.Replace(sql, "count(*)", "*", 1)
return sql, nil
}
func batchAtof(ss []string) ([]float64, error) {
re := make([]float64, len(ss))
for i := range ss {
v, err := strconv.ParseFloat(ss[i], 64)
if err != nil {
return nil, err
}
re[i] = v
}
return re, nil
}
func batchAtoi(ss []string) ([]int, error) {
re := make([]int, len(ss))
for i := range ss {
v, err := strconv.ParseFloat(ss[i], 64)
if err != nil {
return nil, err
}
re[i] = int(math.Round(v))
}
return re, nil
}
func calculateDiff(refer float64, check float64) float64 {
if refer != 0 {
return check / refer
}
return check
}
type metricDiff struct {
tp string
label string
ratio float64
rv float64
v float64
}
func newMetricDiff(tp, label string, refer, check float64) metricDiff {
return metricDiff{
tp: tp,
label: label,
ratio: calculateDiff(refer, check),
rv: refer,
v: check,
}
}
func (d metricDiff) String() string {
if d.ratio > 1 {
return fmt.Sprintf("%s,%s: ↑ %.2f (%.2f / %.2f)", d.tp, d.label, d.ratio, d.v, d.rv)
}
return fmt.Sprintf("%s,%s: ↓ %.2f (%.2f / %.2f)", d.tp, d.label, d.ratio, d.v, d.rv)
}
type compareType bool
const (
compareLT compareType = false
compareGT compareType = true
)
func checkDiffs(diffs []metricDiff, tp compareType, threshold float64) []metricDiff {
var result []metricDiff
for i := range diffs {
switch tp {
case compareLT:
if diffs[i].ratio < threshold {
result = append(result, diffs[i])
}
case compareGT:
if diffs[i].ratio > threshold {
result = append(result, diffs[i])
}
}
}
return result
}
func genMetricDiffsString(diffs []metricDiff) []string {
ss := make([]string, 0, len(diffs))
for i := range diffs {
ss = append(ss, diffs[i].String())
}
return ss
}
================================================
FILE: pkg/apiserver/diagnose/model.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package diagnose
import (
"time"
"github.com/google/uuid"
"github.com/pingcap/tidb-dashboard/pkg/dbstore"
)
type Report struct {
ID string `gorm:"primary_key;size:40" json:"id"`
CreatedAt time.Time `json:"created_at"`
Progress int `json:"progress"` // 0~100
Content string `json:"content"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
CompareStartTime *time.Time `json:"compare_start_time"`
CompareEndTime *time.Time `json:"compare_end_time"`
}
func (Report) TableName() string {
return "diagnose_reports"
}
func autoMigrate(db *dbstore.DB) error {
return db.AutoMigrate(&Report{})
}
func NewReport(db *dbstore.DB, startTime, endTime time.Time, compareStartTime, compareEndTime *time.Time) (string, error) {
report := Report{
ID: uuid.New().String(),
CreatedAt: time.Now(),
StartTime: startTime,
EndTime: endTime,
CompareStartTime: compareStartTime,
CompareEndTime: compareEndTime,
}
err := db.Create(&report).Error
if err != nil {
return "", err
}
return report.ID, nil
}
func GetReports(db *dbstore.DB) ([]Report, error) {
var reports []Report
err := db.
Select("id, created_at, progress, start_time, end_time, compare_start_time, compare_end_time").
Order("created_at desc").
Find(&reports).Error
return reports, err
}
func GetReport(db *dbstore.DB, reportID string) (*Report, error) {
var report Report
err := db.Where("id = ?", reportID).First(&report).Error
return &report, err
}
func UpdateReportProgress(db *dbstore.DB, reportID string, progress int) error {
var report Report
report.ID = reportID
return db.Model(&report).Update("progress", progress).Error
}
func SaveReportContent(db *dbstore.DB, reportID string, content string) error {
var report Report
report.ID = reportID
return db.Model(&report).Update("content", content).Error
}
================================================
FILE: pkg/apiserver/diagnose/query.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package diagnose
import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"gorm.io/gorm"
)
type rowQuery interface {
queryRow(arg *queryArg, db *gorm.DB) (*TableRowDef, error)
}
type queryArg struct {
totalTime float64
startTime string
endTime string
quantiles []float64
}
func newQueryArg(startTime, endTime string) *queryArg {
return &queryArg{
startTime: startTime,
endTime: endTime,
quantiles: []float64{0.999, 0.99, 0.90, 0.80},
}
}
type AvgMaxMinTableDef struct {
name string
tbl string
condition string
labels []string
Comment string
}
// Table schema
// METRIC_NAME , LABEL, AVG(VALUE), MAX(VALUE), MIN(VALUE),.
func (t AvgMaxMinTableDef) queryRow(arg *queryArg, db *gorm.DB) (*TableRowDef, error) {
if len(t.name) == 0 {
t.name = t.tbl
}
condition := fmt.Sprintf("where time >= '%s' and time < '%s' ", arg.startTime, arg.endTime)
if len(t.condition) > 0 {
condition = condition + "and " + t.condition
}
sql := fmt.Sprintf("select '%s', '', avg(value), max(value), min(value) from metrics_schema.%s %s",
t.name, t.tbl, condition)
rows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
if len(t.labels) == 0 {
return t.genRow(rows[0], nil), nil
}
sql = fmt.Sprintf("select '%[1]s',`%[2]v`, avg(value), max(value), min(value) from metrics_schema.%[3]v %[4]s group by `%[2]v` order by avg(value) desc",
t.name, strings.Join(t.labels, "`,`"), t.tbl, condition)
subRows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
for i := range subRows {
row := subRows[i]
row[1] = strings.Join(row[1:1+len(t.labels)], ",")
newRow := row[:2]
newRow = append(newRow, row[1+len(t.labels):]...)
subRows[i] = newRow
}
return t.genRow(rows[0], subRows), nil
}
func (t AvgMaxMinTableDef) genRow(values []string, subValues [][]string) *TableRowDef {
specialHandle := func(row []string) []string {
if len(row) == 0 {
return row
}
row[2] = RoundFloatString(row[2])
row[3] = RoundFloatString(row[3])
row[4] = RoundFloatString(row[4])
return row
}
values = specialHandle(values)
for i := range subValues {
subValues[i] = specialHandle(subValues[i])
}
return &TableRowDef{
Values: values,
SubValues: subValues,
Comment: t.Comment,
}
}
type sumValueQuery struct {
name string
tbl string
condition string
labels []string
comment string
}
// Table schema
// METRIC_NAME , LABEL TOTAL_VALUE.
func (t sumValueQuery) queryRow(arg *queryArg, db *gorm.DB) (*TableRowDef, error) {
if len(t.name) == 0 {
t.name = t.tbl
}
condition := fmt.Sprintf("where time >= '%s' and time < '%s' ", arg.startTime, arg.endTime)
if len(t.condition) > 0 {
condition = condition + "and " + t.condition
}
sql := fmt.Sprintf("select '%s', '', sum(value) from metrics_schema.%s %s",
t.name, t.tbl, condition)
rows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
if len(t.labels) == 0 {
return t.genRow(rows[0], nil), nil
}
sql = fmt.Sprintf("select '%[1]v',`%[2]v`, sum(value) from metrics_schema.%[3]v %[4]s group by `%[2]v` having sum(value) > 0 order by sum(value) desc",
t.name, strings.Join(t.labels, "`,`"), t.tbl, condition)
subRows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
for i := range subRows {
row := subRows[i]
row[1] = strings.Join(row[1:1+len(t.labels)], ",")
newRow := row[:2]
newRow = append(newRow, row[1+len(t.labels):]...)
subRows[i] = newRow
}
return t.genRow(rows[0], subRows), nil
}
func (t sumValueQuery) genRow(values []string, subValues [][]string) *TableRowDef {
specialHandle := func(row []string) []string {
if len(row) == 0 {
return row
}
row[2] = RoundFloatString(row[2])
return row
}
values = specialHandle(values)
for i := range subValues {
subValues[i] = specialHandle(subValues[i])
}
return &TableRowDef{
Values: values,
SubValues: subValues,
Comment: genComment(t.comment, t.labels),
}
}
type totalTimeByLabelsTableDef struct {
name string
tbl string
labels []string
comment string
}
// Table schema
// METRIC_NAME , LABEL , TIME_RATIO , TOTAL_VALUE , TOTAL_COUNT , P999 , P99 , P90 , P80.
func (t totalTimeByLabelsTableDef) queryRow(arg *queryArg, db *gorm.DB) (*TableRowDef, error) {
sql := t.genSumarySQLs(arg.totalTime, arg.startTime, arg.endTime, arg.quantiles)
rows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
if len(t.labels) == 0 {
return t.genRow(rows[0], nil), nil
}
if arg.totalTime == 0 && len(rows[0][3]) > 0 {
totalTime, err := strconv.ParseFloat(rows[0][3], 64)
if err == nil {
arg.totalTime = totalTime
}
}
sql = t.genDetailSQLs(arg.totalTime, arg.startTime, arg.endTime, arg.quantiles)
subRows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
for i := range subRows {
row := subRows[i]
row[1] = strings.Join(row[1:1+len(t.labels)], ",")
newRow := row[:2]
newRow = append(newRow, row[1+len(t.labels):]...)
subRows[i] = newRow
}
return t.genRow(rows[0], subRows), nil
}
func (t totalTimeByLabelsTableDef) genRow(values []string, subValues [][]string) *TableRowDef {
specialHandle := func(row []string) []string {
if len(row) == 0 {
return row
}
name := row[0]
if strings.HasSuffix(name, "(us)") {
if len(row[3]) == 0 {
return row
}
for _, i := range []int{2, 3, 5, 6, 7, 8} {
v, err := strconv.ParseFloat(row[i], 64)
if err == nil {
row[i] = fmt.Sprintf("%f", v/10e5)
}
}
row[0] = name[:len(name)-4]
}
if len(row[4]) > 0 {
row[4] = convertFloatToInt(row[4])
}
for _, i := range []int{2, 3, 5, 6, 7, 8} {
row[i] = RoundFloatString(row[i])
}
return row
}
values = specialHandle(values)
for i := range subValues {
subValues[i] = specialHandle(subValues[i])
}
return &TableRowDef{
Values: values,
SubValues: subValues,
Comment: genComment(t.comment, t.labels),
}
}
func (t totalTimeByLabelsTableDef) genSumarySQLs(totalTime float64, startTime, endTime string, quantiles []float64) string {
sqls := []string{ //nolint:prealloc
fmt.Sprintf("select '%[1]s','', if(%[2]v>0,sum(value)/%[2]v,1) , sum(value) from metrics_schema.%[3]s_total_time where time >= '%[4]s' and time < '%[5]s'",
t.name, totalTime, t.tbl, startTime, endTime),
fmt.Sprintf("select sum(value) from metrics_schema.%s_total_count where time >= '%s' and time < '%s'",
t.tbl, startTime, endTime),
}
for _, quantile := range quantiles {
sql := fmt.Sprintf("select max(value) as max_value from metrics_schema.%s_duration where time >= '%s' and time < '%s' and quantile=%f",
t.tbl, startTime, endTime, quantile)
sqls = append(sqls, sql)
}
var fields strings.Builder
tbls := ""
for i, sql := range sqls {
if i > 0 {
fields.WriteString(",")
tbls += "join "
}
fmt.Fprintf(&fields, "t%v.*", i)
tbls += fmt.Sprintf(" (%s) as t%v ", sql, i)
}
joinSQL := fmt.Sprintf("select %v from %v", fields.String(), tbls)
return joinSQL
}
func (t totalTimeByLabelsTableDef) genDetailSQLs(totalTime float64, startTime, endTime string, quantiles []float64) string {
if len(t.labels) == 0 {
return ""
}
var joinSQL strings.Builder
joinSQL.WriteString("select t0.*,t1.total_count")
sqls := []string{
fmt.Sprintf("select '%[1]s', `%[6]s`, if(%[2]v>0,sum(value)/%[2]v,1) , sum(value) as total from metrics_schema.%[3]s_total_time where time >= '%[4]s' and time < '%[5]s' group by `%[6]s` having sum(value) > 0",
t.name, totalTime, t.tbl, startTime, endTime, strings.Join(t.labels, "`,`")),
fmt.Sprintf("select `%[4]s`, sum(value) as total_count from metrics_schema.%[1]s_total_count where time >= '%[2]s' and time < '%[3]s' group by `%[4]s`",
t.tbl, startTime, endTime, strings.Join(t.labels, "`,`")),
}
for i, quantile := range quantiles {
sql := fmt.Sprintf("select `%[5]s`, max(value) as max_value from metrics_schema.%[1]s_duration where time >= '%[2]s' and time < '%[3]s' and quantile=%[4]f group by `%[5]s`",
t.tbl, startTime, endTime, quantile, strings.Join(t.labels, "`,`"))
sqls = append(sqls, sql)
fmt.Fprintf(&joinSQL, ",t%v.max_value", i+2)
}
joinSQL.WriteString(" from ")
for i, sql := range sqls {
fmt.Fprintf(&joinSQL, " (%s) as t%v ", sql, i)
if i != len(sqls)-1 {
joinSQL.WriteString("join ")
}
}
joinSQL.WriteString(" where ")
for i := 0; i < len(sqls)-1; i++ {
for j, label := range t.labels {
if i > 0 || j > 0 {
joinSQL.WriteString("and ")
}
fmt.Fprintf(&joinSQL, " t%v.%s = t%v.%s ", i, label, i+1, label)
}
}
joinSQL.WriteString(" order by t0.total desc")
return joinSQL.String()
}
type totalValueAndTotalCountTableDef struct {
name string
tbl string
sumTbl string
countTbl string
labels []string
comment string
}
// Table schema
// METRIC_NAME , LABEL TOTAL_VALUE , TOTAL_COUNT , P999 , P99 , P90 , P80.
func (t totalValueAndTotalCountTableDef) queryRow(arg *queryArg, db *gorm.DB) (*TableRowDef, error) {
sql := t.genSumarySQLs(arg.startTime, arg.endTime, arg.quantiles)
rows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
if len(t.labels) == 0 {
return t.genRow(rows[0], nil), nil
}
sql = t.genDetailSQLs(arg.startTime, arg.endTime, arg.quantiles)
subRows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
for i := range subRows {
row := subRows[i]
row[1] = strings.Join(row[1:1+len(t.labels)], ",")
newRow := row[:2]
newRow = append(newRow, row[1+len(t.labels):]...)
subRows[i] = newRow
}
return t.genRow(rows[0], subRows), nil
}
func (t totalValueAndTotalCountTableDef) genRow(values []string, subValues [][]string) *TableRowDef {
specialHandle := func(row []string) []string {
for i := 2; i < len(row); i++ {
if len(row[i]) == 0 {
continue
}
row[i] = convertFloatToInt(row[i])
}
return row
}
values = specialHandle(values)
for i := range subValues {
subValues[i] = specialHandle(subValues[i])
}
return &TableRowDef{
Values: values,
SubValues: subValues,
Comment: genComment(t.comment, t.labels),
}
}
func (t totalValueAndTotalCountTableDef) genSumarySQLs(startTime, endTime string, quantiles []float64) string {
sqls := []string{ //nolint:prealloc
fmt.Sprintf("select '%[1]s','' , sum(value) from metrics_schema.%[2]s where time >= '%[3]s' and time < '%[4]s'",
t.name, t.sumTbl, startTime, endTime),
fmt.Sprintf("select sum(value) from metrics_schema.%s where time >= '%s' and time < '%s'",
t.countTbl, startTime, endTime),
}
for _, quantile := range quantiles {
sql := fmt.Sprintf("select max(value) as max_value from metrics_schema.%s where time >= '%s' and time < '%s' and quantile=%f",
t.tbl, startTime, endTime, quantile)
sqls = append(sqls, sql)
}
var fields strings.Builder
tbls := ""
for i, sql := range sqls {
if i > 0 {
fields.WriteString(",")
tbls += "join "
}
fmt.Fprintf(&fields, "t%v.*", i)
tbls += fmt.Sprintf(" (%s) as t%v ", sql, i)
}
joinSQL := fmt.Sprintf("select %v from %v", fields.String(), tbls)
return joinSQL
}
func (t totalValueAndTotalCountTableDef) genDetailSQLs(startTime, endTime string, quantiles []float64) string {
if len(t.labels) == 0 {
return ""
}
var joinSQL strings.Builder
joinSQL.WriteString("select t0.*,t1.count")
sqls := []string{
fmt.Sprintf("select '%[1]s', `%[5]s` , sum(value) as total from metrics_schema.%[2]s where time >= '%[3]s' and time < '%[4]s' group by `%[5]s` having sum(value) > 0",
t.name, t.sumTbl, startTime, endTime, strings.Join(t.labels, "`,`")),
fmt.Sprintf("select `%[4]s`, sum(value) as count from metrics_schema.%[1]s where time >= '%[2]s' and time < '%[3]s' group by `%[4]s`",
t.countTbl, startTime, endTime, strings.Join(t.labels, "`,`")),
}
for i, quantile := range quantiles {
sql := fmt.Sprintf("select `%[5]s`, max(value) as max_value from metrics_schema.%[1]s where time >= '%[2]s' and time < '%[3]s' and quantile=%[4]f group by `%[5]s`",
t.tbl, startTime, endTime, quantile, strings.Join(t.labels, "`,`"))
sqls = append(sqls, sql)
fmt.Fprintf(&joinSQL, ",t%v.max_value", i+2)
}
joinSQL.WriteString(" from ")
for i, sql := range sqls {
fmt.Fprintf(&joinSQL, " (%s) as t%v ", sql, i)
if i != len(sqls)-1 {
joinSQL.WriteString("join ")
}
}
joinSQL.WriteString(" where ")
for i := 0; i < len(sqls)-1; i++ {
for j, label := range t.labels {
if i > 0 || j > 0 {
joinSQL.WriteString("and ")
}
fmt.Fprintf(&joinSQL, " t%v.%s = t%v.%s ", i, label, i+1, label)
}
}
joinSQL.WriteString(" order by t0.total desc")
return joinSQL.String()
}
func querySQL(db *gorm.DB, sql string) ([][]string, error) {
if len(sql) == 0 {
return nil, nil
}
rows, err := db.Raw(sql).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
// Read all rows.
resultRows := make([][]string, 0, 2)
for rows.Next() {
cols, err1 := rows.Columns()
if err1 != nil {
return nil, err
}
// See https://stackoverflow.com/questions/14477941/read-select-columns-into-string-in-go
rawResult := make([][]byte, len(cols))
dest := make([]interface{}, len(cols))
for i := range rawResult {
dest[i] = &rawResult[i]
}
err1 = rows.Scan(dest...)
if err1 != nil {
return nil, err
}
resultRow := []string{}
for _, raw := range rawResult {
val := ""
if raw != nil {
val = string(raw)
}
resultRow = append(resultRow, val)
}
resultRows = append(resultRows, resultRow)
}
if err = rows.Err(); err != nil {
return nil, err
}
return resultRows, nil
}
func convertFloatToInt(s string) string {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return s
}
f = math.Round(f)
return fmt.Sprintf("%.0f", f)
}
func convertFloatToSize(s string) string {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return s
}
if mb := f / float64(1024*1024*1024); mb > 1 {
f = math.Round(mb*1000) / 1000
return fmt.Sprintf("%.3f GB", f)
}
if mb := f / float64(1024*1024); mb > 1 {
f = math.Round(mb*1000) / 1000
return fmt.Sprintf("%.3f MB", f)
}
kb := f / float64(1024)
f = math.Round(kb*1000) / 1000
return fmt.Sprintf("%.3f KB", f)
}
func convertFloatToDuration(s string, ratio float64) string {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return s
}
f = f * ratio
if f > 10 {
f = math.Round(f*1000) / 1000
return fmt.Sprintf("%.0f s", f)
}
if ms := f * 1000; ms > 10 {
f = math.Round(ms*1000) / 1000
return fmt.Sprintf("%.0f ms", f)
}
us := f * 1000 * 1000
f = math.Round(us*1000) / 1000
return fmt.Sprintf("%.0f us", f)
}
func convertFloatToSizeByRows(rows []TableRowDef, idx int) {
for i := range rows {
convertFloatToSizeByRow(&rows[i], idx)
}
}
func convertFloatToSizeByRow(row *TableRowDef, idx int) {
if len(row.Values) < (idx + 1) {
return
}
row.Values[idx] = convertFloatToSize(row.Values[idx])
for j := range row.SubValues {
if len(row.SubValues[j]) < (idx + 1) {
continue
}
row.SubValues[j][idx] = convertFloatToSize(row.SubValues[j][idx])
}
}
func RoundFloatString(s string) string {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return s
}
return convertFloatToString(f)
}
func convertFloatToString(f float64) string {
if f == 0 {
return "0"
}
sign := float64(1)
if f < 0 {
sign = -1
f = 0 - f
}
tmp := f
n := 2
for tmp <= 0.01 {
tmp = tmp * 10
n++
if n > 15 {
break
}
}
value := math.Pow10(n)
f = math.Round(f*value) / value
format := `%.` + strconv.FormatInt(int64(n), 10) + `f`
str := fmt.Sprintf(format, f*sign)
if strings.Contains(str, ".") {
for strings.HasSuffix(str, "0") {
str = str[:len(str)-1]
}
}
if strings.HasSuffix(str, ".") {
return str[:len(str)-1]
}
return str
}
func genComment(comment string, labels []string) string {
if len(labels) > 0 {
if len(comment) > 0 {
comment += ","
}
comment = fmt.Sprintf("%s the label is [%s]", comment, strings.Join(labels, ", "))
}
return comment
}
func sortRowsByIndex(resultRows []TableRowDef, idx int) {
// sort sub rows.
for j := range resultRows {
subValues := resultRows[j].SubValues
sort.Slice(subValues, func(i, j int) bool {
if len(subValues[i]) < (idx+1) || len(subValues[j]) < (idx+1) {
return false
}
v1, err1 := parseFloat(subValues[i][idx])
v2, err2 := parseFloat(subValues[j][idx])
if err1 != nil || err2 != nil {
return false
}
return v1 > v2
})
resultRows[j].SubValues = subValues
}
sort.Slice(resultRows, func(i, j int) bool {
if len(resultRows[i].Values) < (idx+1) || len(resultRows[j].Values) < (idx+1) {
return false
}
v1, err1 := parseFloat(resultRows[i].Values[idx])
v2, err2 := parseFloat(resultRows[j].Values[idx])
if err1 != nil || err2 != nil {
return false
}
return v1 > v2
})
}
================================================
FILE: pkg/apiserver/diagnose/report.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package diagnose
import (
"bytes"
"fmt"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"gorm.io/gorm"
"github.com/pingcap/tidb-dashboard/pkg/dbstore"
)
type TableDef struct {
Category []string `json:"category"` // The category of the table, such as [TiDB]
Title string `json:"title"`
Comment string `json:"comment"`
joinColumns []int
compareColumns []int
Column []string `json:"column"`
Rows []TableRowDef `json:"rows"`
}
type TableRowDef struct {
Values []string `json:"values"`
SubValues [][]string `json:"sub_values"` // SubValues need fold default.
ratio float64
Comment string `json:"comment"`
}
func (t TableDef) ColumnWidth() []int {
fieldLen := make([]int, 0, len(t.Column))
if len(t.Rows) == 0 {
return fieldLen
}
for i := 0; i < len(t.Column); i++ {
l := 0
for _, row := range t.Rows {
if l < len(row.Values[i]) {
l = len(row.Values[i])
}
for _, subRow := range row.SubValues {
if l < len(subRow[i]) {
l = len(subRow[i])
}
}
}
for _, col := range t.Column {
if l < len(col) {
l = len(col)
}
}
fieldLen = append(fieldLen, l)
}
return fieldLen
}
const (
// Category names.
CategoryHeader = "header"
CategoryDiagnose = "diagnose"
CategoryLoad = "load"
CategoryOverview = "overview"
CategoryTiDB = "TiDB"
CategoryPD = "PD"
CategoryTiKV = "TiKV"
CategoryConfig = "config"
CategoryError = "error"
)
func GetReportTablesForDisplay(startTime, endTime string, db *gorm.DB, sqliteDB *dbstore.DB, reportID string) []*TableDef {
errRows := checkBeforeReport(db)
if len(errRows) > 0 {
return []*TableDef{GenerateReportError(errRows)}
}
tables := GetReportTables(startTime, endTime, db, sqliteDB, reportID)
lastCategory := ""
for _, tbl := range tables {
if tbl == nil {
continue
}
category := strings.Join(tbl.Category, ",")
if category != lastCategory {
lastCategory = category
} else {
tbl.Category = []string{""}
}
}
return tables
}
func checkBeforeReport(db *gorm.DB) (errRows []TableRowDef) {
command := "you can use this shell command to set the config: `curl -X POST -d '{\"metric-storage\":\"http://{PROMETHEUS_ADDRESS}\"}' http://{PD_ADDRESS}/pd/api/v1/config`, \n" +
"PROMETHEUS_ADDRESS is the prometheus address, It's used for query metric data; PD_ADDRESS is the HTTP API address of PD server, all PD servers need to set this config. \n" +
"Here is an example: `curl -X POST -d '{\"metric-storage\":\"http://127.0.0.1:9090\"}' http://127.0.0.1:2379/pd/api/v1/config`"
// Check for query metric.
sql := "select count(*) from metrics_schema.up;"
_, err := querySQL(db, sql)
if err != nil {
errRows = append(errRows, TableRowDef{
Values: []string{
"check before report",
"metrics_schema.up",
err.Error() + ", \n" +
"Currently, the PD config `pd-server.metric-storage` value should be prometheus address, please check whether the config value is correct, you can use below sql check the value: \n" +
"select * from information_schema.cluster_config where type='pd' and `key` ='pd-server.metric-storage'; , \n" + command,
},
})
return
}
return nil
}
type getTableFunc = func(string, string, *gorm.DB) (TableDef, error)
func GetReportTables(startTime, endTime string, db *gorm.DB, sqliteDB *dbstore.DB, reportID string) []*TableDef {
funcs := []getTableFunc{
// Header
GetHeaderTimeTable,
GetClusterHardwareInfoTable,
GetClusterInfoTable,
// Diagnose
GetAllDiagnoseReport,
// Load
GetLoadTable,
GetCPUUsageTable,
GetProcessMemUsageTable,
GetTiKVThreadCPUTable,
GetGoroutinesCountTable,
// Overview
GetTotalTimeConsumeTable,
GetTotalErrorTable,
// TiDB
GetTiDBTimeConsumeTable,
GetTiDBConnectionCountTable,
GetTiDBTxnTableData,
GetTiDBStatisticsInfo,
GetTiDBDDLOwner,
GetTiDBTopNSlowQuery,
GetTiDBTopNSlowQueryGroupByDigest,
GetTiDBSlowQueryWithDiffPlan,
// PD
GetPDTimeConsumeTable,
GetPDSchedulerInfo,
GetPDClusterStatusTable,
GetStoreStatusTable,
GetPDEtcdStatusTable,
// TiKV
GetTiKVTotalTimeConsumeTable,
GetTiKVRocksDBTimeConsumeTable,
GetTiKVErrorTable,
GetTiKVStoreInfo,
GetTiKVRegionSizeInfo,
GetTiKVCopInfo,
GetTiKVSchedulerInfo,
GetTiKVRaftInfo,
GetTiKVSnapshotInfo,
GetTiKVGCInfo,
GetTiKVTaskInfo,
GetTiKVCacheHitTable,
// Config
GetPDConfigInfo,
GetPDConfigChangeInfo,
GetTiDBGCConfigInfo,
GetTiDBGCConfigChangeInfo,
GetTiKVRocksDBConfigInfo,
GetTiKVRocksDBConfigChangeInfo,
GetTiKVRaftStoreConfigInfo,
GetTiKVRaftStoreConfigChangeInfo,
GetTiDBCurrentConfig,
GetPDCurrentConfig,
GetTiKVCurrentConfig,
}
var progress int32
totalTableCount := int32(len(funcs))
tables, errRows := getTablesParallel(startTime, endTime, db, funcs, sqliteDB, reportID, &progress, &totalTableCount)
tables = append(tables, GenerateReportError(errRows))
return tables
}
func getTablesParallel(startTime, endTime string, db *gorm.DB, funcs []getTableFunc, sqliteDB *dbstore.DB, reportID string, progress, totalTableCount *int32) ([]*TableDef, []TableRowDef) {
// get the local CPU count for concurrence
conc := min(runtime.NumCPU(), 20)
if conc > len(funcs) {
conc = len(funcs)
}
taskChan := func2task(funcs)
resChan := make(chan *tblAndErr, len(funcs))
var wg sync.WaitGroup
// get table concurrently
for i := 0; i < conc; i++ {
wg.Add(1)
go doGetTable(taskChan, resChan, &wg, startTime, endTime, db, sqliteDB, reportID, progress, totalTableCount)
}
wg.Wait()
// all task done, close the resChan
close(resChan)
tblAndErrSlice := make([]tblAndErr, 0, cap(resChan))
for tblAndErr := range resChan {
tblAndErrSlice = append(tblAndErrSlice, *tblAndErr)
}
sort.Slice(tblAndErrSlice, func(i, j int) bool {
return tblAndErrSlice[i].taskID < tblAndErrSlice[j].taskID
})
tables := make([]*TableDef, 0, len(tblAndErrSlice)+1)
errRows := make([]TableRowDef, 0, len(tblAndErrSlice))
for _, v := range tblAndErrSlice {
if v.tbl != nil {
tables = append(tables, v.tbl)
}
if v.err != nil {
errRows = append(errRows, *v.err)
}
}
return tables, errRows
}
type tblAndErr struct {
tbl *TableDef
err *TableRowDef
taskID int
}
// 1.doGetTable gets the task from taskChan,and close the taskChan if taskChan is empty.
// 2.doGetTable puts the tblAndErr result to resChan.
// 3.if taskChan is empty, put a true in doneChan.
func doGetTable(taskChan chan *task, resChan chan *tblAndErr, wg *sync.WaitGroup, startTime, endTime string, db *gorm.DB, sqliteDB *dbstore.DB, reportID string, progress, totalTableCount *int32) {
defer wg.Done()
for task := range taskChan {
f := task.t
var tbl TableDef
var err error
func() {
defer func() {
if r := recover(); r != nil {
tbl.Title = fmt.Sprintf("panic_in_table_%v", task.taskID)
err = fmt.Errorf("panic: %v", r)
}
}()
tbl, err = f(startTime, endTime, db)
}()
newProgress := atomic.AddInt32(progress, 1)
tblAndErr := tblAndErr{}
if err != nil {
category := strings.Join(tbl.Category, ",")
tblAndErr.err = &TableRowDef{Values: []string{category, tbl.Title, err.Error()}}
}
if tbl.Rows != nil {
tblAndErr.tbl = &tbl
}
tblAndErr.taskID = task.taskID
resChan <- &tblAndErr
if sqliteDB != nil {
_ = UpdateReportProgress(sqliteDB, reportID, int((newProgress*100)/atomic.LoadInt32(totalTableCount)))
}
}
}
type task struct {
t getTableFunc
taskID int // taskID for arrange the tables in order
}
// change the get-Table-func to task.
func func2task(funcs []getTableFunc) chan *task {
taskChan := make(chan *task, len(funcs))
for i := range funcs {
taskChan <- &task{funcs[i], i}
}
close(taskChan)
return taskChan
}
func GenerateReportError(errRows []TableRowDef) *TableDef {
return &TableDef{
Category: []string{CategoryError},
Title: "generate_report_error",
Comment: "",
Column: []string{"CATEGORY", "TABLE", "ERROR"},
Rows: errRows,
}
}
func GetHeaderTimeTable(startTime, endTime string, _ *gorm.DB) (TableDef, error) {
return TableDef{
Category: []string{CategoryHeader},
Title: "report_time_range",
Comment: "",
Column: []string{"START_TIME", "END_TIME"},
Rows: []TableRowDef{
{Values: []string{startTime, endTime}},
},
}, nil
}
func GetAllDiagnoseReport(startTime, endTime string, db *gorm.DB) (TableDef, error) {
return GetDiagnoseReport(startTime, endTime, db, nil)
}
func GetDiagnoseReport(startTime, endTime string, db *gorm.DB, rules []string) (TableDef, error) {
table := TableDef{
Category: []string{CategoryDiagnose},
Title: "diagnose",
Comment: "",
Column: []string{"RULE", "ITEM", "TYPE", "INSTANCE", "STATUS_ADDRESS", "VALUE", "REFERENCE", "SEVERITY", "DETAILS"},
}
sql := fmt.Sprintf("select /*+ time_range('%s','%s') */ %s from information_schema.INSPECTION_RESULT", startTime, endTime, strings.Join(table.Column, ","))
if len(rules) > 0 {
sql = fmt.Sprintf("%s where RULE in ('%s')", sql, strings.Join(rules, "','"))
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
newRows := make([]TableRowDef, 0, len(rows))
rowIdxMap := make(map[string]int)
for _, row := range rows {
if len(row.Values) < len(table.Column) {
continue
}
// rule + item
name := row.Values[0] + row.Values[1]
idx, ok := rowIdxMap[name]
if ok && idx < len(newRows) {
newRows[idx].SubValues = append(newRows[idx].SubValues, row.Values)
continue
}
newRows = append(newRows, row)
rowIdxMap[name] = len(newRows) - 1
}
table.Rows = newRows
return table, nil
}
func GetTotalTimeConsumeTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []totalTimeByLabelsTableDef{
{name: "tidb_query", tbl: "tidb_query", labels: []string{"sql_type"}},
{name: "tidb_get_token(us)", tbl: "tidb_get_token", labels: []string{"instance"}},
{name: "tidb_parse", tbl: "tidb_parse", labels: []string{"sql_type"}},
{name: "tidb_compile", tbl: "tidb_compile", labels: []string{"sql_type"}},
{name: "tidb_execute", tbl: "tidb_execute", labels: []string{"sql_type"}},
{name: "tidb_distsql_execution", tbl: "tidb_distsql_execution", labels: []string{"type"}},
{name: "tidb_cop", tbl: "tidb_cop", labels: []string{"instance"}},
{name: "tidb_transaction", tbl: "tidb_transaction", labels: []string{"sql_type"}},
{name: "tidb_transaction_local_latch_wait", tbl: "tidb_transaction_local_latch_wait", labels: []string{"instance"}},
{name: "tidb_txn_cmd", tbl: "tidb_txn_cmd", labels: []string{"type"}},
{name: "tidb_kv_backoff", tbl: "tidb_kv_backoff", labels: []string{"type"}},
{name: "tidb_kv_request", tbl: "tidb_kv_request", labels: []string{"type"}},
{name: "tidb_slow_query", tbl: "tidb_slow_query", labels: []string{"instance"}},
{name: "tidb_slow_query_cop_process", tbl: "tidb_slow_query_cop_process", labels: []string{"instance"}},
{name: "tidb_slow_query_cop_wait", tbl: "tidb_slow_query_cop_wait", labels: []string{"instance"}},
{name: "tidb_ddl_handle_job", tbl: "tidb_ddl", labels: []string{"type"}},
{name: "tidb_ddl_worker", tbl: "tidb_ddl_worker", labels: []string{"action"}},
{name: "tidb_ddl_update_self_version", tbl: "tidb_ddl_update_self_version", labels: []string{"result"}},
{name: "tidb_owner_handle_syncer", tbl: "tidb_owner_handle_syncer", labels: []string{"type"}},
{name: "tidb_ddl_batch_add_index", tbl: "tidb_ddl_batch_add_index", labels: []string{"type"}},
{name: "tidb_ddl_deploy_syncer", tbl: "tidb_ddl_deploy_syncer", labels: []string{"type"}},
{name: "tidb_load_schema", tbl: "tidb_load_schema", labels: []string{"instance"}},
{name: "tidb_meta_operation", tbl: "tidb_meta_operation", labels: []string{"type"}},
{name: "tidb_auto_id_request", tbl: "tidb_auto_id_request", labels: []string{"type"}},
{name: "tidb_statistics_auto_analyze", tbl: "tidb_statistics_auto_analyze", labels: []string{"instance"}},
{name: "tidb_gc", tbl: "tidb_gc", labels: []string{"instance"}},
{name: "tidb_gc_push_task", tbl: "tidb_gc_push_task", labels: []string{"type"}},
{name: "tidb_batch_client_unavailable", tbl: "tidb_batch_client_unavailable", labels: []string{"instance"}},
{name: "tidb_batch_client_wait", tbl: "tidb_batch_client_wait", labels: []string{"instance"}},
{name: "tidb_batch_client_wait_conn", tbl: "tidb_batch_client_wait_conn", labels: []string{"instance"}},
// PD
{name: "pd_tso_rpc", tbl: "pd_tso_rpc", labels: []string{"instance"}},
{name: "pd_tso_wait", tbl: "pd_tso_wait", labels: []string{"instance"}},
{name: "pd_client_cmd", tbl: "pd_client_cmd", labels: []string{"type"}},
{name: "pd_client_request_rpc", tbl: "pd_request_rpc", labels: []string{"type"}},
{name: "pd_grpc_completed_commands", tbl: "pd_grpc_completed_commands", labels: []string{"grpc_method"}},
{name: "pd_operator_finish", tbl: "pd_operator_finish", labels: []string{"type"}},
{name: "pd_operator_step_finish", tbl: "pd_operator_step_finish", labels: []string{"type"}},
{name: "pd_handle_transactions", tbl: "pd_handle_transactions", labels: []string{"result"}},
{name: "pd_region_heartbeat", tbl: "pd_region_heartbeat", labels: []string{"address"}},
{name: "etcd_wal_fsync", tbl: "etcd_wal_fsync", labels: []string{"instance"}},
{name: "pd_peer_round_trip", tbl: "pd_peer_round_trip", labels: []string{"To"}},
// TiKV
{name: "tikv_grpc_message", tbl: "tikv_grpc_message", labels: []string{"type"}},
{name: "tikv_cop_request", tbl: "tikv_cop_request", labels: []string{"req"}},
{name: "tikv_cop_handle", tbl: "tikv_cop_handle", labels: []string{"req"}},
{name: "tikv_cop_wait", tbl: "tikv_cop_wait", labels: []string{"req"}},
{name: "tikv_scheduler_command", tbl: "tikv_scheduler_command", labels: []string{"type"}},
{name: "tikv_scheduler_latch_wait", tbl: "tikv_scheduler_latch_wait", labels: []string{"type"}},
{name: "tikv_storage_async_request", tbl: "tikv_storage_async_request", labels: []string{"type"}},
{name: "tikv_scheduler_processing_read", tbl: "tikv_scheduler_processing_read", labels: []string{"type"}},
{name: "tikv_raft_propose_wait", tbl: "tikv_raftstore_propose_wait", labels: []string{"instance"}},
{name: "tikv_raft_process", tbl: "tikv_raftstore_process", labels: []string{"type"}},
{name: "tikv_raft_append_log", tbl: "tikv_raftstore_append_log", labels: []string{"instance"}},
{name: "tikv_raft_commit_log", tbl: "tikv_raftstore_commit_log", labels: []string{"instance"}},
{name: "tikv_raft_apply_wait", tbl: "tikv_raftstore_apply_wait", labels: []string{"instance"}},
{name: "tikv_raft_apply_log", tbl: "tikv_raftstore_apply_log", labels: []string{"instance"}},
{name: "tikv_raft_store_events", tbl: "tikv_raft_store_events", labels: []string{"type"}},
{name: "tikv_handle_snapshot", tbl: "tikv_handle_snapshot", labels: []string{"type"}},
{name: "tikv_send_snapshot", tbl: "tikv_send_snapshot", labels: []string{"instance"}},
{name: "tikv_check_split", tbl: "tikv_check_split", labels: []string{"instance"}},
{name: "tikv_ingest_sst", tbl: "tikv_ingest_sst", labels: []string{"instance"}},
{name: "tikv_gc_tasks", tbl: "tikv_gc_tasks", labels: []string{"task"}},
{name: "tikv_pd_request", tbl: "tikv_pd_request", labels: []string{"type"}},
{name: "tikv_lock_manager_deadlock_detect", tbl: "tikv_lock_manager_deadlock_detect", labels: []string{"instance"}},
{name: "tikv_lock_manager_waiter_lifetime", tbl: "tikv_lock_manager_waiter_lifetime", labels: []string{"instance"}},
{name: "tikv_backup_range", tbl: "tikv_backup_range", labels: []string{"type"}},
{name: "tikv_backup", tbl: "tikv_backup", labels: []string{"instance"}},
}
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
table := TableDef{
Category: []string{CategoryOverview},
Title: "total_time_consume",
Comment: ``,
joinColumns: []int{0, 1},
compareColumns: []int{3, 4, 5},
Column: []string{"METRIC_NAME", "LABEL", "TIME_RATIO", "TOTAL_TIME", "TOTAL_COUNT", "P999", "P99", "P90", "P80"},
}
resultRows := make([]TableRowDef, 0, len(defs))
arg := newQueryArg(startTime, endTime)
specialHandle := func(row []string) []string {
if arg.totalTime == 0 && len(row[3]) > 0 {
totalTime, err := strconv.ParseFloat(row[3], 64)
if err == nil {
arg.totalTime = totalTime
}
}
return row
}
appendRows := func(row TableRowDef) {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
resultRows = append(resultRows, row)
}
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetTotalErrorTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []sumValueQuery{
{tbl: "tidb_binlog_error_total_count", labels: []string{"instance"}},
{tbl: "tidb_handshake_error_total_count", labels: []string{"instance"}},
{tbl: "tidb_transaction_retry_error_total_count", labels: []string{"sql_type"}},
{tbl: "tidb_kv_region_error_total_count", labels: []string{"type"}},
{tbl: "tidb_schema_lease_error_total_count", labels: []string{"instance"}},
{tbl: "tikv_grpc_error_total_count", labels: []string{"type"}},
{tbl: "tikv_critical_error_total_count", labels: []string{"type"}},
{tbl: "tikv_scheduler_is_busy_total_count", labels: []string{"type"}},
{tbl: "tikv_channel_full_total_count", labels: []string{"type"}},
{tbl: "tikv_coprocessor_request_error_total_count", labels: []string{"reason"}},
{tbl: "tikv_engine_write_stall", labels: []string{"instance"}},
{tbl: "tikv_server_report_failures_total_count", labels: []string{"instance"}},
{name: "tikv_storage_async_request_error", tbl: "tikv_storage_async_requests_total_count", labels: []string{"type"}, condition: "status not in ('all','success')"},
{tbl: "tikv_lock_manager_detect_error_total_count", labels: []string{"type"}},
{tbl: "tikv_backup_errors_total_count", labels: []string{"error"}},
{tbl: "node_network_in_errors_total_count", labels: []string{"instance"}},
{tbl: "node_network_out_errors_total_count", labels: []string{"instance"}},
}
table := TableDef{
Category: []string{CategoryOverview},
Title: "total_error",
Comment: ``,
joinColumns: []int{0, 1},
compareColumns: []int{2},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_COUNT"},
}
rows, err := getSumValueTableData(defs1, startTime, endTime, db)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetTiDBTimeConsumeTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []totalTimeByLabelsTableDef{
{name: "tidb_query", tbl: "tidb_query", labels: []string{"instance", "sql_type"}},
{name: "tidb_get_token(us)", tbl: "tidb_get_token", labels: []string{"instance"}},
{name: "tidb_parse", tbl: "tidb_parse", labels: []string{"instance", "sql_type"}},
{name: "tidb_compile", tbl: "tidb_compile", labels: []string{"instance", "sql_type"}},
{name: "tidb_execute", tbl: "tidb_execute", labels: []string{"instance", "sql_type"}},
{name: "tidb_distsql_execution", tbl: "tidb_distsql_execution", labels: []string{"instance", "type"}},
{name: "tidb_cop", tbl: "tidb_cop", labels: []string{"instance"}},
{name: "tidb_transaction", tbl: "tidb_transaction", labels: []string{"instance", "sql_type", "type"}},
{name: "tidb_transaction_local_latch_wait", tbl: "tidb_transaction_local_latch_wait", labels: []string{"instance"}},
{name: "tidb_kv_backoff", tbl: "tidb_kv_backoff", labels: []string{"instance", "type"}},
{name: "tidb_kv_request", tbl: "tidb_kv_request", labels: []string{"instance", "store", "type"}},
{name: "tidb_slow_query", tbl: "tidb_slow_query", labels: []string{"instance"}},
{name: "tidb_slow_query_cop_process", tbl: "tidb_slow_query_cop_process", labels: []string{"instance"}},
{name: "tidb_slow_query_cop_wait", tbl: "tidb_slow_query_cop_wait", labels: []string{"instance"}},
{name: "tidb_ddl_handle_job", tbl: "tidb_ddl", labels: []string{"instance", "type"}},
{name: "tidb_ddl_worker", tbl: "tidb_ddl_worker", labels: []string{"instance", "type", "result", "action"}},
{name: "tidb_ddl_update_self_version", tbl: "tidb_ddl_update_self_version", labels: []string{"instance", "result"}},
{name: "tidb_owner_handle_syncer", tbl: "tidb_owner_handle_syncer", labels: []string{"instance", "type", "result"}},
{name: "tidb_ddl_batch_add_index", tbl: "tidb_ddl_batch_add_index", labels: []string{"instance", "type"}},
{name: "tidb_ddl_deploy_syncer", tbl: "tidb_ddl_deploy_syncer", labels: []string{"instance", "type", "result"}},
{name: "tidb_load_schema", tbl: "tidb_load_schema", labels: []string{"instance"}},
{name: "tidb_meta_operation", tbl: "tidb_meta_operation", labels: []string{"instance", "type", "result"}},
{name: "tidb_auto_id_request", tbl: "tidb_auto_id_request", labels: []string{"instance", "type"}},
{name: "tidb_statistics_auto_analyze", tbl: "tidb_statistics_auto_analyze", labels: []string{"instance"}},
{name: "tidb_gc", tbl: "tidb_gc", labels: []string{"instance"}},
{name: "tidb_gc_push_task", tbl: "tidb_gc_push_task", labels: []string{"instance", "type"}},
{name: "tidb_batch_client_unavailable", tbl: "tidb_batch_client_unavailable", labels: []string{"instance"}},
{name: "tidb_batch_client_wait", tbl: "tidb_batch_client_wait", labels: []string{"instance"}},
{name: "tidb_batch_client_wait_conn", tbl: "tidb_batch_client_wait_conn", labels: []string{"instance"}},
{name: "pd_tso_rpc", tbl: "pd_tso_rpc", labels: []string{"instance"}},
{name: "pd_tso_wait", tbl: "pd_tso_wait", labels: []string{"instance"}},
}
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
table := TableDef{
Category: []string{CategoryTiDB},
Title: "tidb_time_consume",
Comment: ``,
joinColumns: []int{0, 1},
compareColumns: []int{3, 4, 5},
Column: []string{"METRIC_NAME", "LABEL", "TIME_RATIO", "TOTAL_TIME", "TOTAL_COUNT", "P999", "P99", "P90", "P80"},
}
resultRows := make([]TableRowDef, 0, len(defs))
arg := newQueryArg(startTime, endTime)
specialHandle := func(row []string) []string {
if arg.totalTime == 0 && len(row[3]) > 0 {
totalTime, err := strconv.ParseFloat(row[3], 64)
if err == nil {
arg.totalTime = totalTime
}
}
return row
}
appendRows := func(row TableRowDef) {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
resultRows = append(resultRows, row)
}
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetTiDBTxnTableData(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []totalValueAndTotalCountTableDef{
{name: "tidb_transaction_retry_num", tbl: "tidb_transaction_retry_num", sumTbl: "tidb_transaction_retry_total_num", countTbl: "tidb_transaction_retry_num_total_count", labels: []string{"instance"}},
{name: "tidb_transaction_statement_num", tbl: "tidb_transaction_statement_num", sumTbl: "tidb_transaction_statement_total_num", countTbl: "tidb_transaction_statement_num_total_count", labels: []string{"sql_type"}},
{name: "tidb_txn_region_num", tbl: "tidb_txn_region_num", sumTbl: "tidb_txn_region_total_num", countTbl: "tidb_txn_region_num_total_count", labels: []string{"instance"}},
{name: "tidb_txn_kv_write_num", tbl: "tidb_kv_write_num", sumTbl: "tidb_kv_write_total_num", countTbl: "tidb_kv_write_num_total_count", labels: []string{"instance"}},
{name: "tidb_txn_kv_write_size", tbl: "tidb_kv_write_size", sumTbl: "tidb_kv_write_total_size", countTbl: "tidb_kv_write_size_total_count", labels: []string{"instance"}},
}
defs2 := []sumValueQuery{
{name: "tidb_load_safepoint_total_num", tbl: "tidb_load_safepoint_total_num", labels: []string{"type"}},
{name: "tidb_lock_resolver_total_num", tbl: "tidb_lock_resolver_total_num", labels: []string{"type"}},
}
defs := make([]rowQuery, 0, len(defs1)+len(defs2))
for i := range defs1 {
defs = append(defs, defs1[i])
}
for i := range defs2 {
defs = append(defs, defs2[i])
}
resultRows := make([]TableRowDef, 0, len(defs))
quantiles := []float64{0.999, 0.99, 0.90, 0.80}
table := TableDef{
Category: []string{CategoryTiDB},
Title: "transaction",
Comment: ``,
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4, 5},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_VALUE", "TOTAL_COUNT", "P999", "P99", "P90", "P80"},
}
specialHandle := func(row []string) []string {
for len(row) < 8 {
row = append(row, "")
}
for i := 2; i < len(row); i++ {
if len(row[i]) == 0 {
continue
}
if row[0] == "tidb_txn_kv_write_size" && i != 3 {
row[i] = convertFloatToSize(row[i])
} else {
row[i] = convertFloatToInt(row[i])
}
}
return row
}
appendRows := func(row TableRowDef) {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
resultRows = append(resultRows, row)
}
arg := &queryArg{
startTime: startTime,
endTime: endTime,
quantiles: quantiles,
}
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetTiDBConnectionCountTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf("select instance, avg(value), max(value), min(value) from metrics_schema.tidb_connection_count where time >= '%s' and time < '%s' group by instance order by avg(value) desc",
startTime, endTime)
table := TableDef{
Category: []string{CategoryTiDB},
Title: "tidb_connection_count",
Comment: "",
joinColumns: []int{0},
compareColumns: []int{1, 2, 3},
Column: []string{"INSTANCE", "AVG", "MAX", "MIN"},
}
rows, err := getSQLRoundRows(db, sql, []int{1, 2, 3}, "")
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetTiDBStatisticsInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []sumValueQuery{
{name: "pseudo_estimation_total_count", tbl: "tidb_statistics_pseudo_estimation_total_count", labels: []string{"instance"}},
{name: "dump_feedback_total_count", tbl: "tidb_statistics_dump_feedback_total_count", labels: []string{"instance", "type"}},
{name: "store_query_feedback_total_count", tbl: "tidb_statistics_store_query_feedback_total_count", labels: []string{"instance", "type"}},
{name: "update_stats_total_count", tbl: "tidb_statistics_update_stats_total_count", labels: []string{"instance", "type"}},
}
table := TableDef{
Category: []string{CategoryTiDB},
Title: "statistics_info",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_COUNT"},
}
rows, err := getSumValueTableData(defs1, startTime, endTime, db)
if err != nil {
return table, err
}
table.Rows = rows
return table, err
}
func GetTiDBDDLOwner(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf("select min(time),instance from metrics_schema.tidb_ddl_worker_total_count where time>='%s' and time<'%s' and value>0 and type='run_job' group by instance order by min(time);",
startTime, endTime)
table := TableDef{
Category: []string{CategoryTiDB},
Title: "ddl_owner",
Comment: "",
joinColumns: []int{1},
Column: []string{"MIN_TIME", "DDL OWNER"},
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetPDConfigInfo(startTime, _ string, db *gorm.DB) (TableDef, error) {
table := TableDef{
Category: []string{CategoryConfig},
Title: "scheduler_initial_config",
Comment: "",
joinColumns: []int{0, 2},
compareColumns: []int{1},
Column: []string{"CONFIG_ITEM", "VALUE", "CURRENT_VALUE", "DIFF_WITH_CURRENT"},
}
sql := fmt.Sprintf(`select t1.type,t1.value,t2.value,t1.value!=t2.value from
(select distinct type,value from metrics_schema.pd_scheduler_config where time = '%[1]s' and value>0) as t1 join
(select distinct type,value from metrics_schema.pd_scheduler_config where time = now() and value>0) as t2
where t1.type=t2.type order by abs(t2.value-t1.value) desc`, startTime)
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
if len(rows) > 0 {
table.Rows = rows
}
return table, nil
}
func GetPDConfigChangeInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf(`select t1.* from
(select min(time) as time,type,value from metrics_schema.pd_scheduler_config where time>='%[1]s' and time<'%[2]s' group by type,value order by type) as t1 join
(select type, count(distinct value) as count from metrics_schema.pd_scheduler_config where time>='%[1]s' and time<'%[2]s' group by type order by count desc) as t2
where t1.type=t2.type and t2.count > 1 order by t2.count desc, t1.time;`, startTime, endTime)
table := TableDef{
Category: []string{CategoryConfig},
Title: "scheduler_change_config",
Comment: "",
joinColumns: []int{1},
compareColumns: []int{2},
Column: []string{"APPROXIMATE_CHANGE_TIME", "CONFIG_ITEM", "VALUE"},
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetTiDBGCConfigInfo(startTime, _ string, db *gorm.DB) (TableDef, error) {
table := TableDef{
Category: []string{CategoryConfig},
Title: "tidb_gc_initial_config",
Comment: "",
joinColumns: []int{0, 2},
compareColumns: []int{1},
Column: []string{"CONFIG_ITEM", "VALUE", "CURRENT_VALUE", "DIFF_WITH_CURRENT"},
}
sql := fmt.Sprintf(`select t1.type,t1.value,t2.value,t1.value!=t2.value from
(select distinct type,value from metrics_schema.tidb_gc_config where time = '%[1]s' and value>0) as t1 join
(select distinct type,value from metrics_schema.tidb_gc_config where time = now() and value>0) as t2
where t1.type=t2.type order by abs(t2.value-t1.value) desc`, startTime)
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetTiDBGCConfigChangeInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf(`select t1.* from
(select min(time) as time,type,value from metrics_schema.tidb_gc_config where time>='%[1]s' and time<'%[2]s' and value > 0 group by type,value order by type) as t1 join
(select type, count(distinct value) as count from metrics_schema.tidb_gc_config where time>='%[1]s' and time<'%[2]s' and value > 0 group by type order by count desc) as t2
where t1.type=t2.type and t2.count>1 order by t2.count desc, t1.time;`, startTime, endTime)
table := TableDef{
Category: []string{CategoryConfig},
Title: "tidb_gc_change_config",
Comment: ``,
joinColumns: []int{1},
compareColumns: []int{2},
Column: []string{"APPROXIMATE_CHANGE_TIME", "CONFIG_ITEM", "VALUE"},
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetTiKVRocksDBConfigInfo(startTime, _ string, db *gorm.DB) (TableDef, error) {
table := TableDef{
Category: []string{CategoryConfig},
Title: "tikv_rocksdb_initial_config",
Comment: "",
joinColumns: []int{0, 1, 3},
compareColumns: []int{2},
Column: []string{"CONFIG_ITEM", "INSTANCE", "VALUE", "CURRENT_VALUE", "DIFF_WITH_CURRENT", "DISTINCT_VALUES_IN_INSTANCE"},
}
sql := fmt.Sprintf(`select t1.name,'', t1.value,t2.value,t1.value!=t2.value, t1.count from
(select concat(name,' , ',cf) as name, min(value) as value, count(distinct value) as count from metrics_schema.tikv_config_rocksdb where time = '%[1]s' group by cf, name) as t1 join
(select concat(name,' , ',cf) as name, min(value) as value from metrics_schema.tikv_config_rocksdb where time = now() group by cf, name) as t2
where t1.name=t2.name order by abs(t2.value-t1.value) desc,t1.count desc, t1.name`, startTime)
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
// var subRows []TableRowDef
subRowsMap := make(map[string][][]string)
for i, row := range rows {
if len(row.Values) < 6 {
continue
}
if row.Values[5] == "1" {
continue
}
if len(subRowsMap) == 0 {
sql = fmt.Sprintf(`select t1.name,t1.instance,t1.value,t2.value,t1.value!=t2.value, '' from
(select concat(name,' , ',cf) as name,instance, value from metrics_schema.tikv_config_rocksdb where time = '%[1]s' group by cf, name, instance, value) as t1 join
(select concat(name,' , ',cf) as name,instance, value from metrics_schema.tikv_config_rocksdb where time = now() group by cf, name, instance, value) as t2
where t1.name=t2.name and t1.instance = t2.instance order by abs(t2.value-t1.value) desc, t1.name`, startTime)
subRows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
for _, subRow := range subRows {
if len(subRow.Values) < 6 {
continue
}
subRowsMap[subRow.Values[0]] = append(subRowsMap[subRow.Values[0]], subRow.Values)
}
}
rows[i].SubValues = subRowsMap[row.Values[0]]
if len(rows[i].SubValues) > 0 && row.Values[4] == "0" {
for _, subRow := range rows[i].SubValues {
if row.Values[4] != "0" {
break
}
if len(subRow) != 6 {
continue
}
rows[i].Values[4] = subRow[4]
}
}
}
table.Rows = rows
return table, nil
}
func GetTiKVRocksDBConfigChangeInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf(`select t1.* from
(select min(time) as time,concat(name,' , ',cf) as name,instance,value from metrics_schema.tikv_config_rocksdb where time>='%[1]s' and time<'%[2]s' group by name,cf,instance,value order by name) as t1 join
(select concat(name,' , ',cf) as name,instance, count(distinct value) as count from metrics_schema.tikv_config_rocksdb where time>='%[1]s' and time<'%[2]s' group by name,cf,instance order by count desc) as t2
where t1.name=t2.name and t1.instance = t2.instance and t2.count>1 order by t1.name,instance, t2.count desc, t1.time;`, startTime, endTime)
table := TableDef{
Category: []string{CategoryConfig},
Title: "tikv_rocksdb_change_config",
Comment: ``,
joinColumns: []int{1, 2},
compareColumns: []int{3},
Column: []string{"APPROXIMATE_CHANGE_TIME", "CONFIG_ITEM", "INSTANCE", "VALUE"},
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetTiKVRaftStoreConfigInfo(startTime, _ string, db *gorm.DB) (TableDef, error) {
table := TableDef{
Category: []string{CategoryConfig},
Title: "tikv_raftstore_initial_config",
Comment: "",
joinColumns: []int{0, 1, 3},
compareColumns: []int{2},
Column: []string{"CONFIG_ITEM", "INSTANCE", "VALUE", "CURRENT_VALUE", "DIFF_WITH_CURRENT", "DISTINCT_VALUES_IN_INSTANCE"},
}
sql := fmt.Sprintf(`select t1.name,'', t1.value,t2.value,t1.value!=t2.value, t1.count from
(select name, min(value) as value, count(distinct value) as count from metrics_schema.tikv_config_raftstore where time = '%[1]s' group by name) as t1 join
(select name, min(value) as value from metrics_schema.tikv_config_raftstore where time = now() group by name) as t2
where t1.name=t2.name order by abs(t2.value-t1.value) desc,t1.count desc, t1.name`, startTime)
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
// var subRows []TableRowDef
subRowsMap := make(map[string][][]string)
for i, row := range rows {
if len(row.Values) < 6 {
continue
}
if row.Values[5] == "1" {
continue
}
if len(subRowsMap) == 0 {
sql = fmt.Sprintf(`select t1.name,t1.instance,t1.value,t2.value,t1.value!=t2.value, '' from
(select name,instance, value from metrics_schema.tikv_config_raftstore where time = '%[1]s' group by name, instance, value) as t1 join
(select name,instance, value from metrics_schema.tikv_config_raftstore where time = now() group by name, instance, value) as t2
where t1.name=t2.name and t1.instance = t2.instance order by abs(t2.value-t1.value) desc, t1.name`, startTime)
subRows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
for _, subRow := range subRows {
if len(subRow.Values) < 6 {
continue
}
subRowsMap[subRow.Values[0]] = append(subRowsMap[subRow.Values[0]], subRow.Values)
}
}
rows[i].SubValues = subRowsMap[row.Values[0]]
if len(rows[i].SubValues) > 0 && row.Values[4] == "0" {
for _, subRow := range rows[i].SubValues {
if row.Values[4] != "0" {
break
}
if len(subRow) != 6 {
continue
}
rows[i].Values[4] = subRow[4]
}
}
}
table.Rows = rows
return table, nil
}
func GetTiKVRaftStoreConfigChangeInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf(`select t1.* from
(select min(time) as time,name,instance,value from metrics_schema.tikv_config_raftstore where time>='%[1]s' and time<'%[2]s' group by name,instance,value order by name) as t1 join
(select name,instance, count(distinct value) as count from metrics_schema.tikv_config_raftstore where time>='%[1]s' and time<'%[2]s' group by name,instance order by count desc) as t2
where t1.name=t2.name and t1.instance = t2.instance and t2.count>1 order by t1.name,instance,t2.count desc, t1.time;`, startTime, endTime)
table := TableDef{
Category: []string{CategoryConfig},
Title: "tikv_raftstore_change_config",
Comment: ``,
joinColumns: []int{1, 2},
compareColumns: []int{3},
Column: []string{"APPROXIMATE_CHANGE_TIME", "CONFIG_ITEM", "INSTANCE", "VALUE"},
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetPDTimeConsumeTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []totalTimeByLabelsTableDef{
{name: "pd_client_cmd", tbl: "pd_client_cmd", labels: []string{"instance", "type"}},
{name: "pd_client_request_rpc", tbl: "pd_request_rpc", labels: []string{"instance", "type"}},
{name: "pd_grpc_completed_commands", tbl: "pd_grpc_completed_commands", labels: []string{"instance", "grpc_method"}},
{name: "pd_operator_finish", tbl: "pd_operator_finish", labels: []string{"type"}},
{name: "pd_operator_step_finish", tbl: "pd_operator_step_finish", labels: []string{"type"}},
{name: "pd_handle_transactions", tbl: "pd_handle_transactions", labels: []string{"instance", "result"}},
{name: "pd_region_heartbeat", tbl: "pd_region_heartbeat", labels: []string{"address", "store"}},
{name: "etcd_wal_fsync", tbl: "etcd_wal_fsync", labels: []string{"instance"}},
{name: "pd_peer_round_trip", tbl: "pd_peer_round_trip", labels: []string{"instance", "To"}},
}
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
table := TableDef{
Category: []string{CategoryPD},
Title: "pd_time_consume",
Comment: ``,
joinColumns: []int{0, 1},
compareColumns: []int{3, 4, 5},
Column: []string{"METRIC_NAME", "LABEL", "TIME_RATIO", "TOTAL_TIME", "TOTAL_COUNT", "P999", "P99", "P90", "P80"},
}
resultRows := make([]TableRowDef, 0, len(defs))
arg := newQueryArg(startTime, endTime)
appendRows := func(row TableRowDef) {
resultRows = append(resultRows, row)
arg.totalTime = 0
}
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetPDSchedulerInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []sumValueQuery{
{name: "blance-leader-in", tbl: "pd_scheduler_balance_leader", condition: "type='move-leader' and address like '%-in'", labels: []string{"address"}},
{name: "blance-leader-out", tbl: "pd_scheduler_balance_leader", condition: "type='move-leader' and address like '%-out'", labels: []string{"address"}},
{name: "blance-region-in", tbl: "pd_scheduler_balance_region", condition: "type='move-peer' and address like '%-in'", labels: []string{"address"}},
{name: "blance-region-out", tbl: "pd_scheduler_balance_region", condition: "type='move-peer' and address like '%-out'", labels: []string{"address"}},
}
table := TableDef{
Category: []string{CategoryPD},
Title: "balance_leader_region",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_COUNT"},
}
rows, err := getSumValueTableData(defs1, startTime, endTime, db)
if err != nil {
return table, err
}
table.Rows = rows
return table, err
}
func GetTiKVRegionSizeInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []totalValueAndTotalCountTableDef{
{name: "Approximate Region size", tbl: "tikv_approximate_region_size", sumTbl: "tikv_approximate_region_total_size", countTbl: "tikv_approximate_region_size_total_count", labels: []string{"instance"}},
}
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
resultRows := make([]TableRowDef, 0, len(defs))
specialHandle := func(row []string) []string {
if len(row) == 8 {
// total value and total count is not right.
tmpRow := row[:2]
tmpRow = append(tmpRow, row[4:]...)
row = tmpRow
}
for i := 2; i < len(row); i++ {
if len(row[i]) == 0 {
continue
}
row[i] = convertFloatToSize(row[i])
}
return row
}
appendRows := func(row TableRowDef) {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
resultRows = append(resultRows, row)
}
quantiles := []float64{0.99, 0.90, 0.80, 0.50}
arg := &queryArg{
startTime: startTime,
endTime: endTime,
quantiles: quantiles,
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "approximate_region_size",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4, 5},
Column: []string{"METRIC_NAME", "LABEL", "P99", "P90", "P80", "P50"},
}
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetTiKVStoreInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []sumValueQuery{
{name: "store size", tbl: "tikv_engine_size", labels: []string{"instance", "type"}},
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "tikv_engine_size",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_COUNT"},
}
rows, err := getSumValueTableData(defs1, startTime, endTime, db)
if err != nil {
return table, err
}
convertFloatToSizeByRows(rows, 2)
table.Rows = rows
return table, nil
}
func GetTiKVTotalTimeConsumeTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []totalTimeByLabelsTableDef{
{name: "tikv_grpc_message", tbl: "tikv_grpc_message", labels: []string{"instance", "type"}},
{name: "tikv_cop_request", tbl: "tikv_cop_request", labels: []string{"instance", "req"}},
{name: "tikv_cop_handle", tbl: "tikv_cop_handle", labels: []string{"instance", "req"}},
{name: "tikv_cop_wait", tbl: "tikv_cop_wait", labels: []string{"instance", "req"}},
{name: "tikv_scheduler_command", tbl: "tikv_scheduler_command", labels: []string{"instance", "type"}},
{name: "tikv_scheduler_latch_wait", tbl: "tikv_scheduler_latch_wait", labels: []string{"instance", "type"}},
{name: "tikv_storage_async_request", tbl: "tikv_storage_async_request", labels: []string{"instance", "type"}},
{name: "tikv_scheduler_processing_read", tbl: "tikv_scheduler_processing_read", labels: []string{"type"}},
{name: "tikv_raft_propose_wait", tbl: "tikv_raftstore_propose_wait", labels: []string{"instance"}},
{name: "tikv_raft_process", tbl: "tikv_raftstore_process", labels: []string{"instance", "type"}},
{name: "tikv_raft_append_log", tbl: "tikv_raftstore_append_log", labels: []string{"instance"}},
{name: "tikv_raft_commit_log", tbl: "tikv_raftstore_commit_log", labels: []string{"instance"}},
{name: "tikv_raft_apply_wait", tbl: "tikv_raftstore_apply_wait", labels: []string{"instance"}},
{name: "tikv_raft_apply_log", tbl: "tikv_raftstore_apply_log", labels: []string{"instance"}},
{name: "tikv_raft_store_events", tbl: "tikv_raft_store_events", labels: []string{"instance", "type"}},
{name: "tikv_handle_snapshot", tbl: "tikv_handle_snapshot", labels: []string{"instance", "type"}},
{name: "tikv_send_snapshot", tbl: "tikv_send_snapshot", labels: []string{"instance"}},
{name: "tikv_check_split", tbl: "tikv_check_split", labels: []string{"instance"}},
{name: "tikv_ingest_sst", tbl: "tikv_ingest_sst", labels: []string{"instance"}},
{name: "tikv_gc_tasks", tbl: "tikv_gc_tasks", labels: []string{"instance", "task"}},
{name: "tikv_pd_request", tbl: "tikv_pd_request", labels: []string{"instance", "type"}},
{name: "tikv_lock_manager_deadlock_detect", tbl: "tikv_lock_manager_deadlock_detect", labels: []string{"instance"}},
{name: "tikv_lock_manager_waiter_lifetime", tbl: "tikv_lock_manager_waiter_lifetime", labels: []string{"instance"}},
{name: "tikv_backup_range", tbl: "tikv_backup_range", labels: []string{"instance", "type"}},
{name: "tikv_backup", tbl: "tikv_backup", labels: []string{"instance"}},
}
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "tikv_time_consume",
Comment: ``,
joinColumns: []int{0, 1},
compareColumns: []int{3, 4, 5},
Column: []string{"METRIC_NAME", "LABEL", "TIME_RATIO", "TOTAL_TIME", "TOTAL_COUNT", "P999", "P99", "P90", "P80"},
}
resultRows := make([]TableRowDef, 0, len(defs))
arg := newQueryArg(startTime, endTime)
specialHandle := func(row []string) []string {
if arg.totalTime == 0 && len(row[3]) > 0 {
totalTime, err := strconv.ParseFloat(row[3], 64)
if err == nil {
arg.totalTime = totalTime
}
}
return row
}
appendRows := func(row TableRowDef) {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
resultRows = append(resultRows, row)
}
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetTiKVSchedulerInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []totalValueAndTotalCountTableDef{
{name: "tikv_scheduler_keys_read", tbl: "tikv_scheduler_keys_read", sumTbl: "tikv_scheduler_keys_total_read", countTbl: "tikv_scheduler_keys_read_total_count", labels: []string{"instance", "type"}},
{name: "tikv_scheduler_keys_written", tbl: "tikv_scheduler_keys_written", sumTbl: "tikv_scheduler_keys_total_written", countTbl: "tikv_scheduler_keys_written_total_count", labels: []string{"instance", "type"}},
}
defs2 := []sumValueQuery{
{tbl: "tikv_scheduler_scan_details_total_num", labels: []string{"instance", "req", "tag"}},
{tbl: "tikv_scheduler_stage_total_num", labels: []string{"instance", "type", "stage"}},
}
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
for i := range defs2 {
defs = append(defs, defs2[i])
}
resultRows := make([]TableRowDef, 0, len(defs))
specialHandle := func(row []string) []string {
for len(row) < 8 {
row = append(row, "")
}
return row
}
appendRows := func(row TableRowDef) {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
resultRows = append(resultRows, row)
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "scheduler_info",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4, 5},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_VALUE", "TOTAL_COUNT", "P999", "P99", "P90", "P80"},
}
arg := newQueryArg(startTime, endTime)
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetTiKVGCInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []sumValueQuery{
{tbl: "tikv_gc_keys_total_num", labels: []string{"instance", "cf", "tag"}},
{name: "tidb_gc_worker_action_total_num", tbl: "tidb_gc_worker_action_opm", labels: []string{"instance", "type"}},
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "gc_info",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_VALUE"},
}
rows, err := getSumValueTableData(defs1, startTime, endTime, db)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetTiKVTaskInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []sumValueQuery{
{tbl: "tikv_worker_handled_tasks_total_num", labels: []string{"instance", "name"}},
{tbl: "tikv_worker_pending_tasks_total_num", labels: []string{"instance", "name"}},
{tbl: "tikv_futurepool_handled_tasks_total_num", labels: []string{"instance", "name"}},
{tbl: "tikv_futurepool_pending_tasks_total_num", labels: []string{"instance", "name"}},
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "task_info",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_VALUE"},
}
rows, err := getSumValueTableData(defs1, startTime, endTime, db)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func getSumValueTableData(defs1 []sumValueQuery, startTime, endTime string, db *gorm.DB) ([]TableRowDef, error) {
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
resultRows := make([]TableRowDef, 0, len(defs))
specialHandle := func(row []string) []string {
for len(row) < 3 {
return row
}
row[2] = convertFloatToInt(row[2])
return row
}
appendRows := func(row TableRowDef) {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
resultRows = append(resultRows, row)
}
arg := newQueryArg(startTime, endTime)
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return nil, err
}
return resultRows, nil
}
func GetTiKVSnapshotInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []totalValueAndTotalCountTableDef{
{name: "tikv_snapshot_kv_count", tbl: "tikv_snapshot_kv_count", sumTbl: "tikv_snapshot_kv_total_count", countTbl: "tikv_snapshot_kv_count_total_count", labels: []string{"instance"}},
{name: "tikv_snapshot_size", tbl: "tikv_snapshot_size", sumTbl: "tikv_snapshot_total_size", countTbl: "tikv_snapshot_size_total_count", labels: []string{"instance"}},
}
defs2 := []sumValueQuery{
{tbl: "tikv_snapshot_state_total_count", labels: []string{"instance", "type"}},
}
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
for i := range defs2 {
defs = append(defs, defs2[i])
}
resultRows := make([]TableRowDef, 0, len(defs))
specialHandle := func(row []string) []string {
for len(row) < 8 {
row = append(row, "")
}
return row
}
appendRows := func(row TableRowDef) {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
resultRows = append(resultRows, row)
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "snapshot_info",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4, 5},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_VALUE", "TOTAL_COUNT", "P999", "P99", "P90", "P80"},
}
arg := newQueryArg(startTime, endTime)
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetTiKVCopInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []sumValueQuery{
{name: "tikv_cop_scan_keys_num", tbl: "tikv_cop_scan_keys_total_num", labels: []string{"instance", "req"}},
{tbl: "tikv_cop_total_response_total_size", labels: []string{"instance"}},
{name: "tikv_cop_scan_num", tbl: "tikv_cop_scan_details_total", labels: []string{"instance", "req", "tag", "cf"}},
}
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
resultRows := make([]TableRowDef, 0, len(defs))
appendRows := func(row TableRowDef) {
if len(row.Values) == 3 && row.Values[0] == "tikv_cop_total_response_total_size" {
convertFloatToSizeByRow(&row, 2)
}
resultRows = append(resultRows, row)
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "coprocessor_info",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_VALUE"},
}
arg := newQueryArg(startTime, endTime)
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetTiKVRaftInfo(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []sumValueQuery{
{tbl: "tikv_raft_sent_messages_total_num", labels: []string{"instance", "type"}},
{tbl: "tikv_flush_messages_total_num", labels: []string{"instance"}},
{tbl: "tikv_receive_messages_total_num", labels: []string{"instance"}},
{tbl: "tikv_raft_dropped_messages_total", labels: []string{"instance", "type"}},
{tbl: "tikv_raft_proposals_total_num", labels: []string{"instance", "type"}},
}
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
resultRows := make([]TableRowDef, 0, len(defs))
appendRows := func(row TableRowDef) {
resultRows = append(resultRows, row)
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "raft_info",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_VALUE"},
}
arg := newQueryArg(startTime, endTime)
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetTiKVErrorTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []sumValueQuery{
{tbl: "tikv_grpc_error_total_count", labels: []string{"instance", "type"}},
{tbl: "tikv_critical_error_total_count", labels: []string{"instance", "type"}},
{tbl: "tikv_scheduler_is_busy_total_count", labels: []string{"instance", "db", "type", "stage"}},
{tbl: "tikv_channel_full_total_count", labels: []string{"instance", "db", "type"}},
{tbl: "tikv_coprocessor_request_error_total_count", labels: []string{"instance", "reason"}},
{tbl: "tikv_engine_write_stall", labels: []string{"instance", "db"}},
{tbl: "tikv_server_report_failures_total_count", labels: []string{"instance"}},
{name: "tikv_storage_async_request_error", tbl: "tikv_storage_async_requests_total_count", labels: []string{"instance", "status", "type"}, condition: "status not in ('all','success')"},
{tbl: "tikv_lock_manager_detect_error_total_count", labels: []string{"instance", "type"}},
{tbl: "tikv_backup_errors_total_count", labels: []string{"instance", "error"}},
}
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "tikv_error",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2},
Column: []string{"METRIC_NAME", "LABEL", "TOTAL_COUNT"},
}
resultRows := make([]TableRowDef, 0, len(defs))
specialHandle := func(row []string) []string {
row[2] = convertFloatToInt(row[2])
return row
}
appendRows := func(row TableRowDef) {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
resultRows = append(resultRows, row)
}
arg := &queryArg{
startTime: startTime,
endTime: endTime,
}
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return table, err
}
table.Rows = resultRows
return table, nil
}
func GetTiDBCurrentConfig(_, _ string, db *gorm.DB) (TableDef, error) {
sql := "select `key`,`value` from information_schema.CLUSTER_CONFIG where type='tidb' group by `key`,`value` order by `key`;"
table := TableDef{
Category: []string{CategoryConfig},
Title: "tidb_current_config",
Comment: "",
Column: []string{"KEY", "VALUE"},
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetPDCurrentConfig(_, _ string, db *gorm.DB) (TableDef, error) {
sql := "select `key`,`value` from information_schema.CLUSTER_CONFIG where type='pd' group by `key`,`value` order by `key`;"
table := TableDef{
Category: []string{CategoryConfig},
Title: "pd_current_config",
Comment: "",
Column: []string{"KEY", "VALUE"},
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetTiKVCurrentConfig(_, _ string, db *gorm.DB) (TableDef, error) {
sql := "select `key`,`value` from information_schema.CLUSTER_CONFIG where type='tikv' group by `key`,`value` order by `key`;"
table := TableDef{
Category: []string{CategoryConfig},
Title: "tikv_current_config",
Comment: "",
Column: []string{"KEY", "VALUE"},
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func getSQLRows(db *gorm.DB, sql string) ([]TableRowDef, error) {
rows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
resultRows := make([]TableRowDef, len(rows))
for i := range rows {
resultRows[i] = TableRowDef{Values: rows[i]}
}
return resultRows, nil
}
func getSQLRoundRows(db *gorm.DB, sql string, nums []int, comment string) ([]TableRowDef, error) {
rows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
for _, i := range nums {
for _, row := range rows {
row[i] = RoundFloatString(row[i])
}
}
resultRows := make([]TableRowDef, len(rows))
for i := range rows {
resultRows[i] = TableRowDef{Values: rows[i], Comment: comment}
}
return resultRows, nil
}
func getTableRows(defs []rowQuery, arg *queryArg, db *gorm.DB, appendRows func(def TableRowDef)) error {
for _, def := range defs {
row, err := def.queryRow(arg, db)
if err != nil {
fmt.Println(err)
continue
}
if row == nil {
continue
}
appendRows(*row)
}
return nil
}
func NewTableRowDef(values []string, subValues [][]string) TableRowDef {
return TableRowDef{
Values: values,
SubValues: subValues,
}
}
func getAvgValueTableData(defs1 []AvgMaxMinTableDef, startTime, endTime string, db *gorm.DB) ([]TableRowDef, error) {
defs := make([]rowQuery, 0, len(defs1))
for i := range defs1 {
defs = append(defs, defs1[i])
}
resultRows := make([]TableRowDef, 0, len(defs))
appendRows := func(row TableRowDef) {
resultRows = append(resultRows, row)
}
arg := newQueryArg(startTime, endTime)
err := getTableRows(defs, arg, db, appendRows)
if err != nil {
return nil, err
}
return resultRows, nil
}
func GetLoadTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []AvgMaxMinTableDef{
{name: "node_disk_io_utilization", tbl: "node_disk_io_util", labels: []string{"instance", "device"}},
{name: "node_disk_write_latency", tbl: "node_disk_write_latency", labels: []string{"instance", "device"}},
{name: "node_disk_read_latency", tbl: "node_disk_read_latency", labels: []string{"instance", "device"}},
{name: "tikv_disk_read_bytes", tbl: "tikv_disk_read_bytes", labels: []string{"instance", "device"}},
{name: "tikv_disk_write_bytes", tbl: "tikv_disk_write_bytes", labels: []string{"instance", "device"}},
{name: "node_network_in_traffic", tbl: "node_network_in_traffic", labels: []string{"instance", "device"}},
{name: "node_network_out_traffic", tbl: "node_network_out_traffic", labels: []string{"instance", "device"}},
{name: "node_tcp_in_use", tbl: "node_tcp_in_use", labels: []string{"instance"}},
{name: "node_tcp_connections", tbl: "node_tcp_connections", labels: []string{"instance"}},
}
table := TableDef{
Category: []string{CategoryLoad},
Title: "node_load_info",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4},
Column: []string{"METRIC_NAME", "instance", "AVG", "MAX", "MIN"},
}
rows := make([]TableRowDef, 0, 4)
// get cpu usage
row, err := getAvgMaxMinCPUUsage(startTime, endTime, db)
if err != nil {
return table, err
}
rows = append(rows, *row)
// get memory usage
row, err = getAvgMaxMinMemoryUsage(startTime, endTime, db)
if err != nil {
return table, err
}
rows = append(rows, *row)
partRows, err := getAvgValueTableData(defs1, startTime, endTime, db)
if err != nil {
return table, err
}
specialHandle := func(row []string) []string {
if len(row) < 5 {
return row
}
for i := 2; i < 5; i++ {
if len(row[i]) == 0 {
continue
}
switch row[0] {
case "node_disk_io_utilization":
f, err := strconv.ParseFloat(row[i], 64)
if err != nil {
return row
}
row[i] = convertFloatToString(f*100) + "%"
case "node_disk_write_latency", "node_disk_read_latency":
row[i] = convertFloatToDuration(row[i], float64(1))
case "node_tcp_in_use", "node_tcp_connections":
row[i] = convertFloatToInt(row[i])
default:
row[i] = convertFloatToSize(row[i])
}
}
return row
}
for _, row := range partRows {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
}
rows = append(rows, partRows...)
table.Rows = rows
return table, nil
}
func getAvgMaxMinCPUUsage(startTime, endTime string, db *gorm.DB) (*TableRowDef, error) {
condition := fmt.Sprintf("where time >= '%s' and time < '%s' ", startTime, endTime)
sql := fmt.Sprintf("select 'node_cpu_usage', '', 100-avg(value),100-min(value),100-max(value) from metrics_schema.node_cpu_usage %s and mode='idle'", condition)
rows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
sql = fmt.Sprintf("select 'node_cpu_usage', instance, 100-avg(value) as avg_value,100-min(value),100-max(value) from metrics_schema.node_cpu_usage %s and mode='idle' group by instance order by avg_value desc", condition)
subRows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
specialHandle := func(row []string) []string {
if len(row) == 0 {
return row
}
for i := 2; i <= 4; i++ {
if len(row[i]) > 0 {
row[i] = RoundFloatString(row[i]) + "%"
}
}
return row
}
rows[0] = specialHandle(rows[0])
for i := range subRows {
subRows[i] = specialHandle(subRows[i])
}
return &TableRowDef{
Values: rows[0],
SubValues: subRows,
}, nil
}
func getAvgMaxMinMemoryUsage(startTime, endTime string, db *gorm.DB) (*TableRowDef, error) {
condition := fmt.Sprintf("where time >= '%s' and time < '%s' ", startTime, endTime)
sql := fmt.Sprintf(`select 'node_mem_usage','', 100*(1-t1.avg_value/t2.total),100*(1-t1.min_value/t2.total), 100*(1-t1.max_value/t2.total) from
(select avg(value) as avg_value,max(value) as max_value,min(value) as min_value from metrics_schema.node_memory_available %[1]s) as t1 join
(select max(value) as total from metrics_schema.node_total_memory %[1]s) as t2;`, condition)
rows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
sql = fmt.Sprintf(`select 'node_mem_usage',t1.instance, 100*(1-t1.avg_value/t2.total) as avg_value, 100*(1-t1.min_value/t2.total), 100*(1-t1.max_value/t2.total) from
(select instance, avg(value) as avg_value,max(value) as max_value,min(value) as min_value from metrics_schema.node_memory_available %[1]s GROUP BY instance) as t1 join
(select instance, max(value) as total from metrics_schema.node_total_memory %[1]s GROUP BY instance) as t2 where t1.instance = t2.instance order by avg_value desc;`, condition)
subRows, err := querySQL(db, sql)
if err != nil {
return nil, err
}
specialHandle := func(row []string) []string {
if len(row) == 0 {
return row
}
for i := 2; i <= 4; i++ {
if len(row[i]) > 0 {
row[i] = RoundFloatString(row[i]) + "%"
}
}
return row
}
rows[0] = specialHandle(rows[0])
for i := range subRows {
subRows[i] = specialHandle(subRows[i])
}
return &TableRowDef{
Values: rows[0],
SubValues: subRows,
}, nil
}
func GetCPUUsageTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf("select instance, job, avg(value),max(value),min(value) from metrics_schema.process_cpu_usage where time >= '%s' and time < '%s' and job not in ('overwritten-nodes','overwritten-cluster') group by instance, job order by avg(value) desc",
startTime, endTime)
table := TableDef{
Category: []string{CategoryLoad},
Title: "process_cpu_usage",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4},
Column: []string{"INSTANCE", "JOB", "AVG", "MAX", "MIN"},
}
rows, err := getSQLRoundRows(db, sql, []int{2, 3, 4}, "")
if err != nil {
return table, err
}
specialHandle := func(row []string) []string {
if len(row) < 5 {
return row
}
for i := 2; i < 5; i++ {
f, err := strconv.ParseFloat(row[i], 64)
if err != nil {
return row
}
row[i] = convertFloatToString(f*100) + "%"
}
return row
}
for i := range rows {
rows[i].Values = specialHandle(rows[i].Values)
}
table.Rows = rows
return table, nil
}
func GetProcessMemUsageTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf("select instance, job, avg(value),max(value),min(value) from metrics_schema.tidb_process_mem_usage where time >= '%s' and time < '%s' and job not in ('overwritten-nodes','overwritten-cluster') group by instance, job order by avg(value) desc",
startTime, endTime)
table := TableDef{
Category: []string{CategoryLoad},
Title: "process_memory_usage",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4},
Column: []string{"INSTANCE", "JOB", "AVG", "MAX", "MIN"},
}
rows, err := getSQLRoundRows(db, sql, []int{2, 3, 4}, "")
if err != nil {
return table, err
}
specialHandle := func(row []string) []string {
if len(row) < 5 {
return row
}
for i := 2; i < 5; i++ {
row[i] = convertFloatToSize(row[i])
}
return row
}
for i := range rows {
rows[i].Values = specialHandle(rows[i].Values)
}
table.Rows = rows
return table, nil
}
func GetGoroutinesCountTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf("select instance, job, avg(value), max(value), min(value) from metrics_schema.goroutines_count where job in ('tidb','pd') and time >= '%s' and time < '%s' group by instance, job order by avg(value) desc",
startTime, endTime)
table := TableDef{
Category: []string{CategoryLoad},
Title: "tidb/pd_goroutines_count",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4},
Column: []string{"INSTANCE", "JOB", "AVG", "MAX", "MIN"},
}
rows, err := getSQLRoundRows(db, sql, []int{2, 3, 4}, "")
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetTiKVThreadCPUTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs := []AvgMaxMinTableDef{
{name: "grpc", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'grpc%'"},
{name: "raftstore", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'raftstore_%'"},
{name: "Async apply", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'apply%'"},
{name: "sched_worker", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'sched_%'"},
{name: "snapshot", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'snap%'"},
{name: "unified read pool", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'unified_read_po%'"},
{name: "storage read pool", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'store_read%'"},
{name: "storage read pool normal", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'store_read_norm%'"},
{name: "storage read pool high", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'store_read_high%'"},
{name: "storage read pool low", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'store_read_low%'"},
{name: "cop", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'cop%'"},
{name: "cop normal", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'cop_normal%'"},
{name: "cop high", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'cop_high%'"},
{name: "cop low", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'cop_low%'"},
{name: "rocksdb", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'rocksdb%'"},
{name: "gc", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name like 'gc_worker%'"},
{name: "split_check", tbl: "tikv_thread_cpu", labels: []string{"instance"}, condition: "name = 'split_check'"},
}
configKeys := map[string]string{
"grpc": "server.grpc-concurrency",
"sched_worker": "storage.scheduler-worker-pool-size",
"raftstore": "raftstore.store-pool-size",
"Async apply": "raftstore.apply-pool-size",
"unified read pool": "readpool.unified.max-thread-count",
"storage read pool high": "readpool.storage.high-concurrency",
"storage read pool low": "readpool.storage.low-concurrency",
"storage read pool normal": "readpool.storage.normal-concurrency",
"cop high": "readpool.coprocessor.high-concurrency",
"cop low": "readpool.coprocessor.low-concurrency",
"cop normal": "readpool.coprocessor.normal-concurrency",
}
table := TableDef{
Category: []string{CategoryLoad},
Title: "tikv_thread_cpu_usage",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4},
Column: []string{"METRIC_NAME", "INSTANCE", "AVG", "MAX", "MIN", "CONFIG_KEY", "CURRENT_CONFIG_VALUE"},
}
type instanceKey struct {
instance string
key string
}
var keysBuf bytes.Buffer
idx := 0
for _, v := range configKeys {
if idx > 0 {
keysBuf.WriteByte(',')
}
keysBuf.WriteByte('\'')
keysBuf.WriteString(v)
keysBuf.WriteByte('\'')
idx++
}
sql := fmt.Sprintf("select t2.status_address, t1.`key`,t1.value from (select instance, `key`,value from information_schema.cluster_config where type='tikv' and `key` in (%s) ) as t1 join "+
"(select instance,status_address from information_schema.cluster_info where type='tikv') as t2 where t1.instance=t2.instance", keysBuf.String())
rows, err := querySQL(db, sql)
if err != nil {
return table, err
}
cfgMap := make(map[instanceKey]string)
for _, row := range rows {
cfgMap[instanceKey{
instance: row[0],
key: row[1],
}] = row[2]
}
specialHandle := func(row []string) []string {
if len(row) < 7 {
return row
}
for i := 2; i < 5; i++ {
f, err := strconv.ParseFloat(row[i], 64)
if err != nil {
return row
}
row[i] = convertFloatToString(f*100) + "%"
}
// get config value
if cfgValue, ok := cfgMap[instanceKey{
instance: row[1],
key: configKeys[row[0]],
}]; ok {
row[5] = configKeys[row[0]]
row[6] = cfgValue
}
return row
}
resultRows := make([]TableRowDef, 0, len(defs))
appendRows := func(row TableRowDef) {
row.Values = specialHandle(row.Values)
for i := range row.SubValues {
row.SubValues[i] = specialHandle(row.SubValues[i])
}
resultRows = append(resultRows, row)
}
for _, def := range defs {
condition := fmt.Sprintf("where time >= '%s' and time < '%s' ", startTime, endTime)
if len(def.condition) > 0 {
condition = condition + "and " + def.condition
}
sql := fmt.Sprintf("select '%[1]s', '', avg(sum_value),max(sum_value),min(sum_value),'','' from ( select sum(value) as sum_value from metrics_schema.%[2]s %[3]s group by %[4]s, time) as t1",
def.name, def.tbl, condition, def.labels[0])
rows, err := querySQL(db, sql)
if err != nil {
return table, err
}
if len(rows) == 0 {
continue
}
sql = fmt.Sprintf("select '%[1]s', %[2]s,avg(sum_value),max(sum_value),min(sum_value),'','' from ( select %[2]s,sum(value) as sum_value from metrics_schema.%[3]s %[4]s group by %[2]s,time) as t1 group by %[2]s order by avg(sum_value) desc",
def.name, def.labels[0], def.tbl, condition)
subRows, err := querySQL(db, sql)
if err != nil {
return table, err
}
appendRows(TableRowDef{
Values: rows[0],
SubValues: subRows,
Comment: def.Comment,
})
}
sortRowsByIndex(resultRows, 2)
table.Rows = resultRows
return table, nil
}
func GetStoreStatusTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs1 := []AvgMaxMinTableDef{
{name: "region_score", tbl: "pd_scheduler_store_status", condition: "type = 'region_score'", labels: []string{"address"}},
{name: "leader_score", tbl: "pd_scheduler_store_status", condition: "type = 'leader_score'", labels: []string{"address"}},
{name: "region_count", tbl: "pd_scheduler_store_status", condition: "type = 'region_count'", labels: []string{"address"}},
{name: "leader_count", tbl: "pd_scheduler_store_status", condition: "type = 'leader_count'", labels: []string{"address"}},
{name: "region_size", tbl: "pd_scheduler_store_status", condition: "type = 'region_size'", labels: []string{"address"}},
{name: "leader_size", tbl: "pd_scheduler_store_status", condition: "type = 'leader_size'", labels: []string{"address"}},
}
table := TableDef{
Category: []string{CategoryPD},
Title: "store_status",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4},
Column: []string{"METRIC_NAME", "INSTANCE", "AVG", "MAX", "MIN"},
}
rows, err := getAvgValueTableData(defs1, startTime, endTime, db)
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetPDClusterStatusTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf("select type, max(value), min(value) from metrics_schema.pd_cluster_status where time >= '%s' and time < '%s' group by type",
startTime, endTime)
table := TableDef{
Category: []string{CategoryPD},
Title: "cluster_status",
Comment: "",
joinColumns: []int{0},
compareColumns: []int{1, 2},
Column: []string{"TYPE", "MAX", "MIN"},
}
rows, err := getSQLRoundRows(db, sql, []int{1, 2}, "")
if err != nil {
return table, err
}
for i := range rows {
if len(rows[i].Values) != 3 {
continue
}
switch rows[i].Values[0] {
case "store_disconnected_count":
case "leader_count":
rows[i].Comment = "The total number of leader Regions"
case "store_up_count":
rows[i].Comment = "The count of healthy stores"
case "storage_capacity", "storage_size":
rows[i].Values[1] = convertFloatToSize(rows[i].Values[1])
rows[i].Values[2] = convertFloatToSize(rows[i].Values[2])
}
}
table.Rows = rows
return table, nil
}
func GetPDEtcdStatusTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf("select type, max(value), min(value) from metrics_schema.pd_server_etcd_state where time >= '%s' and time < '%s' group by type",
startTime, endTime)
table := TableDef{
Category: []string{CategoryPD},
Title: "etcd_status",
Comment: "",
joinColumns: []int{0},
compareColumns: []int{1, 2},
Column: []string{"TYPE", "MAX", "MIN"},
}
rows, err := getSQLRoundRows(db, sql, []int{1, 2}, "")
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetClusterInfoTable(_, _ string, db *gorm.DB) (TableDef, error) {
sql := "SELECT `TYPE`,`INSTANCE`,`STATUS_ADDRESS`,`VERSION`,`GIT_HASH`,`START_TIME`,`UPTIME`,`SERVER_ID` FROM information_schema.cluster_info ORDER BY `TYPE`,`START_TIME` DESC"
table := TableDef{
Category: []string{CategoryHeader},
Title: "cluster_info",
Comment: "",
joinColumns: []int{0, 1, 2, 3, 4},
Column: []string{"TYPE", "INSTANCE", "STATUS_ADDRESS", "VERSION", "GIT_HASH", "START_TIME", "UPTIME", "SERVER_ID"},
}
rows, err := getSQLRoundRows(db, sql, nil, "")
if err != nil {
return table, err
}
table.Rows = rows
return table, nil
}
func GetTiKVCacheHitTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
tables := []AvgMaxMinTableDef{
{name: "tikv_memtable_hit", tbl: "tikv_memtable_hit", labels: []string{"instance"}},
{name: "tikv_block_all_cache_hit", tbl: "tikv_block_all_cache_hit", labels: []string{"instance"}},
{name: "tikv_block_index_cache_hit", tbl: "tikv_block_index_cache_hit", labels: []string{"instance"}},
{name: "tikv_block_filter_cache_hit", tbl: "tikv_block_filter_cache_hit", labels: []string{"instance"}},
{name: "tikv_block_data_cache_hit", tbl: "tikv_block_data_cache_hit", labels: []string{"instance"}},
{name: "tikv_block_bloom_prefix_cache_hit", tbl: "tikv_block_bloom_prefix_cache_hit", labels: []string{"instance"}},
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "cache_hit",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4},
Column: []string{"METRIC_NAME", "INSTANCE", "AVG", "MAX", "MIN"},
}
rows, err := getAvgValueTableData(tables, startTime, endTime, db)
if err != nil {
return table, err
}
specialHandle := func(row []string) []string {
if len(row) < 5 {
return row
}
for i := 2; i < 5; i++ {
f, err := strconv.ParseFloat(row[i], 64)
if err != nil {
return row
}
row[i] = convertFloatToString(f*100) + "%"
}
return row
}
for i := range rows {
rows[i].Values = specialHandle(rows[i].Values)
for j := range rows[i].SubValues {
rows[i].SubValues[j] = specialHandle(rows[i].SubValues[j])
}
}
table.Rows = rows
return table, nil
}
type hardWare struct {
instance string
Type map[string]int
cpu map[string]int
memory float64
disk map[string]float64
uptime string
}
func GetClusterHardwareInfoTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
resultRows := make([]TableRowDef, 0, 1)
table := TableDef{
Category: []string{CategoryHeader},
Title: "cluster_hardware",
Comment: "",
Column: []string{"HOST", "INSTANCE", "CPU_CORES", "MEMORY (GB)", "DISK (GB)", "UPTIME (DAY)"},
}
sql := `SELECT instance,type,NAME,VALUE
FROM information_schema.CLUSTER_HARDWARE
WHERE device_type='cpu'
group by instance,type,VALUE,NAME HAVING NAME = 'cpu-physical-cores'
OR NAME = 'cpu-logical-cores' ORDER BY INSTANCE`
rows, err := querySQL(db, sql)
if err != nil {
return table, err
}
m := make(map[string]*hardWare)
var s string
for _, row := range rows {
idx := strings.Index(row[0], ":")
s := row[0][:idx]
cpuCnt, err := strconv.Atoi(row[3])
if err != nil {
return table, err
}
_, ok := m[s]
if !ok {
m[s] = &hardWare{s, map[string]int{row[1]: 1}, make(map[string]int), 0, make(map[string]float64), ""}
}
m[s].Type[row[1]]++
if _, ok := m[s].cpu[row[2]]; !ok {
m[s].cpu[row[2]] = cpuCnt
}
}
sql = "SELECT instance,value FROM information_schema.CLUSTER_HARDWARE WHERE device_type='memory' and name = 'capacity' group by instance,value"
rows, err = querySQL(db, sql)
if err != nil {
return table, err
}
for _, row := range rows {
s = row[0][:strings.Index(row[0], ":")]
memCnt, err := strconv.ParseFloat(row[1], 64)
if err != nil {
return table, err
}
m[s].memory = memCnt
}
sql = "SELECT `INSTANCE`,`DEVICE_NAME`,`VALUE` from information_schema.CLUSTER_HARDWARE where `NAME` = 'total' AND `DEVICE_TYPE` = 'disk' AND `DEVICE_NAME` NOT LIKE '%loop%' group by instance,device_name,value"
rows, err = querySQL(db, sql)
if err != nil {
return table, err
}
for _, row := range rows {
s = row[0][:strings.Index(row[0], ":")]
diskCnt, err := strconv.ParseFloat(row[2], 64)
if err != nil {
return table, err
}
if _, ok := m[s].disk[row[1]]; !ok {
m[s].disk[row[1]] = diskCnt
}
}
sql = `SELECT instance,max(value)/60/60/24
FROM metrics_schema.node_uptime
where time >= '%[1]s' and time < '%[2]s'
GROUP BY instance`
sql = fmt.Sprintf(sql, startTime, endTime)
rows, err = querySQL(db, sql)
if err != nil {
return table, err
}
for _, row := range rows {
s = row[0][:strings.Index(row[0], ":")]
if _, ok := m[s]; ok {
m[s].uptime = row[1]
} else {
m[s] = &hardWare{s, make(map[string]int), nil, 0, make(map[string]float64), ""}
}
}
rows = rows[:0]
for _, v := range m {
row := make([]string, 6)
row[0] = v.instance
for k, va := range v.Type {
row[1] += fmt.Sprintf("%[1]s*%[2]s ", k, strconv.Itoa(va/2))
}
row[2] = strconv.Itoa(v.cpu["cpu-physical-cores"]) + "/" + strconv.Itoa(v.cpu["cpu-logical-cores"])
row[3] = fmt.Sprintf("%f", v.memory/(1024*1024*1024))
for k, va := range v.disk {
row[4] += fmt.Sprintf("%[1]s: %[2]f ", k, va/(1024*1024*1024))
}
row[5] = v.uptime
rows = append(rows, row)
}
for _, row := range rows {
resultRows = append(resultRows, NewTableRowDef(row, nil))
}
table.Rows = resultRows
return table, nil
}
func GetTiKVRocksDBTimeConsumeTable(startTime, endTime string, db *gorm.DB) (TableDef, error) {
defs := []struct {
name string
maxTbl string
tbl string
conditions []string
comment string
}{
{
name: "get duration",
maxTbl: "tikv_engine_max_get_duration",
tbl: "tikv_engine_avg_get_duration",
conditions: []string{"type='get_average'", "type='get_max'", "type='get_percentile99'", "type='get_percentile95'"},
comment: "The time consumed when rocksdb executing get operations",
},
{
name: "seek duration",
maxTbl: "tikv_engine_max_seek_duration",
tbl: "tikv_engine_avg_seek_duration",
conditions: []string{"type='seek_average'", "type='seek_max'", "type='seek_percentile99'", "type='seek_percentile95'"},
comment: "The time consumed when rocksdb executing seek operations",
},
{
name: "write duration",
maxTbl: "tikv_engine_write_duration",
tbl: "tikv_engine_write_duration",
conditions: []string{"type='write_average'", "type='write_max'", "type='write_percentile99'", "type='write_percentile95'"},
comment: "The time consumed when rocksdb executing write operations",
},
{
name: "WAL sync duration",
maxTbl: "tikv_wal_sync_max_duration",
tbl: "tikv_wal_sync_duration",
conditions: []string{"type='wal_file_sync_average'", "type='wal_file_sync_max'", "type='wal_file_sync_percentile99'", "type='wal_file_sync_percentile95'"},
comment: "The time consumed when rocksdb executing WAL sync operations",
},
{
name: "compaction duration",
maxTbl: "tikv_compaction_max_duration",
tbl: "tikv_compaction_duration",
conditions: []string{"type='compaction_time_average'", "type='compaction_time_max'", "type='compaction_time_percentile99'", "type='compaction_time_percentile95'"},
comment: "The time consumed when rocksdb executing compaction operations",
},
{
name: "SST read duration",
maxTbl: "tikv_sst_read_max_duration",
tbl: "tikv_sst_read_duration",
conditions: []string{"type='sst_read_micros_average'", "type='sst_read_micros_max'", "type='sst_read_micros_percentile99'", "type='sst_read_micros_percentile95'"},
comment: "The time consumed when rocksdb reading SST files",
},
{
name: "write stall duration",
maxTbl: "tikv_write_stall_max_duration",
tbl: "tikv_write_stall_avg_duration",
conditions: []string{"type='write_stall_average'", "type='write_stall_max'", "type='write_stall_percentile99'", "type='write_stall_percentile95'"},
comment: "The time which is caused by write stall",
},
}
table := TableDef{
Category: []string{CategoryTiKV},
Title: "rocksdb_time_consume",
Comment: "",
joinColumns: []int{0, 1},
compareColumns: []int{2, 3, 4, 5},
Column: []string{"METRIC_NAME", "LABEL", "AVG", "MAX", "P99", "P95"},
}
timeCondition := fmt.Sprintf("where time >= '%s' and time < '%s' ", startTime, endTime)
specialHandle := func(row []string) []string {
if len(row) < 6 {
return row
}
for i := 2; i < 6; i++ {
row[i] = convertFloatToDuration(row[i], float64(1)/float64(10e5))
}
return row
}
resultRows := make([]TableRowDef, 0, len(defs))
for _, def := range defs {
// get sum rows
sql := fmt.Sprintf("select '%s', '', t0.*, t1.*,t2.*,t3.* from ", def.name)
for i := range def.conditions {
condition := timeCondition
if len(def.conditions[i]) > 0 {
condition = condition + " and " + def.conditions[i]
}
// avg value
if i == 0 {
sql = sql + fmt.Sprintf("(select avg(value) from metrics_schema.%s %s) as t%v ", def.tbl, condition, i)
} else {
sql = sql + fmt.Sprintf("join (select max(value) from metrics_schema.%s %s) as t%v ", def.tbl, condition, i)
}
}
rows, err := querySQL(db, sql)
if err != nil {
return table, err
}
if len(rows) == 0 {
continue
}
sql = fmt.Sprintf("select '%s', t0.instance, t0.value, t1.value,t2.value,t3.value from ", def.name)
for i := range def.conditions {
condition := timeCondition
if len(def.conditions[i]) > 0 {
condition = condition + " and " + def.conditions[i]
}
// avg value
if i == 0 {
sql = sql + fmt.Sprintf("(select instance, avg(value) as value from metrics_schema.%s %s group by instance) as t%v ", def.tbl, condition, i)
} else {
sql = sql + fmt.Sprintf("join (select instance, max(value) as value from metrics_schema.%s %s group by instance) as t%v ", def.tbl, condition, i)
}
}
sql += " on t0.instance = t1.instance and t1.instance = t2.instance and t2.instance = t3.instance order by t0.value desc"
subRows, err := querySQL(db, sql)
if err != nil {
return table, err
}
rows[0] = specialHandle(rows[0])
for i := range subRows {
subRows[i] = specialHandle(subRows[i])
}
resultRows = append(resultRows, TableRowDef{
Values: rows[0],
SubValues: subRows,
Comment: def.comment,
})
}
table.Rows = resultRows
return table, nil
}
func GetTiDBTopNSlowQuery(startTime, endTime string, db *gorm.DB) (TableDef, error) {
columns := []string{"query_time", "parse_time", "compile_time", "prewrite_time", "commit_time", "process_time", "wait_time", "backoff_time", "cop_proc_max", "cop_wait_max", "query"}
sql := fmt.Sprintf("select %s from information_schema.cluster_slow_query where time >= '%s' and time < '%s' order by query_time desc limit 10;",
strings.Join(columns, ","), startTime, endTime)
table := TableDef{
Category: []string{CategoryTiDB},
Title: "top_10_slow_query",
Comment: "",
Column: columns,
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = useSubRowForLongColumnValue(rows, len(table.Column)-1)
return table, nil
}
func GetTiDBTopNSlowQueryGroupByDigest(startTime, endTime string, db *gorm.DB) (TableDef, error) {
columns := []string{"*", "query_time", "parse_time", "compile_time", "prewrite_time", "commit_time", "process_time", "wait_time", "backoff_time", "cop_proc_max", "cop_wait_max", "query"}
for i := range columns {
switch columns[i] {
case "*":
columns[i] = "count(*)"
case "query":
columns[i] = "min(query)"
default:
columns[i] = "sum(" + columns[i] + ")"
}
}
sql := fmt.Sprintf("select /*+ AGG_TO_COP(), HASH_AGG() */ %s from information_schema.cluster_slow_query where time >= '%s' and time < '%s' group by digest order by sum(query_time) desc limit 10;",
strings.Join(columns, ","), startTime, endTime)
table := TableDef{
Category: []string{CategoryTiDB},
Title: "top_10_slow_query_group_by_digest",
Comment: "",
Column: columns,
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = useSubRowForLongColumnValue(rows, len(table.Column)-1)
return table, nil
}
func GetTiDBSlowQueryWithDiffPlan(startTime, endTime string, db *gorm.DB) (TableDef, error) {
sql := fmt.Sprintf("select /*+ AGG_TO_COP(), HASH_AGG() */ digest, min(query) from information_schema.cluster_slow_query where time >= '%s' and time < '%s' group by digest having max(plan_digest) != min(plan_digest);",
startTime, endTime)
table := TableDef{
Category: []string{CategoryTiDB},
Title: "slow_query_with_diff_plan",
Comment: "",
Column: []string{"digest", "query"},
}
rows, err := getSQLRows(db, sql)
if err != nil {
return table, err
}
table.Rows = useSubRowForLongColumnValue(rows, 1)
return table, nil
}
================================================
FILE: pkg/apiserver/diagnose/report_test.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package diagnose
import (
"testing"
"github.com/pingcap/check"
)
func TestT(t *testing.T) {
check.CustomVerboseFlag = true
check.TestingT(t)
}
var _ = check.Suite(&testReportSuite{})
type testReportSuite struct{}
func (t *testReportSuite) TestCompareTable(c *check.C) {
table1 := TableDef{
Category: []string{"header"},
Title: "test",
joinColumns: []int{1},
compareColumns: []int{2},
Column: []string{"c1", "c2", "c3"},
Rows: nil,
}
cases := []struct {
rows1 []TableRowDef
rows2 []TableRowDef
out []TableRowDef
}{
{
rows1: nil,
rows2: nil,
out: []TableRowDef{},
},
{
rows1: []TableRowDef{
{Values: []string{"0", "0", "0"}},
},
rows2: nil,
out: []TableRowDef{
{Values: []string{"0", "0", "0", "", "", "1"}},
},
},
{
rows1: []TableRowDef{
{Values: []string{"0", "0", "0"}},
},
rows2: []TableRowDef{
{Values: []string{"1", "1", "1"}},
},
out: []TableRowDef{
{Values: []string{"0", "0", "0", "", "", "1"}},
{Values: []string{"", "1", "", "1", "1", "1"}},
},
},
{
rows1: []TableRowDef{
{Values: []string{"0", "0", "0"}},
},
rows2: []TableRowDef{
{Values: []string{"1", "0", "0"}},
},
out: []TableRowDef{
{Values: []string{"0", "0", "0", "1", "0", "0"}},
},
},
{
rows1: []TableRowDef{
{Values: []string{"0", "0", "0"}},
},
rows2: []TableRowDef{
{Values: []string{"1", "0", "1"}},
},
out: []TableRowDef{
{Values: []string{"0", "0", "0", "1", "1", "1"}},
},
},
}
dr := &diffRows{}
for _, cas := range cases {
t1 := table1
t2 := table1
t1.Rows = cas.rows1
t2.Rows = cas.rows2
t, err := compareTable(&t1, &t2, dr)
c.Assert(err, check.IsNil)
c.Assert(len(t.Rows), check.Equals, len(cas.out))
for i, row := range t.Rows {
c.Assert(row.Values, check.DeepEquals, cas.out[i].Values)
c.Assert(len(row.SubValues), check.Equals, len(cas.out[i].SubValues))
for j, subRow := range cas.out[i].SubValues {
c.Assert(subRow, check.DeepEquals, row.SubValues[j])
}
}
}
}
func (t *testReportSuite) TestRoundFloatString(c *check.C) {
cases := []struct {
in string
out string
}{
{"0", "0"},
{"1", "1"},
{"0.8", "0.8"},
{"0.99", "0.99"},
{"1.12345", "1.12"},
{"1.1256", "1.13"},
{"12345678.1256", "12345678.13"},
{"0.1256", "0.13"},
{"0.00234", "0.002"},
{"0.00254", "0.003"},
{"0.000000056", "0.00000006"},
{"0.00000000000000054", "0.0000000000000005"},
{"0.00000000000000056", "0.0000000000000006"},
{"65.20832000000001", "65.21"},
}
for _, cas := range cases {
result := RoundFloatString(cas.in)
c.Assert(result, check.Equals, cas.out)
}
}
================================================
FILE: pkg/apiserver/info/info.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package info
import (
"context"
"net/http"
"sort"
"strings"
"github.com/Masterminds/semver"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
clientv3 "go.etcd.io/etcd/client/v3"
"go.uber.org/fx"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap/tidb-dashboard/pkg/config"
"github.com/pingcap/tidb-dashboard/pkg/dbstore"
"github.com/pingcap/tidb-dashboard/pkg/tidb"
"github.com/pingcap/tidb-dashboard/pkg/utils/topology"
"github.com/pingcap/tidb-dashboard/pkg/utils/version"
"github.com/pingcap/tidb-dashboard/util/featureflag"
"github.com/pingcap/tidb-dashboard/util/rest"
)
type ServiceParams struct {
fx.In
EtcdClient *clientv3.Client
Config *config.Config
LocalStore *dbstore.DB
TiDBClient *tidb.Client
FeatureFlags *featureflag.Registry
}
type Service struct {
params ServiceParams
lifecycleCtx context.Context
}
func NewService(lc fx.Lifecycle, p ServiceParams) *Service {
s := &Service{params: p}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
s.lifecycleCtx = ctx
return nil
},
})
return s
}
func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/info")
endpoint.GET("/info", s.infoHandler)
endpoint.Use(auth.MWAuthRequired())
endpoint.GET("/whoami", s.WhoamiHandler)
endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient))
endpoint.GET("/databases", s.databasesHandler)
endpoint.GET("/tables", s.tablesHandler)
}
type InfoResponse struct { // nolint
Version *version.Info `json:"version"`
EnableTelemetry bool `json:"enable_telemetry"`
EnableExperimental bool `json:"enable_experimental"`
SupportedFeatures []string `json:"supported_features"`
NgmState utils.NgmState `json:"ngm_state"`
}
// @ID infoGet
// @Summary Get information about this TiDB Dashboard
// @Success 200 {object} InfoResponse
// @Router /info/info [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) infoHandler(c *gin.Context) {
// Checking ngm deployments
// drop "-alpha-xxx" suffix
versionWithoutSuffix := strings.Split(s.params.Config.FeatureVersion, "-")[0]
v, err := semver.NewVersion(versionWithoutSuffix)
if err != nil {
rest.Error(c, err)
return
}
constraint, err := semver.NewConstraint(">= v5.4.0")
if err != nil {
rest.Error(c, err)
return
}
ngmState := utils.NgmStateNotSupported
if constraint.Check(v) {
ngmState = utils.NgmStateNotStarted
addr, err := topology.FetchNgMonitoringTopology(s.lifecycleCtx, s.params.EtcdClient)
if err == nil && addr != "" {
ngmState = utils.NgmStateStarted
}
}
resp := InfoResponse{
Version: version.GetInfo(),
EnableTelemetry: s.params.Config.EnableTelemetry,
EnableExperimental: s.params.Config.EnableExperimental,
SupportedFeatures: s.params.FeatureFlags.SupportedFeatures(),
NgmState: ngmState,
}
c.JSON(http.StatusOK, resp)
}
type WhoAmIResponse struct {
DisplayName string `json:"display_name"`
IsShareable bool `json:"is_shareable"`
IsWriteable bool `json:"is_writeable"`
}
// @ID infoWhoami
// @Summary Get information about current session
// @Success 200 {object} WhoAmIResponse
// @Router /info/whoami [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) WhoamiHandler(c *gin.Context) {
sessionUser := utils.GetSession(c)
resp := WhoAmIResponse{
DisplayName: sessionUser.DisplayName,
IsShareable: sessionUser.IsShareable,
IsWriteable: sessionUser.IsWriteable,
}
c.JSON(http.StatusOK, resp)
}
// @ID infoListDatabases
// @Summary List all databases
// @Success 200 {object} []string
// @Router /info/databases [get]
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) databasesHandler(c *gin.Context) {
type databaseSchemas struct {
Databases string `gorm:"column:Database"`
}
var result []databaseSchemas
db := utils.GetTiDBConnection(c)
err := db.Raw("SHOW DATABASES").Scan(&result).Error
if err != nil {
rest.Error(c, err)
return
}
strs := []string{}
for _, v := range result {
strs = append(strs, strings.ToLower(v.Databases))
}
sort.Strings(strs)
c.JSON(http.StatusOK, strs)
}
type tableSchema struct {
TableName string `gorm:"column:TABLE_NAME" json:"table_name"`
TableID string `gorm:"column:TIDB_TABLE_ID" json:"table_id"`
}
// @ID infoListTables
// @Summary List tables by database name
// @Success 200 {object} []tableSchema
// @Router /info/tables [get]
// @Param database_name query string false "Database name"
// @Security JwtAuth
// @Failure 401 {object} rest.ErrorResponse
func (s *Service) tablesHandler(c *gin.Context) {
var result []tableSchema
db := utils.GetTiDBConnection(c)
tx := db.Select([]string{"TABLE_NAME", "TIDB_TABLE_ID"}).Table("INFORMATION_SCHEMA.TABLES")
databaseName := c.Query("database_name")
if databaseName != "" {
tx = tx.Where("LOWER(TABLE_SCHEMA) = ?", strings.ToLower(databaseName))
}
err := tx.Order("TABLE_NAME").Scan(&result).Error
if err != nil {
rest.Error(c, err)
return
}
result = lo.Map(result, func(item tableSchema, _ int) tableSchema {
item.TableName = strings.ToLower(item.TableName)
return item
})
c.JSON(http.StatusOK, result)
}
================================================
FILE: pkg/apiserver/logsearch/models.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package logsearch
import (
"database/sql/driver"
"encoding/json"
"os"
"github.com/pingcap/kvproto/pkg/diagnosticspb"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/model"
"github.com/pingcap/tidb-dashboard/pkg/dbstore"
)
type TaskState int
const (
TaskStateRunning TaskState = 1
TaskStateFinished TaskState = 2
TaskStateError TaskState = 3
)
type TaskGroupState int
const (
TaskGroupStateRunning TaskGroupState = 1
TaskGroupStateFinished TaskGroupState = 2
)
type LogLevel int32
const (
LogLevelUnknown LogLevel = 0
LogLevelDebug LogLevel = 1
LogLevelInfo LogLevel = 2
LogLevelWarn LogLevel = 3
LogLevelTrace LogLevel = 4
LogLevelCritical LogLevel = 5
LogLevelError LogLevel = 6
)
var PBLogLevelSlice = []diagnosticspb.LogLevel{
diagnosticspb.LogLevel(LogLevelUnknown),
diagnosticspb.LogLevel(LogLevelDebug),
diagnosticspb.LogLevel(LogLevelInfo),
diagnosticspb.LogLevel(LogLevelWarn),
diagnosticspb.LogLevel(LogLevelTrace),
diagnosticspb.LogLevel(LogLevelCritical),
diagnosticspb.LogLevel(LogLevelError),
}
type SearchLogRequest struct {
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
MinLevel LogLevel `json:"min_level"`
// We use a string array to represent multiple CNF pattern sceniaor like:
// SELECT * FROM t WHERE c LIKE '%s%' and c REGEXP '.*a.*' because
// Golang and Rust don't support perl-like (?=re1)(?=re2)
Patterns []string `json:"patterns"`
}
func (r *SearchLogRequest) ConvertToPB(target diagnosticspb.SearchLogRequest_Target) *diagnosticspb.SearchLogRequest {
levels := PBLogLevelSlice[r.MinLevel:]
return &diagnosticspb.SearchLogRequest{
StartTime: r.StartTime,
EndTime: r.EndTime,
Levels: levels,
Patterns: r.Patterns,
Target: target,
}
}
func (r *SearchLogRequest) Scan(src interface{}) error {
return json.Unmarshal([]byte(src.(string)), r)
}
func (r *SearchLogRequest) Value() (driver.Value, error) {
val, err := json.Marshal(r)
return string(val), err
}
type TaskModel struct {
ID uint `json:"id" gorm:"primary_key"`
TaskGroupID uint `json:"task_group_id" gorm:"index"`
Target *model.RequestTargetNode `json:"target" gorm:"embedded;embedded_prefix:target_"`
State TaskState `json:"state" gorm:"index"`
LogStorePath *string `json:"log_store_path" gorm:"type:text"`
SlowLogStorePath *string `json:"slow_log_store_path" gorm:"type:text"`
Size int64 `json:"size" gorm:"index"`
Error *string `json:"error" gorm:"type:text"`
}
func (TaskModel) TableName() string {
return "log_search_tasks"
}
// Note: this function does not save model itself.
func (task *TaskModel) RemoveDataAndPreview(db *dbstore.DB) {
if task.LogStorePath != nil {
_ = os.RemoveAll(*task.LogStorePath)
task.LogStorePath = nil
}
db.Where("task_id = ?", task.ID).Delete(&PreviewModel{})
}
type TaskGroupModel struct {
ID uint `json:"id" gorm:"primary_key"`
SearchRequest *SearchLogRequest `json:"search_request" gorm:"type:text"`
State TaskGroupState `json:"state" gorm:"index"`
TargetStats model.RequestTargetStatistics `json:"target_stats" gorm:"embedded;embedded_prefix:target_stats_"`
LogStoreDir *string `json:"log_store_dir" gorm:"type:text"`
}
func (TaskGroupModel) TableName() string {
return "log_search_task_groups"
}
func (tg *TaskGroupModel) Delete(db *dbstore.DB) {
if tg.LogStoreDir != nil {
_ = os.RemoveAll(*tg.LogStoreDir)
}
db.Where("task_group_id = ?", tg.ID).Delete(&PreviewModel{})
db.Where("task_group_id = ?", tg.ID).Delete(&TaskModel{})
db.Where("id = ?", tg.ID).Delete(&TaskGroupModel{})
}
type PreviewModel struct {
ID uint `json:"id" grom:"primary_key"`
TaskID uint `json:"task_id" gorm:"index:task"`
TaskGroupID uint `json:"task_group_id" gorm:"index:task_group"`
Time int64 `json:"time" gorm:"index:task,task_group"`
Level diagnosticspb.LogLevel `json:"level" gorm:"type:integer" swaggertype:"integer"`
Message string `json:"message" gorm:"type:text"`
}
func (PreviewModel) TableName() string {
return "log_previews"
}
func autoMigrate(db *dbstore.DB) error {
return db.AutoMigrate(&TaskModel{}, &TaskGroupModel{}, &PreviewModel{})
}
func cleanupAllTasks(db *dbstore.DB) {
var taskGroups []*TaskGroupModel
db.Find(&taskGroups)
for _, tg := range taskGroups {
tg.Delete(db)
}
}
================================================
FILE: pkg/apiserver/logsearch/pack.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package logsearch
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/pingcap/log"
"go.uber.org/zap"
"github.com/pingcap/tidb-dashboard/util/rest"
"github.com/pingcap/tidb-dashboard/util/ziputil"
)
func serveTaskForDownload(task *TaskModel, c *gin.Context) {
logPath := task.LogStorePath
if logPath == nil {
logPath = task.SlowLogStorePath
}
if logPath == nil {
rest.Error(c, rest.ErrBadRequest.New("Log is not ready"))
return
}
c.FileAttachment(*logPath, fmt.Sprintf("logs-%s.zip", task.Target.FileName()))
}
func serveMultipleTaskForDownload(tasks []*TaskModel, c *gin.Context) {
filePaths := make([]string, 0, len(tasks))
for _, task := range tasks {
logPath := task.LogStorePath
if logPath == nil {
logPath = task.SlowLogStorePath
}
if logPath == nil {
rest.Error(c, rest.ErrBadRequest.New("Some logs are not available"))
return
}
filePaths = append(filePaths, *logPath)
}
c.Writer.Header().Set("Content-type", "application/octet-stream")
c.Writer.Header().Set("Content-Disposition", "attachment; filename=\"logs.zip\"")
err := ziputil.WriteZipFromFiles(c.Writer, filePaths, false)
if err != nil {
log.Error("Stream zip pack failed", zap.Error(err))
}
}
================================================
FILE: pkg/apiserver/logsearch/scheduler.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package logsearch
import (
"sync"
"github.com/pingcap/log"
"go.uber.org/zap"
)
const (
TaskMaxPreviewLines = 500
TaskGroupMaxPreviewLines = 5000
)
type Scheduler struct {
runningTaskGroups sync.Map
service *Service
}
func NewScheduler(service *Service) *Scheduler {
return &Scheduler{
runningTaskGroups: sync.Map{},
service: service,
}
}
func (s *Scheduler) AsyncStart(taskGroupModel *TaskGroupModel, tasksModel []*TaskModel) bool {
log.Debug("Scheduler start task group", zap.Uint("task_group_id", taskGroupModel.ID))
previewsLinesPerTask := min(TaskGroupMaxPreviewLines/len(tasksModel), TaskMaxPreviewLines)
taskGroup := &TaskGroup{
service: s.service,
model: taskGroupModel,
tasks: nil, // Tasks are created only after successfully adding to the sync map.
tasksMu: sync.Mutex{},
maxPreviewLinesPerTask: previewsLinesPerTask,
}
_, alreadyRunning := s.runningTaskGroups.LoadOrStore(taskGroup.model.ID, taskGroup)
if alreadyRunning {
log.Warn("Scheduler start task group failed, task group is already running", zap.Uint("task_group_id", taskGroupModel.ID))
return false
}
taskGroup.InitTasks(s.service.lifecycleCtx, tasksModel)
go func() {
taskGroup.SyncRun()
s.runningTaskGroups.Delete(taskGroup.model.ID)
log.Debug("Scheduler task group finished", zap.Uint("task_group_id", taskGroupModel.ID))
}()
return true
}
func (s *Scheduler) AsyncAbort(taskGroupID uint) bool {
log.Debug("Scheduler abort task group", zap.Uint("task_group_id", taskGroupID))
v, ok := s.runningTaskGroups.Load(taskGroupID)
if !ok {
log.Warn("Scheduler abort task group failed, task group is not running", zap.Uint("task_group_id", taskGroupID))
return false
}
taskGroup := v.(*TaskGroup)
taskGroup.AbortAll()
return true
}
================================================
FILE: pkg/apiserver/logsearch/service.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package logsearch
import (
"context"
"net/http"
"os"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/pingcap/log"
"go.uber.org/fx"
"go.uber.org/zap"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/model"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap/tidb-dashboard/pkg/config"
"github.com/pingcap/tidb-dashboard/pkg/dbstore"
"github.com/pingcap/tidb-dashboard/util/rest"
)
type Service struct {
// FIXME: Use fx.In
lifecycleCtx context.Context
config *config.Config
logStoreDirectory string
db *dbstore.DB
scheduler *Scheduler
}
func NewService(lc fx.Lifecycle, config *config.Config, db *dbstore.DB) *Service {
dir := config.TempDir
if dir == "" {
var err error
dir, err = os.MkdirTemp("", "dashboard-logs")
if err != nil {
log.Fatal("Failed to create directory for storing logs", zap.Error(err))
}
}
err := autoMigrate(db)
if err != nil {
log.Fatal("Failed to initialize database", zap.Error(err))
}
cleanupAllTasks(db)
service := &Service{
config: config,
logStoreDirectory: dir,
db: db,
scheduler: nil, // will be filled after scheduler is created
}
scheduler := NewScheduler(service)
service.scheduler = scheduler
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
service.lifecycleCtx = ctx
return nil
},
})
return service
}
func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/logs")
{
endpoint.GET("/download", s.DownloadLogs)
endpoint.Use(auth.MWAuthRequired())
{
endpoint.GET("/download/acquire_token", s.GetDownloadToken)
endpoint.PUT("/taskgroup", s.CreateTaskGroup)
endpoint.GET("/taskgroups", s.GetAllTaskGroups)
endpoint.GET("/taskgroups/:id", s.GetTaskGroup)
endpoint.GET("/taskgroups/:id/preview", s.GetTaskGroupPreview)
endpoint.POST("/taskgroups/:id/retry", s.RetryTask)
endpoint.POST("/taskgroups/:id/cancel", s.CancelTask)
endpoint.DELETE("/taskgroups/:id", s.DeleteTaskGroup)
}
}
}
type CreateTaskGroupRequest struct {
Request SearchLogRequest `json:"request" binding:"required"`
Targets []model.RequestTargetNode `json:"targets" binding:"required"`
}
type TaskGroupResponse struct {
TaskGroup TaskGroupModel `json:"task_group"`
Tasks []*TaskModel `json:"tasks"`
}
// @Summary Create and run a new log search task group
// @Param request body CreateTaskGroupRequest true "Request body"
// @Security JwtAuth
// @Success 200 {object} TaskGroupResponse
// @Failure 400 {object} rest.ErrorResponse
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
// @Router /logs/taskgroup [put]
func (s *Service) CreateTaskGroup(c *gin.Context) {
var req CreateTaskGroupRequest
if err := c.ShouldBindJSON(&req); err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
if len(req.Targets) == 0 {
rest.Error(c, rest.ErrBadRequest.New("Expect at least 1 target"))
return
}
stats := model.NewRequestTargetStatisticsFromArray(&req.Targets)
taskGroup := TaskGroupModel{
SearchRequest: &req.Request,
State: TaskGroupStateRunning,
TargetStats: stats,
}
if err := s.db.Create(&taskGroup).Error; err != nil {
rest.Error(c, err)
return
}
tasks := make([]*TaskModel, 0, len(req.Targets))
for _, t := range req.Targets {
target := t
task := &TaskModel{
TaskGroupID: taskGroup.ID,
Target: &target,
State: TaskStateRunning,
}
// Ignore task creation errors
s.db.Create(task)
tasks = append(tasks, task)
}
if !s.scheduler.AsyncStart(&taskGroup, tasks) {
log.Error("Failed to start task group", zap.Uint("task_group_id", taskGroup.ID))
}
resp := TaskGroupResponse{
TaskGroup: taskGroup,
Tasks: tasks,
}
c.JSON(http.StatusOK, resp)
}
// @Summary List all log search task groups
// @Security JwtAuth
// @Success 200 {array} TaskGroupModel
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
// @Router /logs/taskgroups [get]
func (s *Service) GetAllTaskGroups(c *gin.Context) {
var taskGroups []*TaskGroupModel
err := s.db.Find(&taskGroups).Error
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, taskGroups)
}
// @Summary List tasks in a log search task group
// @Param id path string true "Task Group ID"
// @Security JwtAuth
// @Success 200 {object} TaskGroupResponse
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
// @Router /logs/taskgroups/{id} [get]
func (s *Service) GetTaskGroup(c *gin.Context) {
taskGroupID := c.Param("id")
var taskGroup TaskGroupModel
var tasks []*TaskModel
err := s.db.First(&taskGroup, "id = ?", taskGroupID).Error
if err != nil {
rest.Error(c, err)
return
}
err = s.db.Where("task_group_id = ?", taskGroupID).Find(&tasks).Error
if err != nil {
rest.Error(c, err)
return
}
resp := TaskGroupResponse{
TaskGroup: taskGroup,
Tasks: tasks,
}
c.JSON(http.StatusOK, resp)
}
// @Summary Preview a log search task group
// @Param id path string true "task group id"
// @Security JwtAuth
// @Success 200 {array} PreviewModel
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
// @Router /logs/taskgroups/{id}/preview [get]
func (s *Service) GetTaskGroupPreview(c *gin.Context) {
taskGroupID := c.Param("id")
var lines []PreviewModel
err := s.db.
Where("task_group_id = ?", taskGroupID).
Order("time").
Limit(TaskMaxPreviewLines).
Find(&lines).Error
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, lines)
}
// @Summary Retry failed tasks in a log search task group
// @Param id path string true "task group id"
// @Security JwtAuth
// @Success 200 {object} rest.EmptyResponse
// @Failure 400 {object} rest.ErrorResponse
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
// @Router /logs/taskgroups/{id}/retry [post]
func (s *Service) RetryTask(c *gin.Context) {
taskGroupID, err := strconv.Atoi(c.Param("id"))
if err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
// Currently we can only retry finished task group.
taskGroup := TaskGroupModel{}
if err := s.db.Where("id = ? AND state = ?", taskGroupID, TaskGroupStateFinished).First(&taskGroup).Error; err != nil {
rest.Error(c, err)
return
}
tasks := make([]*TaskModel, 0)
if err := s.db.Where("task_group_id = ? AND state = ?", taskGroupID, TaskStateError).Find(&tasks).Error; err != nil {
rest.Error(c, err)
return
}
if len(tasks) == 0 {
// No tasks to retry
c.JSON(http.StatusOK, rest.EmptyResponse{})
return
}
// Reset task status
taskGroup.State = TaskGroupStateRunning
s.db.Save(&taskGroup)
for _, task := range tasks {
task.Error = nil
task.State = TaskStateRunning
s.db.Save(task)
}
if !s.scheduler.AsyncStart(&taskGroup, tasks) {
log.Error("Failed to retry task group", zap.Uint("task_group_id", taskGroup.ID))
}
c.JSON(http.StatusOK, rest.EmptyResponse{})
}
// @Summary Cancel running tasks in a log search task group
// @Param id path string true "task group id"
// @Security JwtAuth
// @Success 200 {object} rest.EmptyResponse
// @Failure 401 {object} rest.ErrorResponse
// @Failure 400 {object} rest.ErrorResponse
// @Router /logs/taskgroups/{id}/cancel [post]
func (s *Service) CancelTask(c *gin.Context) {
taskGroupID, err := strconv.Atoi(c.Param("id"))
if err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
taskGroup := TaskGroupModel{}
err = s.db.First(&taskGroup, taskGroupID).Error
if err != nil {
rest.Error(c, err)
return
}
if taskGroup.State != TaskGroupStateRunning {
rest.Error(c, rest.ErrBadRequest.New("Task is not running"))
return
}
s.scheduler.AsyncAbort(uint(taskGroupID))
c.JSON(http.StatusOK, rest.EmptyResponse{})
}
// @Summary Delete a log search task group
// @Param id path string true "task group id"
// @Security JwtAuth
// @Success 200 {object} rest.EmptyResponse
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
// @Router /logs/taskgroups/{id} [delete]
func (s *Service) DeleteTaskGroup(c *gin.Context) {
taskGroupID := c.Param("id")
taskGroup := TaskGroupModel{}
err := s.db.Where("id = ? AND state != ?", taskGroupID, TaskGroupStateRunning).First(&taskGroup).Error
if err != nil {
rest.Error(c, err)
return
}
taskGroup.Delete(s.db)
c.JSON(http.StatusOK, rest.EmptyResponse{})
}
// @Summary Generate a download token for downloading logs
// @Produce plain
// @Param id query []string false "task id" collectionFormat(csv)
// @Security JwtAuth
// @Success 200 {string} string "xxx"
// @Failure 400 {object} rest.ErrorResponse
// @Failure 401 {object} rest.ErrorResponse
// @Router /logs/download/acquire_token [get]
func (s *Service) GetDownloadToken(c *gin.Context) {
ids := c.QueryArray("id")
str := strings.Join(ids, ",")
token, err := utils.NewJWTString("logs/download", str)
if err != nil {
rest.Error(c, err)
return
}
c.String(http.StatusOK, token)
}
// @Summary Download logs
// @Produce application/x-tar,application/zip
// @Param token query string true "download token"
// @Failure 400 {object} rest.ErrorResponse
// @Failure 401 {object} rest.ErrorResponse
// @Failure 500 {object} rest.ErrorResponse
// @Router /logs/download [get]
func (s *Service) DownloadLogs(c *gin.Context) {
token := c.Query("token")
str, err := utils.ParseJWTString("logs/download", token)
if err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
ids := strings.Split(str, ",")
tasks := make([]*TaskModel, 0, len(ids))
for _, id := range ids {
var task TaskModel
if s.db.
Where("id = ? AND state = ?", id, TaskStateFinished).
First(&task).
Error == nil {
tasks = append(tasks, &task)
// Ignore errors silently
}
}
switch len(tasks) {
case 0:
rest.Error(c, rest.ErrBadRequest.New("Expect at least 1 target"))
case 1:
serveTaskForDownload(tasks[0], c)
default:
serveMultipleTaskForDownload(tasks, c)
}
}
================================================
FILE: pkg/apiserver/logsearch/task.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package logsearch
import (
"archive/zip"
"bufio"
"context"
"fmt"
"io"
"math"
"net"
"os"
"path"
"path/filepath"
"strconv"
"sync"
"time"
"unsafe"
"github.com/pingcap/kvproto/pkg/diagnosticspb"
"github.com/pingcap/log"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/model"
)
// MaxRecvMsgSize set max gRPC receive message size received from server. If any message size is larger than
// current value, an error will be reported from gRPC.
var MaxRecvMsgSize = math.MaxInt64 - 1
type TaskGroup struct {
service *Service
model *TaskGroupModel
tasks []*Task
tasksMu sync.Mutex
maxPreviewLinesPerTask int
}
func (tg *TaskGroup) InitTasks(ctx context.Context, taskModels []*TaskModel) {
// Tasks are assigned after inserting into scheduler, thus it has a chance to run parallel with Abort.
tg.tasksMu.Lock()
defer tg.tasksMu.Unlock()
if tg.tasks != nil {
panic("LogSearchTaskGroup's task is already initialized")
}
tg.tasks = make([]*Task, 0, len(taskModels))
for _, taskModel := range taskModels {
ctx, cancel := context.WithCancel(ctx)
tg.tasks = append(tg.tasks, &Task{
taskGroup: tg,
model: taskModel,
ctx: ctx,
cancel: cancel,
})
}
}
func (tg *TaskGroup) SyncRun() {
log.Debug("LogSearchTaskGroup start", zap.Uint("task_group_id", tg.model.ID))
// Create log directory
dir := path.Join(tg.service.logStoreDirectory, strconv.Itoa(int(tg.model.ID)))
err := os.MkdirAll(dir, 0o777) // #nosec
if err == nil {
tg.model.LogStoreDir = &dir
tg.service.db.Save(tg.model)
}
wg := sync.WaitGroup{}
for _, task := range tg.tasks {
wg.Add(1)
go func(task *Task) {
task.SyncRun()
wg.Done()
}(task)
}
wg.Wait()
log.Debug("LogSearchTaskGroup finished", zap.Uint("task_group_id", tg.model.ID))
tg.model.State = TaskGroupStateFinished
tg.service.db.Save(tg.model)
}
// This function is multi-thread safe.
func (tg *TaskGroup) AbortAll() {
log.Debug("LogSearchTaskGroup abort", zap.Uint("task_group_id", tg.model.ID))
tg.tasksMu.Lock()
defer tg.tasksMu.Unlock()
for _, task := range tg.tasks {
task.Abort()
}
}
type Task struct {
taskGroup *TaskGroup
model *TaskModel
ctx context.Context
cancel context.CancelFunc
}
func (t *Task) String() string {
return fmt.Sprintf("LogSearchTask { id = %d, target = %s, task_group_id = %d }", t.model.ID, t.model.Target, t.taskGroup.model.ID)
}
// This function is multi-thread safe.
func (t *Task) Abort() {
log.Debug("LogSearchTask abort", zap.Any("task", t))
if t.cancel != nil {
t.cancel()
}
}
func (t *Task) setError(err error) {
errStr := err.Error()
t.model.Error = &errStr
}
func (t *Task) accumulateLogSize(path *string) {
if path != nil {
stat, err := os.Stat(*path)
if err != nil {
log.Warn("Can NOT fetch log file size for LogSearchTask",
zap.String("dir", *path),
zap.Any("task", t),
zap.String("err", err.Error()),
)
} else {
t.model.Size += stat.Size()
}
}
}
func (t *Task) SyncRun() {
defer func() {
if t.model.Error != nil {
log.Warn("LogSearchTask stopped with error",
zap.Any("task", t),
zap.String("err", *t.model.Error),
)
t.model.RemoveDataAndPreview(t.taskGroup.service.db)
t.model.State = TaskStateError
t.taskGroup.service.db.Save(t.model)
return
}
t.model.State = TaskStateFinished
t.accumulateLogSize(t.model.LogStorePath)
t.accumulateLogSize(t.model.SlowLogStorePath)
log.Debug("LogSearchTask finished", zap.Any("task", t))
t.taskGroup.service.db.Save(t.model)
}()
log.Debug("LogSearchTask start", zap.Any("task", t))
if t.taskGroup.model.LogStoreDir == nil {
t.setError(fmt.Errorf("failed to create temporary directory"))
return
}
secureOpt := grpc.WithTransportCredentials(insecure.NewCredentials())
if t.taskGroup.service.config.ClusterTLSConfig != nil {
creds := credentials.NewTLS(t.taskGroup.service.config.ClusterTLSConfig)
secureOpt = grpc.WithTransportCredentials(creds)
}
conn, err := grpc.Dial(net.JoinHostPort(t.model.Target.IP, strconv.Itoa(t.model.Target.Port)), //nolint:staticcheck // Dial is deprecated, but we use it here temporarily
secureOpt,
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(MaxRecvMsgSize)),
)
if err != nil {
t.setError(err)
return
}
defer conn.Close()
cli := diagnosticspb.NewDiagnosticsClient(conn)
t.searchLog(cli, diagnosticspb.SearchLogRequest_Normal)
// Only TiKV support searching slow log now
if t.model.Target.Kind == model.NodeKindTiKV {
t.searchLog(cli, diagnosticspb.SearchLogRequest_Slow)
}
}
func (t *Task) searchLog(client diagnosticspb.DiagnosticsClient, targetType diagnosticspb.SearchLogRequest_Target) {
if t.model.Error != nil {
return
}
req := t.taskGroup.model.SearchRequest.ConvertToPB(targetType)
patterns := make([]string, len(req.Patterns))
for i, p := range req.Patterns {
patterns[i] = "(?i)" + p
}
req.Patterns = patterns
stream, err := client.SearchLog(t.ctx, req)
if err != nil {
t.setError(err)
return
}
// Create zip file for the log in the log directory
fileName := t.model.Target.FileName()
if targetType == diagnosticspb.SearchLogRequest_Slow {
fileName = fileName + "-slow"
}
savedPath := path.Join(*t.taskGroup.model.LogStoreDir, fileName+".zip")
f, err := os.Create(filepath.Clean(savedPath))
if err != nil {
t.setError(err)
return
}
defer f.Close() // #nosec
// TODO: Could we use a memory buffer for this and flush the writer regularly to avoid OOM.
// This might perform an faster processing. This could also avoid creating an empty .zip
// firstly even if the searching result is empty.
zw := zip.NewWriter(f)
defer zw.Close()
defer zw.Flush()
writer, err := zw.Create(fileName + ".log")
if err != nil {
t.setError(err)
return
}
bufWriter := bufio.NewWriterSize(writer, 16*1024*1024) // 16M buffer size
defer bufWriter.Flush()
t.model.State = TaskStateRunning
previewLogLinesCount := 0
for {
res, err := stream.Recv()
if err != nil {
if err != io.EOF {
t.setError(err)
}
if previewLogLinesCount != 0 {
if targetType == diagnosticspb.SearchLogRequest_Normal {
t.model.LogStorePath = &savedPath
} else {
t.model.SlowLogStorePath = &savedPath
}
}
return
}
for _, msg := range res.Messages {
line := logMessageToString(msg)
_, err := bufWriter.Write(*(*[]byte)(unsafe.Pointer(&line))) // #nosec
if err != nil {
t.setError(err)
return
}
if previewLogLinesCount < t.taskGroup.maxPreviewLinesPerTask {
t.taskGroup.service.db.Create(&PreviewModel{
TaskID: t.model.ID,
TaskGroupID: t.taskGroup.model.ID,
Time: msg.Time,
Level: msg.Level,
Message: msg.Message,
})
previewLogLinesCount++
}
}
}
}
func logMessageToString(msg *diagnosticspb.LogMessage) string {
timeStr := time.Unix(0, msg.Time*int64(time.Millisecond)).Format("2006/01/02 15:04:05.000 -07:00")
return fmt.Sprintf("[%s] [%s] %s\n", timeStr, msg.Level.String(), msg.Message)
}
================================================
FILE: pkg/apiserver/metrics/prom_resolve.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package metrics
import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"time"
"github.com/pingcap/tidb-dashboard/pkg/utils/topology"
)
const (
promCacheTTL = time.Second * 5
)
type promAddressCacheEntity struct {
address string
cacheAt time.Time
}
type pdServerConfig struct {
MetricStorage string `json:"metric-storage"`
}
type pdConfig struct {
PdServer pdServerConfig `json:"pd-server"`
}
// Check and normalize a Prometheus address supplied by user.
func normalizeCustomizedPromAddress(addr string) (string, error) {
if !strings.HasPrefix(addr, "http://") && !strings.HasPrefix(addr, "https://") {
addr = "http://" + addr
}
u, err := url.Parse(addr)
if err != nil {
return "", fmt.Errorf("invalid Prometheus address format: %v", err)
}
if len(u.Host) == 0 || len(u.Scheme) == 0 {
return "", fmt.Errorf("invalid Prometheus address format")
}
// Normalize the address, remove unnecessary parts.
addr = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, strings.TrimSuffix(u.Path, "/"))
return addr, nil
}
// Resolve the customized Prometheus address in PD config. If it is not configured, empty address will be returned.
// The returned address must be valid. If an invalid Prometheus address is configured, errors will be returned.
func (s *Service) resolveCustomizedPromAddress(acceptInvalidAddr bool) (string, error) {
// Lookup "metric-storage" cluster config in PD.
data, err := s.params.PDClient.SendGetRequest("/config")
if err != nil {
return "", err
}
var config pdConfig
if err := json.Unmarshal(data, &config); err != nil {
return "", err
}
addr := config.PdServer.MetricStorage
if len(addr) > 0 {
if acceptInvalidAddr {
return addr, nil
}
// Verify whether address is valid. If not valid, throw error.
addr, err = normalizeCustomizedPromAddress(addr)
if err != nil {
return "", err
}
return addr, nil
}
return "", nil
}
// Resolve the Prometheus address recorded by deployment tools in the `/topology` etcd namespace.
// If the address is not recorded (for example, when Prometheus is not deployed), empty address will be returned.
func (s *Service) resolveDeployedPromAddress() (string, error) {
pi, err := topology.FetchPrometheusTopology(s.lifecycleCtx, s.params.EtcdClient)
if err != nil {
return "", err
}
if pi == nil {
return "", nil
}
return fmt.Sprintf("http://%s", net.JoinHostPort(pi.IP, strconv.Itoa(int(pi.Port)))), nil
}
// Resolve the final Prometheus address. When user has customized an address, this address is returned. Otherwise,
// address recorded by deployment tools will be returned.
// If neither custom address nor deployed address is available, empty address will be returned.
func (s *Service) resolveFinalPromAddress() (string, error) {
addr, err := s.resolveCustomizedPromAddress(false)
if err != nil {
return "", err
}
if addr != "" {
return addr, nil
}
addr, err = s.resolveDeployedPromAddress()
if err != nil {
return "", err
}
if addr != "" {
return addr, nil
}
return "", nil
}
// Get the final Prometheus address from cache. If cache item is not valid, the address will be resolved from PD
// or etcd and then the cache will be updated.
func (s *Service) getPromAddressFromCache() (string, error) {
fn := func() (string, error) {
// Check whether cache is valid, and use the cache if possible.
if v := s.promAddressCache.Load(); v != nil {
entity := v.(*promAddressCacheEntity)
if entity.cacheAt.Add(promCacheTTL).After(time.Now()) {
return entity.address, nil
}
}
// Cache is not valid, read from PD and etcd.
addr, err := s.resolveFinalPromAddress()
if err != nil {
return "", err
}
s.promAddressCache.Store(&promAddressCacheEntity{
address: addr,
cacheAt: time.Now(),
})
return addr, nil
}
resolveResult, err, _ := s.promRequestGroup.Do("any_key", func() (interface{}, error) {
return fn()
})
if err != nil {
return "", err
}
return resolveResult.(string), nil
}
// Set the customized Prometheus address. Address can be empty or a valid address like `http://host:port`.
// If address is set to empty, address from deployment tools will be used later.
func (s *Service) setCustomPromAddress(addr string) (string, error) {
var err error
if len(addr) > 0 {
addr, err = normalizeCustomizedPromAddress(addr)
if err != nil {
return "", err
}
}
body := make(map[string]interface{})
body["metric-storage"] = addr
bodyJSON, err := json.Marshal(&body)
if err != nil {
return "", err
}
_, err = s.params.PDClient.SendPostRequest("/config", bytes.NewBuffer(bodyJSON))
if err != nil {
return "", err
}
// Invalidate cache immediately.
s.promAddressCache.Store(&promAddressCacheEntity{
address: addr,
cacheAt: time.Time{},
})
return addr, nil
}
================================================
FILE: pkg/apiserver/metrics/prom_resolve_test.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package metrics
import (
"testing"
"github.com/stretchr/testify/require"
)
// https://github.com/pingcap/tidb-dashboard/issues/1560
func Test_normalizeCustomizedPromAddress(t *testing.T) {
addr, err := normalizeCustomizedPromAddress("http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090")
require.NoError(t, err)
require.Equal(t, "http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090", addr)
addr, err = normalizeCustomizedPromAddress("http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090/")
require.NoError(t, err)
require.Equal(t, "http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090", addr)
addr, err = normalizeCustomizedPromAddress("http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090/_/tsdb/")
require.NoError(t, err)
require.Equal(t, "http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090/_/tsdb", addr)
}
================================================
FILE: pkg/apiserver/metrics/router.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package metrics
import (
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"github.com/gin-gonic/gin"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/util/rest"
)
type QueryRequest struct {
StartTimeSec int `json:"start_time_sec" form:"start_time_sec"`
EndTimeSec int `json:"end_time_sec" form:"end_time_sec"`
StepSec int `json:"step_sec" form:"step_sec"`
Query string `json:"query" form:"query"`
}
type QueryResponse struct {
Status string `json:"status"`
Data map[string]interface{} `json:"data"`
}
func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/metrics")
endpoint.Use(auth.MWAuthRequired())
endpoint.GET("/query", s.queryMetrics)
endpoint.GET("/prom_address", s.getPromAddressConfig)
endpoint.PUT("/prom_address", auth.MWRequireWritePriv(), s.putCustomPromAddress)
}
// @Summary Query metrics
// @Description Query metrics in the given range
// @Param q query QueryRequest true "Query"
// @Success 200 {object} QueryResponse
// @Failure 401 {object} rest.ErrorResponse
// @Security JwtAuth
// @Router /metrics/query [get]
func (s *Service) queryMetrics(c *gin.Context) {
var req QueryRequest
if err := c.ShouldBindQuery(&req); err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
addr, err := s.getPromAddressFromCache()
if err != nil {
rest.Error(c, ErrLoadPrometheusAddressFailed.Wrap(err, "Load prometheus address failed"))
return
}
if addr == "" {
rest.Error(c, ErrPrometheusNotFound.New("Prometheus is not deployed in the cluster"))
return
}
params := url.Values{}
params.Add("query", req.Query)
params.Add("start", strconv.Itoa(req.StartTimeSec))
params.Add("end", strconv.Itoa(req.EndTimeSec))
params.Add("step", strconv.Itoa(req.StepSec))
uri := fmt.Sprintf("%s/api/v1/query_range?%s", addr, params.Encode())
promReq, err := http.NewRequestWithContext(s.lifecycleCtx, http.MethodGet, uri, nil)
if err != nil {
rest.Error(c, ErrPrometheusQueryFailed.Wrap(err, "failed to build Prometheus request"))
return
}
promResp, err := s.params.HTTPClient.WithTimeout(defaultPromQueryTimeout).Do(promReq)
if err != nil {
rest.Error(c, ErrPrometheusQueryFailed.Wrap(err, "failed to send requests to Prometheus"))
return
}
defer promResp.Body.Close()
if promResp.StatusCode != http.StatusOK {
rest.Error(c, ErrPrometheusQueryFailed.New("failed to query Prometheus"))
return
}
body, err := io.ReadAll(promResp.Body)
if err != nil {
rest.Error(c, ErrPrometheusQueryFailed.Wrap(err, "failed to read Prometheus query result"))
return
}
c.Data(promResp.StatusCode, promResp.Header.Get("content-type"), body)
}
type GetPromAddressConfigResponse struct {
CustomizedAddr string `json:"customized_addr"`
DeployedAddr string `json:"deployed_addr"`
}
// @ID metricsGetPromAddress
// @Summary Get the Prometheus address cluster config
// @Success 200 {object} GetPromAddressConfigResponse
// @Failure 401 {object} rest.ErrorResponse
// @Security JwtAuth
// @Router /metrics/prom_address [get]
func (s *Service) getPromAddressConfig(c *gin.Context) {
cAddr, err := s.resolveCustomizedPromAddress(true)
if err != nil {
rest.Error(c, err)
return
}
dAddr, err := s.resolveDeployedPromAddress()
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, GetPromAddressConfigResponse{
CustomizedAddr: cAddr,
DeployedAddr: dAddr,
})
}
type PutCustomPromAddressRequest struct {
Addr string `json:"address"`
}
type PutCustomPromAddressResponse struct {
NormalizedAddr string `json:"normalized_address"`
}
// @ID metricsSetCustomPromAddress
// @Summary Set or clear the customized Prometheus address
// @Param request body PutCustomPromAddressRequest true "Request body"
// @Success 200 {object} PutCustomPromAddressResponse
// @Failure 401 {object} rest.ErrorResponse
// @Security JwtAuth
// @Router /metrics/prom_address [put]
func (s *Service) putCustomPromAddress(c *gin.Context) {
var req PutCustomPromAddressRequest
if err := c.ShouldBindJSON(&req); err != nil {
rest.Error(c, rest.ErrBadRequest.NewWithNoMessage())
return
}
if s.params.Config.DisableCustomPromAddr && req.Addr != "" {
rest.Error(c, rest.ErrForbidden.New("custom prometheus address has been disabled"))
return
}
addr, err := s.setCustomPromAddress(req.Addr)
if err != nil {
rest.Error(c, err)
return
}
c.JSON(http.StatusOK, PutCustomPromAddressResponse{
NormalizedAddr: addr,
})
}
================================================
FILE: pkg/apiserver/metrics/service.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package metrics
import (
"context"
"time"
"github.com/joomcode/errorx"
clientv3 "go.etcd.io/etcd/client/v3"
"go.uber.org/atomic"
"go.uber.org/fx"
"golang.org/x/sync/singleflight"
"github.com/pingcap/tidb-dashboard/pkg/config"
"github.com/pingcap/tidb-dashboard/pkg/httpc"
"github.com/pingcap/tidb-dashboard/pkg/pd"
)
var (
ErrNS = errorx.NewNamespace("error.api.metrics")
ErrLoadPrometheusAddressFailed = ErrNS.NewType("load_prom_address_failed")
ErrPrometheusNotFound = ErrNS.NewType("prom_not_found")
ErrPrometheusQueryFailed = ErrNS.NewType("prom_query_failed")
)
const (
defaultPromQueryTimeout = time.Second * 30
)
type ServiceParams struct {
fx.In
Config *config.Config
HTTPClient *httpc.Client
EtcdClient *clientv3.Client
PDClient *pd.Client
}
type Service struct {
params ServiceParams
lifecycleCtx context.Context
promRequestGroup singleflight.Group
promAddressCache atomic.Value
}
func NewService(lc fx.Lifecycle, p ServiceParams) *Service {
s := &Service{params: p}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
s.lifecycleCtx = ctx
return nil
},
})
return s
}
================================================
FILE: pkg/apiserver/model/common_models.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package model
import (
"fmt"
"strings"
)
type NodeKind string
const (
NodeKindTiDB NodeKind = "tidb"
NodeKindTiKV NodeKind = "tikv"
NodeKindPD NodeKind = "pd"
NodeKindTiFlash NodeKind = "tiflash"
NodeKindTiCDC NodeKind = "ticdc"
NodeKindTiProxy NodeKind = "tiproxy"
NodeKindTSO NodeKind = "tso"
NodeKindScheduling NodeKind = "scheduling"
)
type RequestTargetNode struct {
Kind NodeKind `json:"kind" gorm:"size:8" example:"tidb"`
DisplayName string `json:"display_name" gorm:"size:32" example:"127.0.0.1:4000"`
IP string `json:"ip" gorm:"size:32" example:"127.0.0.1"`
Port int `json:"port" example:"4000"`
}
func (n *RequestTargetNode) String() string {
return fmt.Sprintf("%s(%s)", n.Kind, n.DisplayName)
}
func (n *RequestTargetNode) FileName() string {
displayName := strings.NewReplacer(":", "_").Replace(n.DisplayName)
return fmt.Sprintf("%s_%s", n.Kind, displayName)
}
type RequestTargetStatistics struct {
NumTiKVNodes int `json:"num_tikv_nodes"`
NumTiDBNodes int `json:"num_tidb_nodes"`
NumPDNodes int `json:"num_pd_nodes"`
NumTiFlashNodes int `json:"num_tiflash_nodes"`
NumTiCDCNodes int `json:"num_ticdc_nodes"`
NumTiProxyNodes int `json:"num_tiproxy_nodes"`
NumTSONodes int `json:"num_tso_nodes"`
NumSchedulingNodes int `json:"num_scheduling_nodes"`
}
func NewRequestTargetStatisticsFromArray(arr *[]RequestTargetNode) RequestTargetStatistics {
stats := RequestTargetStatistics{}
for _, node := range *arr {
switch node.Kind {
case NodeKindTiDB:
stats.NumTiDBNodes++
case NodeKindTiKV:
stats.NumTiKVNodes++
case NodeKindPD:
stats.NumPDNodes++
case NodeKindTiFlash:
stats.NumTiFlashNodes++
case NodeKindTiCDC:
stats.NumTiCDCNodes++
case NodeKindTiProxy:
stats.NumTiProxyNodes++
case NodeKindTSO:
stats.NumTSONodes++
case NodeKindScheduling:
stats.NumSchedulingNodes++
}
}
return stats
}
================================================
FILE: pkg/apiserver/profiling/fetcher.go
================================================
// Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0.
package profiling
import (
_ "embed"
"fmt"
"io"
"net"
"os"
"os/exec"
"strconv"
"strings"
"time"
"go.uber.org/fx"
"github.com/pingcap/tidb-dashboard/pkg/config"
"github.com/pingcap/tidb-dashboard/pkg/pd"
"github.com/pingcap/tidb-dashboard/pkg/scheduling"
"github.com/pingcap/tidb-dashboard/pkg/ticdc"
"github.com/pingcap/tidb-dashboard/pkg/tidb"
"github.com/pingcap/tidb-dashboard/pkg/tiflash"
"github.com/pingcap/tidb-dashboard/pkg/tikv"
"github.com/pingcap/tidb-dashboard/pkg/tiproxy"
"github.com/pingcap/tidb-dashboard/pkg/tso"
)
const (
maxProfilingTimeout = time.Minute * 5
)
type fetchOptions struct {
ip string
port int
path string
}
type profileFetcher interface {
fetch(op *fetchOptions) ([]byte, error)
}
type fetchers struct {
tikv profileFetcher
tiflash profileFetcher
tidb profileFetcher
pd profileFetcher
ticdc profileFetcher
tiproxy profileFetcher
tso profileFetcher
scheduling profileFetcher
}
var newFetchers = fx.Provide(func(
tikvClient *tikv.Client,
tidbClient *tidb.Client,
pdClient *pd.Client,
tiflashClient *tiflash.Client,
ticdcClient *ticdc.Client,
tiproxyClient *tiproxy.Client,
tsoClient *tso.Client,
schedulingClient *scheduling.Client,
config *config.Config,
) *fetchers {
return &fetchers{
tikv: &tikvFetcher{
client: tikvClient,
},
tiflash: &tiflashFetcher{
client: tiflashClient,
},
tidb: &tidbFetcher{
client: tidbClient,
},
pd: &pdFetcher{
client: pdClient,
statusAPIHTTPScheme: config.GetClusterHTTPScheme(),
},
ticdc: &ticdcFecther{
client: ticdcClient,
},
tiproxy: &tiproxyFecther{
client: tiproxyClient,
},
tso: &tsoFetcher{
client: tsoClient,
},
scheduling: &schedulingFetcher{
client: schedulingClient,
},
}
})
type tikvFetcher struct {
client *tikv.Client
}
//go:embed jeprof.in
var jeprof string
func (f *tikvFetcher) fetch(op *fetchOptions) ([]byte, error) {
if strings.HasSuffix(op.path, "heap") {
scheme := f.client.GetHTTPScheme()
cmd := exec.Command("perl", "/dev/stdin", "--raw", scheme+"://"+op.ip+":"+strconv.Itoa(op.port)+op.path) //nolint:gosec
cmd.Stdin = strings.NewReader(jeprof)
if f.client.GetTLSInfo() != nil {
cmd.Env = append(os.Environ(), fmt.Sprintf(
"URL_FETCHER=curl -s --cert %s --key %s --cacert %s",
f.client.GetTLSInfo().CertFile,
f.client.GetTLSInfo().KeyFile,
f.client.GetTLSInfo().TrustedCAFile,
))
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
// use jeprof to fetch tikv heap profile
err = cmd.Start()
if err != nil {
return nil, err
}
data, err := io.ReadAll(stdout)
if err != nil {
return nil, err
}
errMsg, err := io.ReadAll(stderr)
if err != nil {
return nil, err
}
err = cmd.Wait()
if err != nil {
return nil, fmt.Errorf("failed to fetch tikv heap profile: %s", errMsg)
}
return data, nil
}
return f.client.WithTimeout(maxProfilingTimeout).AddRequestHeader("Content-Type", "application/protobuf").SendGetRequest(op.ip, op.port, op.path)
}
type tiflashFetcher struct {
client *tiflash.Client
}
func (f *tiflashFetcher) fetch(op *fetchOptions) ([]byte, error) {
if strings.HasSuffix(op.path, "heap") {
scheme := f.client.GetHTTPScheme()
cmd := exec.Command("perl", "/dev/stdin", "--raw", scheme+"://"+op.ip+":"+strconv.Itoa(op.port)+op.path) //nolint:gosec
cmd.Stdin = strings.NewReader(jeprof)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
// use jeprof to fetch tiflash heap profile
err = cmd.Start()
if err != nil {
return nil, err
}
data, err := io.ReadAll(stdout)
if err != nil {
return nil, err
}
errMsg, err := io.ReadAll(stderr)
if err != nil {
return nil, err
}
err = cmd.Wait()
if err != nil {
return nil, fmt.Errorf("failed to fetch tiflash heap profile: %s", errMsg)
}
return data, nil
}
return f.client.WithTimeout(maxProfilingTimeout).AddRequestHeader("Content-Type", "application/protobuf").SendGetRequest(op.ip, op.port, op.path)
}
type tidbFetcher struct {
client *tidb.Client
}
func (f *tidbFetcher) fetch(op *fetchOptions) ([]byte, error) {
return f.client.WithEnforcedStatusAPIAddress(op.ip, op.port).WithStatusAPITimeout(maxProfilingTimeout).SendGetRequest(op.path)
}
type pdFetcher struct {
client *pd.Client
statusAPIHTTPScheme string
}
func (f *pdFetcher) fetch(op *fetchOptions) ([]byte, error) {
baseURL := fmt.Sprintf("%s://%s", f.statusAPIHTTPScheme, net.JoinHostPort(op.ip, strconv.Itoa(op.port)))
return f.client.
WithTimeout(maxProfilingTimeout).
WithBaseURL(baseURL).
WithoutPrefix(). // pprof API does not have /pd/api/v1 prefix
SendGetRequest(op.path)
}
type ticdcFecther struct {
client *ticdc.Client
}
func (f *ticdcFecther) fetch(op *fetchOptions) ([]byte, error) {
return f.client.WithTimeout(maxProfilingTimeout).SendGetRequest(op.ip, op.port, op.path)
}
type tiproxyFecther struct {
client *tiproxy.Client
}
func (f *tiproxyFecther) fetch(op *fetchOptions) ([]byte, error) {
return f.client.WithTimeout(maxProfilingTimeout).SendGetRequest(op.ip, op.port, op.path)
}
type tsoFetcher struct {
client *tso.Client
}
func (f *tsoFetcher) fetch(op *fetchOptions) ([]byte, error) {
return f.client.WithTimeout(maxProfilingTimeout).SendGetRequest(op.ip, op.port, op.path)
}
type schedulingFetcher struct {
client *scheduling.Client
}
func (f *schedulingFetcher) fetch(op *fetchOptions) ([]byte, error) {
return f.client.WithTimeout(maxProfilingTimeout).SendGetRequest(op.ip, op.port, op.path)
}
================================================
FILE: pkg/apiserver/profiling/jeprof.in
================================================
#! /usr/bin/env perl
# Copyright (c) 1998-2007, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# ---
# Program for printing the profile generated by common/profiler.cc,
# or by the heap profiler (common/debugallocation.cc)
#
# The profile contains a sequence of entries of the form:
#
# This program parses the profile, and generates user-readable
# output.
#
# Examples:
#
# % tools/jeprof "program" "profile"
# Enters "interactive" mode
#
# % tools/jeprof --text "program" "profile"
# Generates one line per procedure
#
# % tools/jeprof --gv "program" "profile"
# Generates annotated call-graph and displays via "gv"
#
# % tools/jeprof --gv --focus=Mutex "program" "profile"
# Restrict to code paths that involve an entry that matches "Mutex"
#
# % tools/jeprof --gv --focus=Mutex --ignore=string "program" "profile"
# Restrict to code paths that involve an entry that matches "Mutex"
# and does not match "string"
#
# % tools/jeprof --list=IBF_CheckDocid "program" "profile"
# Generates disassembly listing of all routines with at least one
# sample that match the --list= pattern. The listing is
# annotated with the flat and cumulative sample counts at each line.
#
# % tools/jeprof --disasm=IBF_CheckDocid "program" "profile"
# Generates disassembly listing of all routines with at least one
# sample that match the --disasm= pattern. The listing is
# annotated with the flat and cumulative sample counts at each PC value.
#
# TODO: Use color to indicate files?
use strict;
use warnings;
use Getopt::Long;
use Cwd;
my $JEPROF_VERSION = "unknown";
my $PPROF_VERSION = "2.0";
# These are the object tools we use which can come from a
# user-specified location using --tools, from the JEPROF_TOOLS
# environment variable, or from the environment.
my %obj_tool_map = (
"objdump" => "objdump",
"nm" => "nm",
"addr2line" => "addr2line",
"c++filt" => "c++filt",
## ConfigureObjTools may add architecture-specific entries:
#"nm_pdb" => "nm-pdb", # for reading windows (PDB-format) executables
#"addr2line_pdb" => "addr2line-pdb", # ditto
#"otool" => "otool", # equivalent of objdump on OS X
);
# NOTE: these are lists, so you can put in commandline flags if you want.
my @DOT = ("dot"); # leave non-absolute, since it may be in /usr/local
my @GV = ("gv");
my @EVINCE = ("evince"); # could also be xpdf or perhaps acroread
my @KCACHEGRIND = ("kcachegrind");
my @PS2PDF = ("ps2pdf");
# These are used for dynamic profiles
my @URL_FETCHER = ("curl", "-s", "--fail");
# These are the web pages that servers need to support for dynamic profiles
my $HEAP_PAGE = "/pprof/heap";
my $PROFILE_PAGE = "/pprof/profile"; # must support cgi-param "?seconds=#"
my $PMUPROFILE_PAGE = "/pprof/pmuprofile(?:\\?.*)?"; # must support cgi-param
# ?seconds=#&event=x&period=n
my $GROWTH_PAGE = "/pprof/growth";
my $CONTENTION_PAGE = "/pprof/contention";
my $WALL_PAGE = "/pprof/wall(?:\\?.*)?"; # accepts options like namefilter
my $FILTEREDPROFILE_PAGE = "/pprof/filteredprofile(?:\\?.*)?";
my $CENSUSPROFILE_PAGE = "/pprof/censusprofile(?:\\?.*)?"; # must support cgi-param
# "?seconds=#",
# "?tags_regexp=#" and
# "?type=#".
my $SYMBOL_PAGE = "/pprof/symbol"; # must support symbol lookup via POST
my $PROGRAM_NAME_PAGE = "/pprof/cmdline";
# These are the web pages that can be named on the command line.
# All the alternatives must begin with /.
my $PROFILES = "($HEAP_PAGE|$PROFILE_PAGE|$PMUPROFILE_PAGE|" .
"$GROWTH_PAGE|$CONTENTION_PAGE|$WALL_PAGE|" .
"$FILTEREDPROFILE_PAGE|$CENSUSPROFILE_PAGE)";
# default binary name
my $UNKNOWN_BINARY = "(unknown)";
# There is a pervasive dependency on the length (in hex characters,
# i.e., nibbles) of an address, distinguishing between 32-bit and
# 64-bit profiles. To err on the safe size, default to 64-bit here:
my $address_length = 16;
my $dev_null = "/dev/null";
if (! -e $dev_null && $^O =~ /MSWin/) { # $^O is the OS perl was built for
$dev_null = "nul";
}
# A list of paths to search for shared object files
my @prefix_list = ();
# Special routine name that should not have any symbols.
# Used as separator to parse "addr2line -i" output.
my $sep_symbol = '_fini';
my $sep_address = undef;
##### Argument parsing #####
sub usage_string {
return < is a space separated list of profile names.
jeprof [options] is a list of profile files where each file contains
the necessary symbol mappings as well as profile data (likely generated
with --raw).
jeprof [options] is a remote form. Symbols are obtained from host:port$SYMBOL_PAGE
Each name can be:
/path/to/profile - a path to a profile file
host:port[/] - a location of a service to get profile from
The / can be $HEAP_PAGE, $PROFILE_PAGE, /pprof/pmuprofile,
$GROWTH_PAGE, $CONTENTION_PAGE, /pprof/wall,
$CENSUSPROFILE_PAGE, or /pprof/filteredprofile.
For instance:
jeprof http://myserver.com:80$HEAP_PAGE
If / is omitted, the service defaults to $PROFILE_PAGE (cpu profiling).
jeprof --symbols
Maps addresses to symbol names. In this mode, stdin should be a
list of library mappings, in the same format as is found in the heap-
and cpu-profile files (this loosely matches that of /proc/self/maps
on linux), followed by a list of hex addresses to map, one per line.
For more help with querying remote servers, including how to add the
necessary server-side support code, see this filename (or one like it):
/usr/doc/gperftools-$PPROF_VERSION/pprof_remote_servers.html
Options:
--cum Sort by cumulative data
--base= Subtract from before display
--interactive Run in interactive mode (interactive "help" gives help) [default]
--seconds= Length of time for dynamic profiles [default=30 secs]
--add_lib= Read additional symbols and line info from the given library
--lib_prefix= Comma separated list of library path prefixes
Reporting Granularity:
--addresses Report at address level
--lines Report at source line level
--functions Report at function level [default]
--files Report at source file level
Output type:
--text Generate text report
--callgrind Generate callgrind format to stdout
--gv Generate Postscript and display
--evince Generate PDF and display
--web Generate SVG and display
--list= Generate source listing of matching routines
--disasm= Generate disassembly of matching routines
--symbols Print demangled symbol names found at given addresses
--dot Generate DOT file to stdout
--ps Generate Postcript to stdout
--pdf Generate PDF to stdout
--svg Generate SVG to stdout
--gif Generate GIF to stdout
--raw Generate symbolized jeprof data (useful with remote fetch)
--collapsed Generate collapsed stacks for building flame graphs
(see http://www.brendangregg.com/flamegraphs.html)
Heap-Profile Options:
--inuse_space Display in-use (mega)bytes [default]
--inuse_objects Display in-use objects
--alloc_space Display allocated (mega)bytes
--alloc_objects Display allocated objects
--show_bytes Display space in bytes
--drop_negative Ignore negative differences
Contention-profile options:
--total_delay Display total delay at each region [default]
--contentions Display number of delays at each region
--mean_delay Display mean delay at each region
Call-graph Options:
--nodecount= Show at most so many nodes [default=80]
--nodefraction= Hide nodes below *total [default=.005]
--edgefraction= Hide edges below *total [default=.001]
--maxdegree= Max incoming/outgoing edges per node [default=8]
--focus= Focus on backtraces with nodes matching
--thread= Show profile for thread
--ignore= Ignore backtraces with nodes matching
--scale= Set GV scaling [default=0]
--heapcheck Make nodes with non-0 object counts
(i.e. direct leak generators) more visible
--retain= Retain only nodes that match
--exclude= Exclude all nodes that match
Miscellaneous:
--tools=[,...] \$PATH for object tool pathnames
--test Run unit tests
--help This message
--version Version information
--debug-syms-by-id (Linux only) Find debug symbol files by build ID as well as by name
Environment Variables:
JEPROF_TMPDIR Profiles directory. Defaults to \$HOME/jeprof
JEPROF_TOOLS Prefix for object tools pathnames
URL_FETCHER Command to fetch remote profiles
Examples:
jeprof /bin/ls ls.prof
Enters "interactive" mode
jeprof --text /bin/ls ls.prof
Outputs one line per procedure
jeprof --web /bin/ls ls.prof
Displays annotated call-graph in web browser
jeprof --gv /bin/ls ls.prof
Displays annotated call-graph via 'gv'
jeprof --gv --focus=Mutex /bin/ls ls.prof
Restricts to code paths including a .*Mutex.* entry
jeprof --gv --focus=Mutex --ignore=string /bin/ls ls.prof
Code paths including Mutex but not string
jeprof --list=getdir /bin/ls ls.prof
(Per-line) annotated source listing for getdir()
jeprof --disasm=getdir /bin/ls ls.prof
(Per-PC) annotated disassembly for getdir()
jeprof http://localhost:1234/
Enters "interactive" mode
jeprof --text localhost:1234
Outputs one line per procedure for localhost:1234
jeprof --raw localhost:1234 > ./local.raw
jeprof --text ./local.raw
Fetches a remote profile for later analysis and then
analyzes it in text mode.
EOF
}
sub version_string {
return < \$main::opt_help,
"version!" => \$main::opt_version,
"cum!" => \$main::opt_cum,
"base=s" => \$main::opt_base,
"seconds=i" => \$main::opt_seconds,
"add_lib=s" => \$main::opt_lib,
"lib_prefix=s" => \$main::opt_lib_prefix,
"functions!" => \$main::opt_functions,
"lines!" => \$main::opt_lines,
"addresses!" => \$main::opt_addresses,
"files!" => \$main::opt_files,
"text!" => \$main::opt_text,
"callgrind!" => \$main::opt_callgrind,
"list=s" => \$main::opt_list,
"disasm=s" => \$main::opt_disasm,
"symbols!" => \$main::opt_symbols,
"gv!" => \$main::opt_gv,
"evince!" => \$main::opt_evince,
"web!" => \$main::opt_web,
"dot!" => \$main::opt_dot,
"ps!" => \$main::opt_ps,
"pdf!" => \$main::opt_pdf,
"svg!" => \$main::opt_svg,
"gif!" => \$main::opt_gif,
"raw!" => \$main::opt_raw,
"collapsed!" => \$main::opt_collapsed,
"interactive!" => \$main::opt_interactive,
"nodecount=i" => \$main::opt_nodecount,
"nodefraction=f" => \$main::opt_nodefraction,
"edgefraction=f" => \$main::opt_edgefraction,
"maxdegree=i" => \$main::opt_maxdegree,
"focus=s" => \$main::opt_focus,
"thread=s" => \$main::opt_thread,
"ignore=s" => \$main::opt_ignore,
"scale=i" => \$main::opt_scale,
"heapcheck" => \$main::opt_heapcheck,
"retain=s" => \$main::opt_retain,
"exclude=s" => \$main::opt_exclude,
"inuse_space!" => \$main::opt_inuse_space,
"inuse_objects!" => \$main::opt_inuse_objects,
"alloc_space!" => \$main::opt_alloc_space,
"alloc_objects!" => \$main::opt_alloc_objects,
"show_bytes!" => \$main::opt_show_bytes,
"drop_negative!" => \$main::opt_drop_negative,
"total_delay!" => \$main::opt_total_delay,
"contentions!" => \$main::opt_contentions,
"mean_delay!" => \$main::opt_mean_delay,
"tools=s" => \$main::opt_tools,
"test!" => \$main::opt_test,
"debug!" => \$main::opt_debug,
"debug-syms-by-id!" => \$main::opt_debug_syms_by_id,
# Undocumented flags used only by unittests:
"test_stride=i" => \$main::opt_test_stride,
) || usage("Invalid option(s)");
# Deal with the standard --help and --version
if ($main::opt_help) {
print usage_string();
exit(0);
}
if ($main::opt_version) {
print version_string();
exit(0);
}
# Disassembly/listing/symbols mode requires address-level info
if ($main::opt_disasm || $main::opt_list || $main::opt_symbols) {
$main::opt_functions = 0;
$main::opt_lines = 0;
$main::opt_addresses = 1;
$main::opt_files = 0;
}
# Check heap-profiling flags
if ($main::opt_inuse_space +
$main::opt_inuse_objects +
$main::opt_alloc_space +
$main::opt_alloc_objects > 1) {
usage("Specify at most on of --inuse/--alloc options");
}
# Check output granularities
my $grains =
$main::opt_functions +
$main::opt_lines +
$main::opt_addresses +
$main::opt_files +
0;
if ($grains > 1) {
usage("Only specify one output granularity option");
}
if ($grains == 0) {
$main::opt_functions = 1;
}
# Check output modes
my $modes =
$main::opt_text +
$main::opt_callgrind +
($main::opt_list eq '' ? 0 : 1) +
($main::opt_disasm eq '' ? 0 : 1) +
($main::opt_symbols == 0 ? 0 : 1) +
$main::opt_gv +
$main::opt_evince +
$main::opt_web +
$main::opt_dot +
$main::opt_ps +
$main::opt_pdf +
$main::opt_svg +
$main::opt_gif +
$main::opt_raw +
$main::opt_collapsed +
$main::opt_interactive +
0;
if ($modes > 1) {
usage("Only specify one output mode");
}
if ($modes == 0) {
if (-t STDOUT) { # If STDOUT is a tty, activate interactive mode
$main::opt_interactive = 1;
} else {
$main::opt_text = 1;
}
}
if ($main::opt_test) {
RunUnitTests();
# Should not return
exit(1);
}
# Binary name and profile arguments list
$main::prog = "";
@main::pfile_args = ();
# Override url_fetcher variable if URL_FETCHER environment variable is set
if ($ENV{URL_FETCHER}) {
@URL_FETCHER = split(' ', $ENV{URL_FETCHER});
}
# Remote profiling without a binary (using $SYMBOL_PAGE instead)
if (@ARGV > 0) {
if (IsProfileURL($ARGV[0])) {
$main::use_symbol_page = 1;
} elsif (IsSymbolizedProfileFile($ARGV[0])) {
$main::use_symbolized_profile = 1;
$main::prog = $UNKNOWN_BINARY; # will be set later from the profile file
}
}
if ($main::use_symbol_page || $main::use_symbolized_profile) {
# We don't need a binary!
my %disabled = ('--lines' => $main::opt_lines,
'--disasm' => $main::opt_disasm);
for my $option (keys %disabled) {
usage("$option cannot be used without a binary") if $disabled{$option};
}
# Set $main::prog later...
scalar(@ARGV) || usage("Did not specify profile file");
} elsif ($main::opt_symbols) {
# --symbols needs a binary-name (to run nm on, etc) but not profiles
$main::prog = shift(@ARGV) || usage("Did not specify program");
} else {
$main::prog = shift(@ARGV) || usage("Did not specify program");
scalar(@ARGV) || usage("Did not specify profile file");
}
# Parse profile file/location arguments
foreach my $farg (@ARGV) {
if ($farg =~ m/(.*)\@([0-9]+)(|\/.*)$/ ) {
my $machine = $1;
my $num_machines = $2;
my $path = $3;
for (my $i = 0; $i < $num_machines; $i++) {
unshift(@main::pfile_args, "$i.$machine$path");
}
} else {
unshift(@main::pfile_args, $farg);
}
}
if ($main::use_symbol_page) {
unless (IsProfileURL($main::pfile_args[0])) {
error("The first profile should be a remote form to use $SYMBOL_PAGE\n");
}
CheckSymbolPage();
$main::prog = FetchProgramName();
} elsif (!$main::use_symbolized_profile) { # may not need objtools!
ConfigureObjTools($main::prog)
}
# Break the opt_lib_prefix into the prefix_list array
@prefix_list = split (',', $main::opt_lib_prefix);
# Remove trailing / from the prefixes, in the list to prevent
# searching things like /my/path//lib/mylib.so
foreach (@prefix_list) {
s|/+$||;
}
# Flag to prevent us from trying over and over to use
# elfutils if it's not installed (used only with
# --debug-syms-by-id option).
$main::gave_up_on_elfutils = 0;
}
sub FilterAndPrint {
my ($profile, $symbols, $libs, $thread) = @_;
# Get total data in profile
my $total = TotalProfile($profile);
# Remove uniniteresting stack items
$profile = RemoveUninterestingFrames($symbols, $profile);
# Focus?
if ($main::opt_focus ne '') {
$profile = FocusProfile($symbols, $profile, $main::opt_focus);
}
# Ignore?
if ($main::opt_ignore ne '') {
$profile = IgnoreProfile($symbols, $profile, $main::opt_ignore);
}
my $calls = ExtractCalls($symbols, $profile);
# Reduce profiles to required output granularity, and also clean
# each stack trace so a given entry exists at most once.
my $reduced = ReduceProfile($symbols, $profile);
# Get derived profiles
my $flat = FlatProfile($reduced);
my $cumulative = CumulativeProfile($reduced);
# Print
if (!$main::opt_interactive) {
if ($main::opt_disasm) {
PrintDisassembly($libs, $flat, $cumulative, $main::opt_disasm);
} elsif ($main::opt_list) {
PrintListing($total, $libs, $flat, $cumulative, $main::opt_list, 0);
} elsif ($main::opt_text) {
# Make sure the output is empty when have nothing to report
# (only matters when --heapcheck is given but we must be
# compatible with old branches that did not pass --heapcheck always):
if ($total != 0) {
printf("Total%s: %s %s\n",
(defined($thread) ? " (t$thread)" : ""),
Unparse($total), Units());
}
PrintText($symbols, $flat, $cumulative, -1);
} elsif ($main::opt_raw) {
PrintSymbolizedProfile($symbols, $profile, $main::prog);
} elsif ($main::opt_collapsed) {
PrintCollapsedStacks($symbols, $profile);
} elsif ($main::opt_callgrind) {
PrintCallgrind($calls);
} else {
if (PrintDot($main::prog, $symbols, $profile, $flat, $cumulative, $total)) {
if ($main::opt_gv) {
RunGV(TempName($main::next_tmpfile, "ps"), "");
} elsif ($main::opt_evince) {
RunEvince(TempName($main::next_tmpfile, "pdf"), "");
} elsif ($main::opt_web) {
my $tmp = TempName($main::next_tmpfile, "svg");
RunWeb($tmp);
# The command we run might hand the file name off
# to an already running browser instance and then exit.
# Normally, we'd remove $tmp on exit (right now),
# but fork a child to remove $tmp a little later, so that the
# browser has time to load it first.
delete $main::tempnames{$tmp};
if (fork() == 0) {
sleep 5;
unlink($tmp);
exit(0);
}
}
} else {
cleanup();
exit(1);
}
}
} else {
InteractiveMode($profile, $symbols, $libs, $total);
}
}
sub Main() {
Init();
$main::collected_profile = undef;
@main::profile_files = ();
$main::op_time = time();
# Printing symbols is special and requires a lot less info that most.
if ($main::opt_symbols) {
PrintSymbols(*STDIN); # Get /proc/maps and symbols output from stdin
return;
}
# Fetch all profile data
FetchDynamicProfiles();
# this will hold symbols that we read from the profile files
my $symbol_map = {};
# Read one profile, pick the last item on the list
my $data = ReadProfile($main::prog, $main::profile_files[0]);
my $profile = $data->{profile};
my $pcs = $data->{pcs};
my $libs = $data->{libs}; # Info about main program and shared libraries
$symbol_map = MergeSymbols($symbol_map, $data->{symbols});
# Add additional profiles, if available.
if (scalar(@main::profile_files) > 1) {
foreach my $pname (@main::profile_files[1..$#main::profile_files]) {
my $data2 = ReadProfile($main::prog, $pname);
$profile = AddProfile($profile, $data2->{profile});
$pcs = AddPcs($pcs, $data2->{pcs});
$symbol_map = MergeSymbols($symbol_map, $data2->{symbols});
}
}
# Subtract base from profile, if specified
if ($main::opt_base ne '') {
my $base = ReadProfile($main::prog, $main::opt_base);
$profile = SubtractProfile($profile, $base->{profile});
$pcs = AddPcs($pcs, $base->{pcs});
$symbol_map = MergeSymbols($symbol_map, $base->{symbols});
}
# Collect symbols
my $symbols;
if ($main::use_symbolized_profile) {
$symbols = FetchSymbols($pcs, $symbol_map);
} elsif ($main::use_symbol_page) {
$symbols = FetchSymbols($pcs);
} else {
# TODO(csilvers): $libs uses the /proc/self/maps data from profile1,
# which may differ from the data from subsequent profiles, especially
# if they were run on different machines. Use appropriate libs for
# each pc somehow.
$symbols = ExtractSymbols($libs, $pcs);
}
if (!defined($main::opt_thread)) {
FilterAndPrint($profile, $symbols, $libs);
}
if (defined($data->{threads})) {
foreach my $thread (sort { $a <=> $b } keys(%{$data->{threads}})) {
if (defined($main::opt_thread) &&
($main::opt_thread eq '*' || $main::opt_thread == $thread)) {
my $thread_profile = $data->{threads}{$thread};
FilterAndPrint($thread_profile, $symbols, $libs, $thread);
}
}
}
cleanup();
exit(0);
}
##### Entry Point #####
Main();
# Temporary code to detect if we're running on a Goobuntu system.
# These systems don't have the right stuff installed for the special
# Readline libraries to work, so as a temporary workaround, we default
# to using the normal stdio code, rather than the fancier readline-based
# code
sub ReadlineMightFail {
if (-e '/lib/libtermcap.so.2') {
return 0; # libtermcap exists, so readline should be okay
} else {
return 1;
}
}
sub RunGV {
my $fname = shift;
my $bg = shift; # "" or " &" if we should run in background
if (!system(ShellEscape(@GV, "--version") . " >$dev_null 2>&1")) {
# Options using double dash are supported by this gv version.
# Also, turn on noantialias to better handle bug in gv for
# postscript files with large dimensions.
# TODO: Maybe we should not pass the --noantialias flag
# if the gv version is known to work properly without the flag.
system(ShellEscape(@GV, "--scale=$main::opt_scale", "--noantialias", $fname)
. $bg);
} else {
# Old gv version - only supports options that use single dash.
print STDERR ShellEscape(@GV, "-scale", $main::opt_scale) . "\n";
system(ShellEscape(@GV, "-scale", "$main::opt_scale", $fname) . $bg);
}
}
sub RunEvince {
my $fname = shift;
my $bg = shift; # "" or " &" if we should run in background
system(ShellEscape(@EVINCE, $fname) . $bg);
}
sub RunWeb {
my $fname = shift;
print STDERR "Loading web page file:///$fname\n";
if (`uname` =~ /Darwin/) {
# OS X: open will use standard preference for SVG files.
system("/usr/bin/open", $fname);
return;
}
# Some kind of Unix; try generic symlinks, then specific browsers.
# (Stop once we find one.)
# Works best if the browser is already running.
my @alt = (
"/etc/alternatives/gnome-www-browser",
"/etc/alternatives/x-www-browser",
"google-chrome",
"firefox",
);
foreach my $b (@alt) {
if (system($b, $fname) == 0) {
return;
}
}
print STDERR "Could not load web browser.\n";
}
sub RunKcachegrind {
my $fname = shift;
my $bg = shift; # "" or " &" if we should run in background
print STDERR "Starting '@KCACHEGRIND " . $fname . $bg . "'\n";
system(ShellEscape(@KCACHEGRIND, $fname) . $bg);
}
##### Interactive helper routines #####
sub InteractiveMode {
$| = 1; # Make output unbuffered for interactive mode
my ($orig_profile, $symbols, $libs, $total) = @_;
print STDERR "Welcome to jeprof! For help, type 'help'.\n";
# Use ReadLine if it's installed and input comes from a console.
if ( -t STDIN &&
!ReadlineMightFail() &&
defined(eval {require Term::ReadLine}) ) {
my $term = new Term::ReadLine 'jeprof';
while ( defined ($_ = $term->readline('(jeprof) '))) {
$term->addhistory($_) if /\S/;
if (!InteractiveCommand($orig_profile, $symbols, $libs, $total, $_)) {
last; # exit when we get an interactive command to quit
}
}
} else { # don't have readline
while (1) {
print STDERR "(jeprof) ";
$_ = ;
last if ! defined $_ ;
s/\r//g; # turn windows-looking lines into unix-looking lines
# Save some flags that might be reset by InteractiveCommand()
my $save_opt_lines = $main::opt_lines;
if (!InteractiveCommand($orig_profile, $symbols, $libs, $total, $_)) {
last; # exit when we get an interactive command to quit
}
# Restore flags
$main::opt_lines = $save_opt_lines;
}
}
}
# Takes two args: orig profile, and command to run.
# Returns 1 if we should keep going, or 0 if we were asked to quit
sub InteractiveCommand {
my($orig_profile, $symbols, $libs, $total, $command) = @_;
$_ = $command; # just to make future m//'s easier
if (!defined($_)) {
print STDERR "\n";
return 0;
}
if (m/^\s*quit/) {
return 0;
}
if (m/^\s*help/) {
InteractiveHelpMessage();
return 1;
}
# Clear all the mode options -- mode is controlled by "$command"
$main::opt_text = 0;
$main::opt_callgrind = 0;
$main::opt_disasm = 0;
$main::opt_list = 0;
$main::opt_gv = 0;
$main::opt_evince = 0;
$main::opt_cum = 0;
if (m/^\s*(text|top)(\d*)\s*(.*)/) {
$main::opt_text = 1;
my $line_limit = ($2 ne "") ? int($2) : 10;
my $routine;
my $ignore;
($routine, $ignore) = ParseInteractiveArgs($3);
my $profile = ProcessProfile($total, $orig_profile, $symbols, "", $ignore);
my $reduced = ReduceProfile($symbols, $profile);
# Get derived profiles
my $flat = FlatProfile($reduced);
my $cumulative = CumulativeProfile($reduced);
PrintText($symbols, $flat, $cumulative, $line_limit);
return 1;
}
if (m/^\s*callgrind\s*([^ \n]*)/) {
$main::opt_callgrind = 1;
# Get derived profiles
my $calls = ExtractCalls($symbols, $orig_profile);
my $filename = $1;
if ( $1 eq '' ) {
$filename = TempName($main::next_tmpfile, "callgrind");
}
PrintCallgrind($calls, $filename);
if ( $1 eq '' ) {
RunKcachegrind($filename, " & ");
$main::next_tmpfile++;
}
return 1;
}
if (m/^\s*(web)?list\s*(.+)/) {
my $html = (defined($1) && ($1 eq "web"));
$main::opt_list = 1;
my $routine;
my $ignore;
($routine, $ignore) = ParseInteractiveArgs($2);
my $profile = ProcessProfile($total, $orig_profile, $symbols, "", $ignore);
my $reduced = ReduceProfile($symbols, $profile);
# Get derived profiles
my $flat = FlatProfile($reduced);
my $cumulative = CumulativeProfile($reduced);
PrintListing($total, $libs, $flat, $cumulative, $routine, $html);
return 1;
}
if (m/^\s*disasm\s*(.+)/) {
$main::opt_disasm = 1;
my $routine;
my $ignore;
($routine, $ignore) = ParseInteractiveArgs($1);
# Process current profile to account for various settings
my $profile = ProcessProfile($total, $orig_profile, $symbols, "", $ignore);
my $reduced = ReduceProfile($symbols, $profile);
# Get derived profiles
my $flat = FlatProfile($reduced);
my $cumulative = CumulativeProfile($reduced);
PrintDisassembly($libs, $flat, $cumulative, $routine);
return 1;
}
if (m/^\s*(gv|web|evince)\s*(.*)/) {
$main::opt_gv = 0;
$main::opt_evince = 0;
$main::opt_web = 0;
if ($1 eq "gv") {
$main::opt_gv = 1;
} elsif ($1 eq "evince") {
$main::opt_evince = 1;
} elsif ($1 eq "web") {
$main::opt_web = 1;
}
my $focus;
my $ignore;
($focus, $ignore) = ParseInteractiveArgs($2);
# Process current profile to account for various settings
my $profile = ProcessProfile($total, $orig_profile, $symbols,
$focus, $ignore);
my $reduced = ReduceProfile($symbols, $profile);
# Get derived profiles
my $flat = FlatProfile($reduced);
my $cumulative = CumulativeProfile($reduced);
if (PrintDot($main::prog, $symbols, $profile, $flat, $cumulative, $total)) {
if ($main::opt_gv) {
RunGV(TempName($main::next_tmpfile, "ps"), " &");
} elsif ($main::opt_evince) {
RunEvince(TempName($main::next_tmpfile, "pdf"), " &");
} elsif ($main::opt_web) {
RunWeb(TempName($main::next_tmpfile, "svg"));
}
$main::next_tmpfile++;
}
return 1;
}
if (m/^\s*$/) {
return 1;
}
print STDERR "Unknown command: try 'help'.\n";
return 1;
}
sub ProcessProfile {
my $total_count = shift;
my $orig_profile = shift;
my $symbols = shift;
my $focus = shift;
my $ignore = shift;
# Process current profile to account for various settings
my $profile = $orig_profile;
printf("Total: %s %s\n", Unparse($total_count), Units());
if ($focus ne '') {
$profile = FocusProfile($symbols, $profile, $focus);
my $focus_count = TotalProfile($profile);
printf("After focusing on '%s': %s %s of %s (%0.1f%%)\n",
$focus,
Unparse($focus_count), Units(),
Unparse($total_count), ($focus_count*100.0) / $total_count);
}
if ($ignore ne '') {
$profile = IgnoreProfile($symbols, $profile, $ignore);
my $ignore_count = TotalProfile($profile);
printf("After ignoring '%s': %s %s of %s (%0.1f%%)\n",
$ignore,
Unparse($ignore_count), Units(),
Unparse($total_count),
($ignore_count*100.0) / $total_count);
}
return $profile;
}
sub InteractiveHelpMessage {
print STDERR <{$k};
my @addrs = split(/\n/, $k);
if ($#addrs >= 0) {
my $depth = $#addrs + 1;
# int(foo / 2**32) is the only reliable way to get rid of bottom
# 32 bits on both 32- and 64-bit systems.
print pack('L*', $count & 0xFFFFFFFF, int($count / 2**32));
print pack('L*', $depth & 0xFFFFFFFF, int($depth / 2**32));
foreach my $full_addr (@addrs) {
my $addr = $full_addr;
$addr =~ s/0x0*//; # strip off leading 0x, zeroes
if (length($addr) > 16) {
print STDERR "Invalid address in profile: $full_addr\n";
next;
}
my $low_addr = substr($addr, -8); # get last 8 hex chars
my $high_addr = substr($addr, -16, 8); # get up to 8 more hex chars
print pack('L*', hex('0x' . $low_addr), hex('0x' . $high_addr));
}
}
}
}
# Print symbols and profile data
sub PrintSymbolizedProfile {
my $symbols = shift;
my $profile = shift;
my $prog = shift;
$SYMBOL_PAGE =~ m,[^/]+$,; # matches everything after the last slash
my $symbol_marker = $&;
print '--- ', $symbol_marker, "\n";
if (defined($prog)) {
print 'binary=', $prog, "\n";
}
while (my ($pc, $name) = each(%{$symbols})) {
my $sep = ' ';
print '0x', $pc;
# We have a list of function names, which include the inlined
# calls. They are separated (and terminated) by --, which is
# illegal in function names.
for (my $j = 2; $j <= $#{$name}; $j += 3) {
print $sep, $name->[$j];
$sep = '--';
}
print "\n";
}
print '---', "\n";
my $profile_marker;
if ($main::profile_type eq 'heap') {
$HEAP_PAGE =~ m,[^/]+$,; # matches everything after the last slash
$profile_marker = $&;
} elsif ($main::profile_type eq 'growth') {
$GROWTH_PAGE =~ m,[^/]+$,; # matches everything after the last slash
$profile_marker = $&;
} elsif ($main::profile_type eq 'contention') {
$CONTENTION_PAGE =~ m,[^/]+$,; # matches everything after the last slash
$profile_marker = $&;
} else { # elsif ($main::profile_type eq 'cpu')
$PROFILE_PAGE =~ m,[^/]+$,; # matches everything after the last slash
$profile_marker = $&;
}
print '--- ', $profile_marker, "\n";
if (defined($main::collected_profile)) {
# if used with remote fetch, simply dump the collected profile to output.
open(SRC, "<$main::collected_profile");
while () {
print $_;
}
close(SRC);
} else {
# --raw/http: For everything to work correctly for non-remote profiles, we
# would need to extend PrintProfileData() to handle all possible profile
# types, re-enable the code that is currently disabled in ReadCPUProfile()
# and FixCallerAddresses(), and remove the remote profile dumping code in
# the block above.
die "--raw/http: jeprof can only dump remote profiles for --raw\n";
# dump a cpu-format profile to standard out
PrintProfileData($profile);
}
}
# Print text output
sub PrintText {
my $symbols = shift;
my $flat = shift;
my $cumulative = shift;
my $line_limit = shift;
my $total = TotalProfile($flat);
# Which profile to sort by?
my $s = $main::opt_cum ? $cumulative : $flat;
my $running_sum = 0;
my $lines = 0;
foreach my $k (sort { GetEntry($s, $b) <=> GetEntry($s, $a) || $a cmp $b }
keys(%{$cumulative})) {
my $f = GetEntry($flat, $k);
my $c = GetEntry($cumulative, $k);
$running_sum += $f;
my $sym = $k;
if (exists($symbols->{$k})) {
$sym = $symbols->{$k}->[0] . " " . $symbols->{$k}->[1];
if ($main::opt_addresses) {
$sym = $k . " " . $sym;
}
}
if ($f != 0 || $c != 0) {
printf("%8s %6s %6s %8s %6s %s\n",
Unparse($f),
Percent($f, $total),
Percent($running_sum, $total),
Unparse($c),
Percent($c, $total),
$sym);
}
$lines++;
last if ($line_limit >= 0 && $lines >= $line_limit);
}
}
# Callgrind format has a compression for repeated function and file
# names. You show the name the first time, and just use its number
# subsequently. This can cut down the file to about a third or a
# quarter of its uncompressed size. $key and $val are the key/value
# pair that would normally be printed by callgrind; $map is a map from
# value to number.
sub CompressedCGName {
my($key, $val, $map) = @_;
my $idx = $map->{$val};
# For very short keys, providing an index hurts rather than helps.
if (length($val) <= 3) {
return "$key=$val\n";
} elsif (defined($idx)) {
return "$key=($idx)\n";
} else {
# scalar(keys $map) gives the number of items in the map.
$idx = scalar(keys(%{$map})) + 1;
$map->{$val} = $idx;
return "$key=($idx) $val\n";
}
}
# Print the call graph in a way that's suiteable for callgrind.
sub PrintCallgrind {
my $calls = shift;
my $filename;
my %filename_to_index_map;
my %fnname_to_index_map;
if ($main::opt_interactive) {
$filename = shift;
print STDERR "Writing callgrind file to '$filename'.\n"
} else {
$filename = "&STDOUT";
}
open(CG, ">$filename");
printf CG ("events: Hits\n\n");
foreach my $call ( map { $_->[0] }
sort { $a->[1] cmp $b ->[1] ||
$a->[2] <=> $b->[2] }
map { /([^:]+):(\d+):([^ ]+)( -> ([^:]+):(\d+):(.+))?/;
[$_, $1, $2] }
keys %$calls ) {
my $count = int($calls->{$call});
$call =~ /([^:]+):(\d+):([^ ]+)( -> ([^:]+):(\d+):(.+))?/;
my ( $caller_file, $caller_line, $caller_function,
$callee_file, $callee_line, $callee_function ) =
( $1, $2, $3, $5, $6, $7 );
# TODO(csilvers): for better compression, collect all the
# caller/callee_files and functions first, before printing
# anything, and only compress those referenced more than once.
printf CG CompressedCGName("fl", $caller_file, \%filename_to_index_map);
printf CG CompressedCGName("fn", $caller_function, \%fnname_to_index_map);
if (defined $6) {
printf CG CompressedCGName("cfl", $callee_file, \%filename_to_index_map);
printf CG CompressedCGName("cfn", $callee_function, \%fnname_to_index_map);
printf CG ("calls=$count $callee_line\n");
}
printf CG ("$caller_line $count\n\n");
}
}
# Print disassembly for all all routines that match $main::opt_disasm
sub PrintDisassembly {
my $libs = shift;
my $flat = shift;
my $cumulative = shift;
my $disasm_opts = shift;
my $total = TotalProfile($flat);
foreach my $lib (@{$libs}) {
my $symbol_table = GetProcedureBoundaries($lib->[0], $disasm_opts);
my $offset = AddressSub($lib->[1], $lib->[3]);
foreach my $routine (sort ByName keys(%{$symbol_table})) {
my $start_addr = $symbol_table->{$routine}->[0];
my $end_addr = $symbol_table->{$routine}->[1];
# See if there are any samples in this routine
my $length = hex(AddressSub($end_addr, $start_addr));
my $addr = AddressAdd($start_addr, $offset);
for (my $i = 0; $i < $length; $i++) {
if (defined($cumulative->{$addr})) {
PrintDisassembledFunction($lib->[0], $offset,
$routine, $flat, $cumulative,
$start_addr, $end_addr, $total);
last;
}
$addr = AddressInc($addr);
}
}
}
}
# Return reference to array of tuples of the form:
# [start_address, filename, linenumber, instruction, limit_address]
# E.g.,
# ["0x806c43d", "/foo/bar.cc", 131, "ret", "0x806c440"]
sub Disassemble {
my $prog = shift;
my $offset = shift;
my $start_addr = shift;
my $end_addr = shift;
my $objdump = $obj_tool_map{"objdump"};
my $cmd = ShellEscape($objdump, "-C", "-d", "-l", "--no-show-raw-insn",
"--start-address=0x$start_addr",
"--stop-address=0x$end_addr", $prog);
open(OBJDUMP, "$cmd |") || error("$cmd: $!\n");
my @result = ();
my $filename = "";
my $linenumber = -1;
my $last = ["", "", "", ""];
while () {
s/\r//g; # turn windows-looking lines into unix-looking lines
chop;
if (m|\s*([^:\s]+):(\d+)\s*$|) {
# Location line of the form:
# :
$filename = $1;
$linenumber = $2;
} elsif (m/^ +([0-9a-f]+):\s*(.*)/) {
# Disassembly line -- zero-extend address to full length
my $addr = HexExtend($1);
my $k = AddressAdd($addr, $offset);
$last->[4] = $k; # Store ending address for previous instruction
$last = [$k, $filename, $linenumber, $2, $end_addr];
push(@result, $last);
}
}
close(OBJDUMP);
return @result;
}
# The input file should contain lines of the form /proc/maps-like
# output (same format as expected from the profiles) or that looks
# like hex addresses (like "0xDEADBEEF"). We will parse all
# /proc/maps output, and for all the hex addresses, we will output
# "short" symbol names, one per line, in the same order as the input.
sub PrintSymbols {
my $maps_and_symbols_file = shift;
# ParseLibraries expects pcs to be in a set. Fine by us...
my @pclist = (); # pcs in sorted order
my $pcs = {};
my $map = "";
foreach my $line (<$maps_and_symbols_file>) {
$line =~ s/\r//g; # turn windows-looking lines into unix-looking lines
if ($line =~ /\b(0x[0-9a-f]+)\b/i) {
push(@pclist, HexExtend($1));
$pcs->{$pclist[-1]} = 1;
} else {
$map .= $line;
}
}
my $libs = ParseLibraries($main::prog, $map, $pcs);
my $symbols = ExtractSymbols($libs, $pcs);
foreach my $pc (@pclist) {
# ->[0] is the shortname, ->[2] is the full name
print(($symbols->{$pc}->[0] || "??") . "\n");
}
}
# For sorting functions by name
sub ByName {
return ShortFunctionName($a) cmp ShortFunctionName($b);
}
# Print source-listing for all all routines that match $list_opts
sub PrintListing {
my $total = shift;
my $libs = shift;
my $flat = shift;
my $cumulative = shift;
my $list_opts = shift;
my $html = shift;
my $output = \*STDOUT;
my $fname = "";
if ($html) {
# Arrange to write the output to a temporary file
$fname = TempName($main::next_tmpfile, "html");
$main::next_tmpfile++;
if (!open(TEMP, ">$fname")) {
print STDERR "$fname: $!\n";
return;
}
$output = \*TEMP;
print $output HtmlListingHeader();
printf $output ("
%s Total: %s %s
\n",
$main::prog, Unparse($total), Units());
}
my $listed = 0;
foreach my $lib (@{$libs}) {
my $symbol_table = GetProcedureBoundaries($lib->[0], $list_opts);
my $offset = AddressSub($lib->[1], $lib->[3]);
foreach my $routine (sort ByName keys(%{$symbol_table})) {
# Print if there are any samples in this routine
my $start_addr = $symbol_table->{$routine}->[0];
my $end_addr = $symbol_table->{$routine}->[1];
my $length = hex(AddressSub($end_addr, $start_addr));
my $addr = AddressAdd($start_addr, $offset);
for (my $i = 0; $i < $length; $i++) {
if (defined($cumulative->{$addr})) {
$listed += PrintSource(
$lib->[0], $offset,
$routine, $flat, $cumulative,
$start_addr, $end_addr,
$html,
$output);
last;
}
$addr = AddressInc($addr);
}
}
}
if ($html) {
if ($listed > 0) {
print $output HtmlListingFooter();
close($output);
RunWeb($fname);
} else {
close($output);
unlink($fname);
}
}
}
sub HtmlListingHeader {
return <<'EOF';
Pprof listing
EOF
}
sub HtmlListingFooter {
return <<'EOF';
EOF
}
sub HtmlEscape {
my $text = shift;
$text =~ s/&/&/g;
$text =~ s/</g;
$text =~ s/>/>/g;
return $text;
}
# Returns the indentation of the line, if it has any non-whitespace
# characters. Otherwise, returns -1.
sub Indentation {
my $line = shift;
if (m/^(\s*)\S/) {
return length($1);
} else {
return -1;
}
}
# If the symbol table contains inlining info, Disassemble() may tag an
# instruction with a location inside an inlined function. But for
# source listings, we prefer to use the location in the function we
# are listing. So use MapToSymbols() to fetch full location
# information for each instruction and then pick out the first
# location from a location list (location list contains callers before
# callees in case of inlining).
#
# After this routine has run, each entry in $instructions contains:
# [0] start address
# [1] filename for function we are listing
# [2] line number for function we are listing
# [3] disassembly
# [4] limit address
# [5] most specific filename (may be different from [1] due to inlining)
# [6] most specific line number (may be different from [2] due to inlining)
sub GetTopLevelLineNumbers {
my ($lib, $offset, $instructions) = @_;
my $pcs = [];
for (my $i = 0; $i <= $#{$instructions}; $i++) {
push(@{$pcs}, $instructions->[$i]->[0]);
}
my $symbols = {};
MapToSymbols($lib, $offset, $pcs, $symbols);
for (my $i = 0; $i <= $#{$instructions}; $i++) {
my $e = $instructions->[$i];
push(@{$e}, $e->[1]);
push(@{$e}, $e->[2]);
my $addr = $e->[0];
my $sym = $symbols->{$addr};
if (defined($sym)) {
if ($#{$sym} >= 2 && $sym->[1] =~ m/^(.*):(\d+)$/) {
$e->[1] = $1; # File name
$e->[2] = $2; # Line number
}
}
}
}
# Print source-listing for one routine
sub PrintSource {
my $prog = shift;
my $offset = shift;
my $routine = shift;
my $flat = shift;
my $cumulative = shift;
my $start_addr = shift;
my $end_addr = shift;
my $html = shift;
my $output = shift;
# Disassemble all instructions (just to get line numbers)
my @instructions = Disassemble($prog, $offset, $start_addr, $end_addr);
GetTopLevelLineNumbers($prog, $offset, \@instructions);
# Hack 1: assume that the first source file encountered in the
# disassembly contains the routine
my $filename = undef;
for (my $i = 0; $i <= $#instructions; $i++) {
if ($instructions[$i]->[2] >= 0) {
$filename = $instructions[$i]->[1];
last;
}
}
if (!defined($filename)) {
print STDERR "no filename found in $routine\n";
return 0;
}
# Hack 2: assume that the largest line number from $filename is the
# end of the procedure. This is typically safe since if P1 contains
# an inlined call to P2, then P2 usually occurs earlier in the
# source file. If this does not work, we might have to compute a
# density profile or just print all regions we find.
my $lastline = 0;
for (my $i = 0; $i <= $#instructions; $i++) {
my $f = $instructions[$i]->[1];
my $l = $instructions[$i]->[2];
if (($f eq $filename) && ($l > $lastline)) {
$lastline = $l;
}
}
# Hack 3: assume the first source location from "filename" is the start of
# the source code.
my $firstline = 1;
for (my $i = 0; $i <= $#instructions; $i++) {
if ($instructions[$i]->[1] eq $filename) {
$firstline = $instructions[$i]->[2];
last;
}
}
# Hack 4: Extend last line forward until its indentation is less than
# the indentation we saw on $firstline
my $oldlastline = $lastline;
{
if (!open(FILE, "<$filename")) {
print STDERR "$filename: $!\n";
return 0;
}
my $l = 0;
my $first_indentation = -1;
while () {
s/\r//g; # turn windows-looking lines into unix-looking lines
$l++;
my $indent = Indentation($_);
if ($l >= $firstline) {
if ($first_indentation < 0 && $indent >= 0) {
$first_indentation = $indent;
last if ($first_indentation == 0);
}
}
if ($l >= $lastline && $indent >= 0) {
if ($indent >= $first_indentation) {
$lastline = $l+1;
} else {
last;
}
}
}
close(FILE);
}
# Assign all samples to the range $firstline,$lastline,
# Hack 4: If an instruction does not occur in the range, its samples
# are moved to the next instruction that occurs in the range.
my $samples1 = {}; # Map from line number to flat count
my $samples2 = {}; # Map from line number to cumulative count
my $running1 = 0; # Unassigned flat counts
my $running2 = 0; # Unassigned cumulative counts
my $total1 = 0; # Total flat counts
my $total2 = 0; # Total cumulative counts
my %disasm = (); # Map from line number to disassembly
my $running_disasm = ""; # Unassigned disassembly
my $skip_marker = "---\n";
if ($html) {
$skip_marker = "";
for (my $l = $firstline; $l <= $lastline; $l++) {
$disasm{$l} = "";
}
}
my $last_dis_filename = '';
my $last_dis_linenum = -1;
my $last_touched_line = -1; # To detect gaps in disassembly for a line
foreach my $e (@instructions) {
# Add up counts for all address that fall inside this instruction
my $c1 = 0;
my $c2 = 0;
for (my $a = $e->[0]; $a lt $e->[4]; $a = AddressInc($a)) {
$c1 += GetEntry($flat, $a);
$c2 += GetEntry($cumulative, $a);
}
if ($html) {
my $dis = sprintf(" %6s %6s \t\t%8s: %s ",
HtmlPrintNumber($c1),
HtmlPrintNumber($c2),
UnparseAddress($offset, $e->[0]),
CleanDisassembly($e->[3]));
# Append the most specific source line associated with this instruction
if (length($dis) < 80) { $dis .= (' ' x (80 - length($dis))) };
$dis = HtmlEscape($dis);
my $f = $e->[5];
my $l = $e->[6];
if ($f ne $last_dis_filename) {
$dis .= sprintf("%s:%d",
HtmlEscape(CleanFileName($f)), $l);
} elsif ($l ne $last_dis_linenum) {
# De-emphasize the unchanged file name portion
$dis .= sprintf("%s" .
":%d",
HtmlEscape(CleanFileName($f)), $l);
} else {
# De-emphasize the entire location
$dis .= sprintf("%s:%d",
HtmlEscape(CleanFileName($f)), $l);
}
$last_dis_filename = $f;
$last_dis_linenum = $l;
$running_disasm .= $dis;
$running_disasm .= "\n";
}
$running1 += $c1;
$running2 += $c2;
$total1 += $c1;
$total2 += $c2;
my $file = $e->[1];
my $line = $e->[2];
if (($file eq $filename) &&
($line >= $firstline) &&
($line <= $lastline)) {
# Assign all accumulated samples to this line
AddEntry($samples1, $line, $running1);
AddEntry($samples2, $line, $running2);
$running1 = 0;
$running2 = 0;
if ($html) {
if ($line != $last_touched_line && $disasm{$line} ne '') {
$disasm{$line} .= "\n";
}
$disasm{$line} .= $running_disasm;
$running_disasm = '';
$last_touched_line = $line;
}
}
}
# Assign any leftover samples to $lastline
AddEntry($samples1, $lastline, $running1);
AddEntry($samples2, $lastline, $running2);
if ($html) {
if ($lastline != $last_touched_line && $disasm{$lastline} ne '') {
$disasm{$lastline} .= "\n";
}
$disasm{$lastline} .= $running_disasm;
}
if ($html) {
printf $output (
"
%s
%s\n
\n" .
"Total:%6s %6s (flat / cumulative %s)\n",
HtmlEscape(ShortFunctionName($routine)),
HtmlEscape(CleanFileName($filename)),
Unparse($total1),
Unparse($total2),
Units());
} else {
printf $output (
"ROUTINE ====================== %s in %s\n" .
"%6s %6s Total %s (flat / cumulative)\n",
ShortFunctionName($routine),
CleanFileName($filename),
Unparse($total1),
Unparse($total2),
Units());
}
if (!open(FILE, "<$filename")) {
print STDERR "$filename: $!\n";
return 0;
}
my $l = 0;
while () {
s/\r//g; # turn windows-looking lines into unix-looking lines
$l++;
if ($l >= $firstline - 5 &&
(($l <= $oldlastline + 5) || ($l <= $lastline))) {
chop;
my $text = $_;
if ($l == $firstline) { print $output $skip_marker; }
my $n1 = GetEntry($samples1, $l);
my $n2 = GetEntry($samples2, $l);
if ($html) {
# Emit a span that has one of the following classes:
# livesrc -- has samples
# deadsrc -- has disassembly, but with no samples
# nop -- has no matching disasembly
# Also emit an optional span containing disassembly.
my $dis = $disasm{$l};
my $asm = "";
if (defined($dis) && $dis ne '') {
$asm = "" . $dis . "";
}
my $source_class = (($n1 + $n2 > 0)
? "livesrc"
: (($asm ne "") ? "deadsrc" : "nop"));
printf $output (
"%5d " .
"%6s %6s %s%s\n",
$l, $source_class,
HtmlPrintNumber($n1),
HtmlPrintNumber($n2),
HtmlEscape($text),
$asm);
} else {
printf $output(
"%6s %6s %4d: %s\n",
UnparseAlt($n1),
UnparseAlt($n2),
$l,
$text);
}
if ($l == $lastline) { print $output $skip_marker; }
};
}
close(FILE);
if ($html) {
print $output "
\n";
}
return 1;
}
# Return the source line for the specified file/linenumber.
# Returns undef if not found.
sub SourceLine {
my $file = shift;
my $line = shift;
# Look in cache
if (!defined($main::source_cache{$file})) {
if (100 < scalar keys(%main::source_cache)) {
# Clear the cache when it gets too big
$main::source_cache = ();
}
# Read all lines from the file
if (!open(FILE, "<$file")) {
print STDERR "$file: $!\n";
$main::source_cache{$file} = []; # Cache the negative result
return undef;
}
my $lines = [];
push(@{$lines}, ""); # So we can use 1-based line numbers as indices
while () {
push(@{$lines}, $_);
}
close(FILE);
# Save the lines in the cache
$main::source_cache{$file} = $lines;
}
my $lines = $main::source_cache{$file};
if (($line < 0) || ($line > $#{$lines})) {
return undef;
} else {
return $lines->[$line];
}
}
# Print disassembly for one routine with interspersed source if available
sub PrintDisassembledFunction {
my $prog = shift;
my $offset = shift;
my $routine = shift;
my $flat = shift;
my $cumulative = shift;
my $start_addr = shift;
my $end_addr = shift;
my $total = shift;
# Disassemble all instructions
my @instructions = Disassemble($prog, $offset, $start_addr, $end_addr);
# Make array of counts per instruction
my @flat_count = ();
my @cum_count = ();
my $flat_total = 0;
my $cum_total = 0;
foreach my $e (@instructions) {
# Add up counts for all address that fall inside this instruction
my $c1 = 0;
my $c2 = 0;
for (my $a = $e->[0]; $a lt $e->[4]; $a = AddressInc($a)) {
$c1 += GetEntry($flat, $a);
$c2 += GetEntry($cumulative, $a);
}
push(@flat_count, $c1);
push(@cum_count, $c2);
$flat_total += $c1;
$cum_total += $c2;
}
# Print header with total counts
printf("ROUTINE ====================== %s\n" .
"%6s %6s %s (flat, cumulative) %.1f%% of total\n",
ShortFunctionName($routine),
Unparse($flat_total),
Unparse($cum_total),
Units(),
($cum_total * 100.0) / $total);
# Process instructions in order
my $current_file = "";
for (my $i = 0; $i <= $#instructions; ) {
my $e = $instructions[$i];
# Print the new file name whenever we switch files
if ($e->[1] ne $current_file) {
$current_file = $e->[1];
my $fname = $current_file;
$fname =~ s|^\./||; # Trim leading "./"
# Shorten long file names
if (length($fname) >= 58) {
$fname = "..." . substr($fname, -55);
}
printf("-------------------- %s\n", $fname);
}
# TODO: Compute range of lines to print together to deal with
# small reorderings.
my $first_line = $e->[2];
my $last_line = $first_line;
my %flat_sum = ();
my %cum_sum = ();
for (my $l = $first_line; $l <= $last_line; $l++) {
$flat_sum{$l} = 0;
$cum_sum{$l} = 0;
}
# Find run of instructions for this range of source lines
my $first_inst = $i;
while (($i <= $#instructions) &&
($instructions[$i]->[2] >= $first_line) &&
($instructions[$i]->[2] <= $last_line)) {
$e = $instructions[$i];
$flat_sum{$e->[2]} += $flat_count[$i];
$cum_sum{$e->[2]} += $cum_count[$i];
$i++;
}
my $last_inst = $i - 1;
# Print source lines
for (my $l = $first_line; $l <= $last_line; $l++) {
my $line = SourceLine($current_file, $l);
if (!defined($line)) {
$line = "?\n";
next;
} else {
$line =~ s/^\s+//;
}
printf("%6s %6s %5d: %s",
UnparseAlt($flat_sum{$l}),
UnparseAlt($cum_sum{$l}),
$l,
$line);
}
# Print disassembly
for (my $x = $first_inst; $x <= $last_inst; $x++) {
my $e = $instructions[$x];
printf("%6s %6s %8s: %6s\n",
UnparseAlt($flat_count[$x]),
UnparseAlt($cum_count[$x]),
UnparseAddress($offset, $e->[0]),
CleanDisassembly($e->[3]));
}
}
}
# Print DOT graph
sub PrintDot {
my $prog = shift;
my $symbols = shift;
my $raw = shift;
my $flat = shift;
my $cumulative = shift;
my $overall_total = shift;
# Get total
my $local_total = TotalProfile($flat);
my $nodelimit = int($main::opt_nodefraction * $local_total);
my $edgelimit = int($main::opt_edgefraction * $local_total);
my $nodecount = $main::opt_nodecount;
# Find nodes to include
my @list = (sort { abs(GetEntry($cumulative, $b)) <=>
abs(GetEntry($cumulative, $a))
|| $a cmp $b }
keys(%{$cumulative}));
my $last = $nodecount - 1;
if ($last > $#list) {
$last = $#list;
}
while (($last >= 0) &&
(abs(GetEntry($cumulative, $list[$last])) <= $nodelimit)) {
$last--;
}
if ($last < 0) {
print STDERR "No nodes to print\n";
return 0;
}
if ($nodelimit > 0 || $edgelimit > 0) {
printf STDERR ("Dropping nodes with <= %s %s; edges with <= %s abs(%s)\n",
Unparse($nodelimit), Units(),
Unparse($edgelimit), Units());
}
# Open DOT output file
my $output;
my $escaped_dot = ShellEscape(@DOT);
my $escaped_ps2pdf = ShellEscape(@PS2PDF);
if ($main::opt_gv) {
my $escaped_outfile = ShellEscape(TempName($main::next_tmpfile, "ps"));
$output = "| $escaped_dot -Tps2 >$escaped_outfile";
} elsif ($main::opt_evince) {
my $escaped_outfile = ShellEscape(TempName($main::next_tmpfile, "pdf"));
$output = "| $escaped_dot -Tps2 | $escaped_ps2pdf - $escaped_outfile";
} elsif ($main::opt_ps) {
$output = "| $escaped_dot -Tps2";
} elsif ($main::opt_pdf) {
$output = "| $escaped_dot -Tps2 | $escaped_ps2pdf - -";
} elsif ($main::opt_web || $main::opt_svg) {
# We need to post-process the SVG, so write to a temporary file always.
my $escaped_outfile = ShellEscape(TempName($main::next_tmpfile, "svg"));
$output = "| $escaped_dot -Tsvg >$escaped_outfile";
} elsif ($main::opt_gif) {
$output = "| $escaped_dot -Tgif";
} else {
$output = ">&STDOUT";
}
open(DOT, $output) || error("$output: $!\n");
# Title
printf DOT ("digraph \"%s; %s %s\" {\n",
$prog,
Unparse($overall_total),
Units());
if ($main::opt_pdf) {
# The output is more printable if we set the page size for dot.
printf DOT ("size=\"8,11\"\n");
}
printf DOT ("node [width=0.375,height=0.25];\n");
# Print legend
printf DOT ("Legend [shape=box,fontsize=24,shape=plaintext," .
"label=\"%s\\l%s\\l%s\\l%s\\l%s\\l\"];\n",
$prog,
sprintf("Total %s: %s", Units(), Unparse($overall_total)),
sprintf("Focusing on: %s", Unparse($local_total)),
sprintf("Dropped nodes with <= %s abs(%s)",
Unparse($nodelimit), Units()),
sprintf("Dropped edges with <= %s %s",
Unparse($edgelimit), Units())
);
# Print nodes
my %node = ();
my $nextnode = 1;
foreach my $a (@list[0..$last]) {
# Pick font size
my $f = GetEntry($flat, $a);
my $c = GetEntry($cumulative, $a);
my $fs = 8;
if ($local_total > 0) {
$fs = 8 + (50.0 * sqrt(abs($f * 1.0 / $local_total)));
}
$node{$a} = $nextnode++;
my $sym = $a;
$sym =~ s/\s+/\\n/g;
$sym =~ s/::/\\n/g;
# Extra cumulative info to print for non-leaves
my $extra = "";
if ($f != $c) {
$extra = sprintf("\\rof %s (%s)",
Unparse($c),
Percent($c, $local_total));
}
my $style = "";
if ($main::opt_heapcheck) {
if ($f > 0) {
# make leak-causing nodes more visible (add a background)
$style = ",style=filled,fillcolor=gray"
} elsif ($f < 0) {
# make anti-leak-causing nodes (which almost never occur)
# stand out as well (triple border)
$style = ",peripheries=3"
}
}
printf DOT ("N%d [label=\"%s\\n%s (%s)%s\\r" .
"\",shape=box,fontsize=%.1f%s];\n",
$node{$a},
$sym,
Unparse($f),
Percent($f, $local_total),
$extra,
$fs,
$style,
);
}
# Get edges and counts per edge
my %edge = ();
my $n;
my $fullname_to_shortname_map = {};
FillFullnameToShortnameMap($symbols, $fullname_to_shortname_map);
foreach my $k (keys(%{$raw})) {
# TODO: omit low %age edges
$n = $raw->{$k};
my @translated = TranslateStack($symbols, $fullname_to_shortname_map, $k);
for (my $i = 1; $i <= $#translated; $i++) {
my $src = $translated[$i];
my $dst = $translated[$i-1];
#next if ($src eq $dst); # Avoid self-edges?
if (exists($node{$src}) && exists($node{$dst})) {
my $edge_label = "$src\001$dst";
if (!exists($edge{$edge_label})) {
$edge{$edge_label} = 0;
}
$edge{$edge_label} += $n;
}
}
}
# Print edges (process in order of decreasing counts)
my %indegree = (); # Number of incoming edges added per node so far
my %outdegree = (); # Number of outgoing edges added per node so far
foreach my $e (sort { $edge{$b} <=> $edge{$a} } keys(%edge)) {
my @x = split(/\001/, $e);
$n = $edge{$e};
# Initialize degree of kept incoming and outgoing edges if necessary
my $src = $x[0];
my $dst = $x[1];
if (!exists($outdegree{$src})) { $outdegree{$src} = 0; }
if (!exists($indegree{$dst})) { $indegree{$dst} = 0; }
my $keep;
if ($indegree{$dst} == 0) {
# Keep edge if needed for reachability
$keep = 1;
} elsif (abs($n) <= $edgelimit) {
# Drop if we are below --edgefraction
$keep = 0;
} elsif ($outdegree{$src} >= $main::opt_maxdegree ||
$indegree{$dst} >= $main::opt_maxdegree) {
# Keep limited number of in/out edges per node
$keep = 0;
} else {
$keep = 1;
}
if ($keep) {
$outdegree{$src}++;
$indegree{$dst}++;
# Compute line width based on edge count
my $fraction = abs($local_total ? (3 * ($n / $local_total)) : 0);
if ($fraction > 1) { $fraction = 1; }
my $w = $fraction * 2;
if ($w < 1 && ($main::opt_web || $main::opt_svg)) {
# SVG output treats line widths < 1 poorly.
$w = 1;
}
# Dot sometimes segfaults if given edge weights that are too large, so
# we cap the weights at a large value
my $edgeweight = abs($n) ** 0.7;
if ($edgeweight > 100000) { $edgeweight = 100000; }
$edgeweight = int($edgeweight);
my $style = sprintf("setlinewidth(%f)", $w);
if ($x[1] =~ m/\(inline\)/) {
$style .= ",dashed";
}
# Use a slightly squashed function of the edge count as the weight
printf DOT ("N%s -> N%s [label=%s, weight=%d, style=\"%s\"];\n",
$node{$x[0]},
$node{$x[1]},
Unparse($n),
$edgeweight,
$style);
}
}
print DOT ("}\n");
close(DOT);
if ($main::opt_web || $main::opt_svg) {
# Rewrite SVG to be more usable inside web browser.
RewriteSvg(TempName($main::next_tmpfile, "svg"));
}
return 1;
}
sub RewriteSvg {
my $svgfile = shift;
open(SVG, $svgfile) || die "open temp svg: $!";
my @svg =