Repository: pingcap/tidb-dashboard Branch: master Commit: 5d39d7905988 Files: 1662 Total size: 6.1 MB Directory structure: gitextract_tfv9ubrh/ ├── .all-contributorsrc ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ ├── feature-request.md │ │ └── question.md │ ├── autolabeler.yml │ ├── codecov.yml │ ├── stale.yml │ └── workflows/ │ ├── build.yaml │ ├── manual-create-pd-pr.yaml │ ├── release.yaml │ ├── test-docker-image.yaml │ ├── test.yaml │ └── upload-e2e-snapshots.yaml ├── .gitignore ├── .golangci.yml ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── SECURITY.md ├── cmd/ │ └── tidb-dashboard/ │ └── main.go ├── dockerfiles/ │ └── docker-compose.yml ├── etc/ │ ├── go.mod │ └── manualTestEnv/ │ ├── .gitignore │ ├── _shared/ │ │ ├── Vagrantfile.partial.pubKey.rb │ │ ├── vagrant_key │ │ └── vagrant_key.pub │ ├── complexCase1/ │ │ ├── README.md │ │ ├── Vagrantfile │ │ └── topology.yaml │ ├── multiHost/ │ │ ├── README.md │ │ ├── Vagrantfile │ │ └── topology.yaml │ ├── multiReplica/ │ │ ├── README.md │ │ ├── Vagrantfile │ │ └── topology.yaml │ ├── singleHost/ │ │ ├── README.md │ │ ├── Vagrantfile │ │ └── topology.yaml │ └── singleHostMultiDisk/ │ ├── .gitignore │ ├── README.md │ ├── Vagrantfile │ └── topology.yaml ├── go.mod ├── go.sum ├── pkg/ │ ├── apiserver/ │ │ ├── apiserver.go │ │ ├── clusterinfo/ │ │ │ ├── host.go │ │ │ ├── hostinfo/ │ │ │ │ ├── cluster_config.go │ │ │ │ ├── cluster_hardware.go │ │ │ │ ├── cluster_load.go │ │ │ │ └── hostinfo.go │ │ │ ├── service.go │ │ │ ├── statistics.go │ │ │ └── topology.go │ │ ├── configuration/ │ │ │ ├── editable.go │ │ │ ├── flatten.go │ │ │ ├── router.go │ │ │ └── service.go │ │ ├── conprof/ │ │ │ ├── module.go │ │ │ └── service.go │ │ ├── deadlock/ │ │ │ ├── model.go │ │ │ ├── module.go │ │ │ └── service.go │ │ ├── debugapi/ │ │ │ ├── apis.go │ │ │ ├── endpoint/ │ │ │ │ ├── 1_main_test.go │ │ │ │ ├── errors.go │ │ │ │ ├── models.go │ │ │ │ ├── models_test.go │ │ │ │ ├── payload.go │ │ │ │ └── payload_test.go │ │ │ ├── module.go │ │ │ └── service.go │ │ ├── diagnose/ │ │ │ ├── compare.go │ │ │ ├── diagnose.go │ │ │ ├── inspection.go │ │ │ ├── model.go │ │ │ ├── query.go │ │ │ ├── report.go │ │ │ └── report_test.go │ │ ├── info/ │ │ │ └── info.go │ │ ├── logsearch/ │ │ │ ├── models.go │ │ │ ├── pack.go │ │ │ ├── scheduler.go │ │ │ ├── service.go │ │ │ └── task.go │ │ ├── metrics/ │ │ │ ├── prom_resolve.go │ │ │ ├── prom_resolve_test.go │ │ │ ├── router.go │ │ │ └── service.go │ │ ├── model/ │ │ │ └── common_models.go │ │ ├── profiling/ │ │ │ ├── fetcher.go │ │ │ ├── jeprof.in │ │ │ ├── model.go │ │ │ ├── module.go │ │ │ ├── pprof.go │ │ │ ├── profile.go │ │ │ ├── protobuf_to_svg.go │ │ │ ├── router.go │ │ │ └── service.go │ │ ├── queryeditor/ │ │ │ └── service.go │ │ ├── resource_manager/ │ │ │ ├── module.go │ │ │ └── service.go │ │ ├── slowquery/ │ │ │ ├── model.go │ │ │ ├── module.go │ │ │ ├── queries.go │ │ │ ├── service.go │ │ │ └── statement_gen.go │ │ ├── statement/ │ │ │ ├── config.go │ │ │ ├── config_test.go │ │ │ ├── models.go │ │ │ ├── module.go │ │ │ ├── queries.go │ │ │ ├── service.go │ │ │ └── statement_gen.go │ │ ├── topsql/ │ │ │ ├── module.go │ │ │ └── service.go │ │ ├── user/ │ │ │ ├── auth.go │ │ │ ├── code/ │ │ │ │ ├── codeauth/ │ │ │ │ │ └── auth.go │ │ │ │ ├── router.go │ │ │ │ └── service.go │ │ │ ├── module.go │ │ │ ├── rsa_utils.go │ │ │ ├── sqlauth/ │ │ │ │ └── sqlauth.go │ │ │ ├── sso/ │ │ │ │ ├── models.go │ │ │ │ ├── router.go │ │ │ │ ├── service.go │ │ │ │ └── ssoauth/ │ │ │ │ └── auth.go │ │ │ ├── verify_sql_user.go │ │ │ └── verify_sql_user_test.go │ │ ├── utils/ │ │ │ ├── auth.go │ │ │ ├── binary_plan.go │ │ │ ├── binary_plan_test.go │ │ │ ├── error.go │ │ │ ├── export.go │ │ │ ├── gorm.go │ │ │ ├── gorm_test.go │ │ │ ├── jwt.go │ │ │ ├── mw_experimental.go │ │ │ ├── ngm.go │ │ │ ├── subset.go │ │ │ └── tidb_conn.go │ │ └── visualplan/ │ │ ├── module.go │ │ └── service.go │ ├── config/ │ │ ├── config.go │ │ ├── dynamic_config.go │ │ └── dynamic_config_manager.go │ ├── dbstore/ │ │ └── dbstore.go │ ├── httpc/ │ │ ├── client.go │ │ └── client_test.go │ ├── keyvisual/ │ │ ├── decorator/ │ │ │ ├── decorator.go │ │ │ ├── decorator_test.go │ │ │ ├── separator.go │ │ │ ├── tidb.go │ │ │ ├── tidb_requests.go │ │ │ └── tidb_test.go │ │ ├── input/ │ │ │ ├── api.go │ │ │ ├── file.go │ │ │ ├── input.go │ │ │ └── periodic.go │ │ ├── manager.go │ │ ├── matrix/ │ │ │ ├── average.go │ │ │ ├── average_test.go │ │ │ ├── axis.go │ │ │ ├── axis_test.go │ │ │ ├── distance.go │ │ │ ├── distance_test.go │ │ │ ├── interface.go │ │ │ ├── key.go │ │ │ ├── key_test.go │ │ │ ├── matrix.go │ │ │ ├── matrix_test.go │ │ │ ├── plane.go │ │ │ ├── plane_test.go │ │ │ ├── util.go │ │ │ └── util_test.go │ │ ├── region/ │ │ │ ├── interface.go │ │ │ ├── tag.go │ │ │ └── utils.go │ │ ├── service.go │ │ ├── storage/ │ │ │ ├── model.go │ │ │ ├── model_test.go │ │ │ ├── region.go │ │ │ ├── region_test.go │ │ │ ├── stat.go │ │ │ ├── stat_persist.go │ │ │ └── stat_test.go │ │ └── testdata/ │ │ └── dis.json.gzip │ ├── pd/ │ │ ├── client.go │ │ ├── client_test.go │ │ ├── etcd.go │ │ └── pd.go │ ├── scheduling/ │ │ ├── client.go │ │ └── scheduling.go │ ├── swaggerserver/ │ │ └── handler.go │ ├── ticdc/ │ │ ├── client.go │ │ └── ticdc.go │ ├── tidb/ │ │ ├── client.go │ │ ├── forwarder.go │ │ ├── model/ │ │ │ ├── codec.go │ │ │ ├── codec_test.go │ │ │ └── model.go │ │ ├── proxy.go │ │ ├── proxy_test.go │ │ └── tidb.go │ ├── tiflash/ │ │ ├── client.go │ │ └── tiflash.go │ ├── tikv/ │ │ ├── client.go │ │ └── tikv.go │ ├── tiproxy/ │ │ ├── client.go │ │ └── tiproxy.go │ ├── tso/ │ │ ├── client.go │ │ └── tso.go │ ├── uiserver/ │ │ ├── .gitignore │ │ ├── embedded_assets_rewriter.go │ │ ├── empty_assets_handler.go │ │ └── uiserver.go │ └── utils/ │ ├── fx.go │ ├── grpc.go │ ├── service_status.go │ ├── sys_schema.go │ ├── topology/ │ │ ├── models.go │ │ ├── monitor.go │ │ ├── pd.go │ │ ├── scheduling.go │ │ ├── store.go │ │ ├── ticdc.go │ │ ├── tidb.go │ │ ├── tiproxy.go │ │ ├── topology.go │ │ └── tso.go │ └── version/ │ ├── fips.go │ └── version.go ├── release-version ├── scripts/ │ ├── _inc/ │ │ ├── download_tools.sh │ │ └── run_services.sh │ ├── create_release_tag.js │ ├── distro/ │ │ ├── write_strings.go │ │ └── write_strings.sh │ ├── embed_ui_assets.sh │ ├── generate_assets.go │ ├── generate_swagger_spec.sh │ ├── go.mod │ ├── go.sum │ ├── install_go_tools.sh │ ├── lint.sh │ ├── pd_version_matrix.go │ ├── start_tiup.sh │ ├── tiup.config.toml │ ├── tools.go │ └── wait_tiup_playground.sh ├── swaggerspec/ │ ├── .gitignore │ └── placeholder.go ├── tests/ │ ├── create_table.sh │ ├── dump.sh │ ├── fixtures/ │ │ └── CLUSTER_SLOW_QUERY.yml │ ├── integration/ │ │ ├── diagnose_report_test.go │ │ ├── info/ │ │ │ └── info_test.go │ │ ├── slowquery/ │ │ │ ├── compatibility_test.go │ │ │ └── mock_db_test.go │ │ └── user/ │ │ └── user_test.go │ ├── run.sh │ ├── schema/ │ │ └── test.CLUSTER_SLOW_QUERY-schema.sql │ └── util/ │ ├── compatibility.go │ ├── dump/ │ │ └── dump.go │ ├── fixtures.go │ ├── gin_test_helper.go │ ├── mock_app.go │ └── tidb_version.go ├── ui/ │ ├── .editorconfig │ ├── .eslintrc.js │ ├── .gitignore │ ├── .husky/ │ │ └── pre-commit │ ├── .prettierignore │ ├── .prettierrc │ ├── OWNERS │ ├── README.md │ ├── go.mod │ ├── less-vars.js │ ├── netlify.toml │ ├── package.json │ ├── packages/ │ │ ├── clinic-client/ │ │ │ ├── gulpfile.js │ │ │ ├── openapitools.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── client/ │ │ │ │ └── api/ │ │ │ │ ├── api.ts │ │ │ │ ├── base.ts │ │ │ │ ├── common.ts │ │ │ │ ├── configuration.ts │ │ │ │ └── index.ts │ │ │ ├── swagger/ │ │ │ │ ├── .openapi_config.yaml │ │ │ │ ├── gen_api.sh │ │ │ │ └── spec.json │ │ │ └── tsconfig.json │ │ ├── tidb-dashboard-client/ │ │ │ ├── gulpfile.js │ │ │ ├── openapitools.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── client/ │ │ │ │ └── api/ │ │ │ │ ├── api/ │ │ │ │ │ ├── default-api.ts │ │ │ │ │ └── statement-api.ts │ │ │ │ ├── api.ts │ │ │ │ ├── base.ts │ │ │ │ ├── common.ts │ │ │ │ ├── configuration.ts │ │ │ │ ├── index.ts │ │ │ │ └── models/ │ │ │ │ ├── clusterinfo-cluster-statistics-partial.ts │ │ │ │ ├── clusterinfo-cluster-statistics.ts │ │ │ │ ├── clusterinfo-get-hosts-info-response.ts │ │ │ │ ├── clusterinfo-store-topology-response.ts │ │ │ │ ├── code-share-request.ts │ │ │ │ ├── code-share-response.ts │ │ │ │ ├── config-key-visual-config.ts │ │ │ │ ├── config-profiling-config.ts │ │ │ │ ├── config-ssocore-config.ts │ │ │ │ ├── configuration-all-config-items.ts │ │ │ │ ├── configuration-edit-request.ts │ │ │ │ ├── configuration-edit-response.ts │ │ │ │ ├── configuration-item.ts │ │ │ │ ├── conprof-component-num.ts │ │ │ │ ├── conprof-component.ts │ │ │ │ ├── conprof-continuous-profiling-config.ts │ │ │ │ ├── conprof-estimate-size-res.ts │ │ │ │ ├── conprof-group-profile-detail.ts │ │ │ │ ├── conprof-group-profiles.ts │ │ │ │ ├── conprof-ng-monitoring-config.ts │ │ │ │ ├── conprof-profile-detail.ts │ │ │ │ ├── conprof-target.ts │ │ │ │ ├── deadlock-model.ts │ │ │ │ ├── decorator-label-key.ts │ │ │ │ ├── diagnose-gen-diagnosis-report-request.ts │ │ │ │ ├── diagnose-generate-metrics-relation-request.ts │ │ │ │ ├── diagnose-generate-report-request.ts │ │ │ │ ├── diagnose-report.ts │ │ │ │ ├── diagnose-table-def.ts │ │ │ │ ├── diagnose-table-row-def.ts │ │ │ │ ├── endpoint-apidefinition.ts │ │ │ │ ├── endpoint-apiparam-definition.ts │ │ │ │ ├── endpoint-request-payload.ts │ │ │ │ ├── hostinfo-cpuinfo.ts │ │ │ │ ├── hostinfo-cpuusage-info.ts │ │ │ │ ├── hostinfo-info.ts │ │ │ │ ├── hostinfo-instance-info.ts │ │ │ │ ├── hostinfo-memory-usage-info.ts │ │ │ │ ├── hostinfo-partition-info.ts │ │ │ │ ├── index.ts │ │ │ │ ├── info-info-response.ts │ │ │ │ ├── info-table-schema.ts │ │ │ │ ├── info-who-am-iresponse.ts │ │ │ │ ├── logsearch-create-task-group-request.ts │ │ │ │ ├── logsearch-preview-model.ts │ │ │ │ ├── logsearch-search-log-request.ts │ │ │ │ ├── logsearch-task-group-model.ts │ │ │ │ ├── logsearch-task-group-response.ts │ │ │ │ ├── logsearch-task-model.ts │ │ │ │ ├── matrix-matrix.ts │ │ │ │ ├── metrics-get-prom-address-config-response.ts │ │ │ │ ├── metrics-put-custom-prom-address-request.ts │ │ │ │ ├── metrics-put-custom-prom-address-response.ts │ │ │ │ ├── metrics-query-response.ts │ │ │ │ ├── model-request-target-node.ts │ │ │ │ ├── model-request-target-statistics.ts │ │ │ │ ├── profiling-group-detail-response.ts │ │ │ │ ├── profiling-start-request.ts │ │ │ │ ├── profiling-task-group-model.ts │ │ │ │ ├── profiling-task-model.ts │ │ │ │ ├── queryeditor-run-request.ts │ │ │ │ ├── queryeditor-run-response.ts │ │ │ │ ├── resourcemanager-calibrate-response.ts │ │ │ │ ├── resourcemanager-get-config-response.ts │ │ │ │ ├── resourcemanager-resource-info-row-def.ts │ │ │ │ ├── rest-error-response.ts │ │ │ │ ├── slowquery-get-list-request.ts │ │ │ │ ├── slowquery-model.ts │ │ │ │ ├── sso-create-impersonation-request.ts │ │ │ │ ├── sso-set-config-request.ts │ │ │ │ ├── sso-ssoimpersonation-model.ts │ │ │ │ ├── statement-binding.ts │ │ │ │ ├── statement-editable-config.ts │ │ │ │ ├── statement-get-statements-request.ts │ │ │ │ ├── statement-model.ts │ │ │ │ ├── statement-time-range.ts │ │ │ │ ├── topology-alert-manager-info.ts │ │ │ │ ├── topology-grafana-info.ts │ │ │ │ ├── topology-pdinfo.ts │ │ │ │ ├── topology-scheduling-info.ts │ │ │ │ ├── topology-store-info.ts │ │ │ │ ├── topology-store-labels.ts │ │ │ │ ├── topology-store-location.ts │ │ │ │ ├── topology-ti-cdcinfo.ts │ │ │ │ ├── topology-ti-dbinfo.ts │ │ │ │ ├── topology-ti-proxy-info.ts │ │ │ │ ├── topology-tsoinfo.ts │ │ │ │ ├── topsql-editable-config.ts │ │ │ │ ├── topsql-instance-item.ts │ │ │ │ ├── topsql-instance-response.ts │ │ │ │ ├── topsql-summary-by-item.ts │ │ │ │ ├── topsql-summary-item.ts │ │ │ │ ├── topsql-summary-plan-item.ts │ │ │ │ ├── topsql-summary-response.ts │ │ │ │ ├── topsql-tikv-network-io-collection-config.ts │ │ │ │ ├── topsql-update-tikv-network-io-collection-response.ts │ │ │ │ ├── user-authenticate-form.ts │ │ │ │ ├── user-get-login-info-response.ts │ │ │ │ ├── user-sign-out-info.ts │ │ │ │ ├── user-token-response.ts │ │ │ │ └── version-info.ts │ │ │ ├── swagger/ │ │ │ │ ├── .openapi_config.yaml │ │ │ │ ├── gen_api.sh │ │ │ │ └── spec.json │ │ │ └── tsconfig.json │ │ ├── tidb-dashboard-for-clinic-cloud/ │ │ │ ├── .gitignore │ │ │ ├── builder.js │ │ │ ├── gulpfile.js │ │ │ ├── package.json │ │ │ ├── process-shim.js │ │ │ ├── public/ │ │ │ │ ├── diagnose-report/ │ │ │ │ │ └── index.html │ │ │ │ ├── graphvizlib.wasm │ │ │ │ ├── index.html │ │ │ │ └── ngm.html │ │ │ ├── src/ │ │ │ │ ├── apps/ │ │ │ │ │ ├── ClusterInfo/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Configuration/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── ContinuousProfiling/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Deadlock/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── DebugAPI/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Diagnose/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── InstanceProfiling/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── KeyViz/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Monitoring/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── meta.ts │ │ │ │ │ │ └── metricsQueries.ts │ │ │ │ │ ├── OptimizerTrace/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Overview/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── meta.ts │ │ │ │ │ │ └── metricsQueries.ts │ │ │ │ │ ├── QueryEditor/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── ResourceManager/ │ │ │ │ │ │ ├── context-impl.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── SearchLogs/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── SlowQuery/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Statement/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── SystemReport/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── TopSQL/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── TopSlowQuery/ │ │ │ │ │ │ ├── context-provider.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── meta.ts │ │ │ │ │ │ └── sample-data/ │ │ │ │ │ │ └── slowqueries.json │ │ │ │ │ └── UserProfile/ │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── meta.ts │ │ │ │ ├── client/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ ├── dashboardApp/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── layout/ │ │ │ │ │ │ ├── main/ │ │ │ │ │ │ │ ├── Sider/ │ │ │ │ │ │ │ │ ├── Banner.module.less │ │ │ │ │ │ │ │ ├── Banner.tsx │ │ │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ ├── main.tsx │ │ │ │ │ └── nprogress.less │ │ │ │ ├── diagnoseReportApp/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── DiagnosisReport.tsx │ │ │ │ │ │ └── DiagnosisTable.tsx │ │ │ │ │ ├── index.css │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── react-app-env.d.ts │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── types.ts │ │ │ │ ├── react-app-env.d.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── override.less │ │ │ │ │ └── style.less │ │ │ │ └── utils/ │ │ │ │ ├── distro/ │ │ │ │ │ ├── assetsRes.ts │ │ │ │ │ ├── stringsRes.ts │ │ │ │ │ └── strings_res.json │ │ │ │ ├── globalConfig.ts │ │ │ │ ├── publicPathPrefix.ts │ │ │ │ ├── registry.ts │ │ │ │ └── store.ts │ │ │ └── tsconfig.json │ │ ├── tidb-dashboard-for-clinic-op/ │ │ │ ├── README.md │ │ │ ├── builder.js │ │ │ ├── gulpfile.js │ │ │ ├── package.json │ │ │ ├── process-shim.js │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ ├── src/ │ │ │ │ ├── App.tsx │ │ │ │ ├── apps/ │ │ │ │ │ └── SlowQuery/ │ │ │ │ │ ├── context.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── client/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ ├── index.tsx │ │ │ │ ├── react-app-env.d.ts │ │ │ │ └── styles/ │ │ │ │ ├── override.less │ │ │ │ └── style.less │ │ │ └── tsconfig.json │ │ ├── tidb-dashboard-for-op/ │ │ │ ├── builder.js │ │ │ ├── cypress/ │ │ │ │ ├── .eslintrc.json │ │ │ │ ├── README.md │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── topsql_instance:end=1641934800&start=1641916800.json │ │ │ │ │ ├── topsql_summary:end=1641934800&instance=127.0.0.1%3A10080&instance_type=tidb&start=1641916800&top=5&window=123s.json │ │ │ │ │ ├── topsql_summary_large_timerange:end=1641916800&instance=127.0.0.1%3A10080&instance_type=tidb&start=1641484800&top=5&window=2929s.json │ │ │ │ │ ├── topsql_summary_small_timerange:end=1641920460&instance=127.0.0.1%3A10080&instance_type=tidb&start=1641920400&top=5&window=1s.json │ │ │ │ │ └── uri.json │ │ │ │ ├── integration/ │ │ │ │ │ ├── components.js │ │ │ │ │ ├── login/ │ │ │ │ │ │ ├── login_session.spec.js │ │ │ │ │ │ ├── user_login.compat_spec.js │ │ │ │ │ │ └── user_login.spec.js │ │ │ │ │ ├── slow_query/ │ │ │ │ │ │ ├── 01-list.spec.js │ │ │ │ │ │ ├── 02-detail.spec.js │ │ │ │ │ │ └── list.compat_spec.js │ │ │ │ │ ├── statement/ │ │ │ │ │ │ ├── 01-list.spec.js │ │ │ │ │ │ ├── 02-detail.spec.js │ │ │ │ │ │ └── list.compat_spec.js │ │ │ │ │ ├── topsql/ │ │ │ │ │ │ ├── topsql.spec.ts │ │ │ │ │ │ ├── topsql.without_ngm_spec.js │ │ │ │ │ │ └── topsql_security.spec.js │ │ │ │ │ └── utils.js │ │ │ │ ├── plugins/ │ │ │ │ │ └── index.js │ │ │ │ ├── support/ │ │ │ │ │ ├── commands.js │ │ │ │ │ └── index.js │ │ │ │ ├── tsconfig.json │ │ │ │ └── types/ │ │ │ │ ├── global.d.ts │ │ │ │ └── mocha.d.ts │ │ │ ├── cypress.json │ │ │ ├── gulpfile.js │ │ │ ├── package.json │ │ │ ├── process-shim.js │ │ │ ├── public/ │ │ │ │ ├── .gitignore │ │ │ │ ├── diagnoseReport.html │ │ │ │ ├── graphvizlib.wasm │ │ │ │ ├── index.html │ │ │ │ └── test-portal/ │ │ │ │ └── index.html │ │ │ ├── src/ │ │ │ │ ├── apps/ │ │ │ │ │ ├── ClusterInfo/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Configuration/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── ContinuousProfiling/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Deadlock/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── DebugAPI/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Diagnose/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── InstanceProfiling/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── KeyViz/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Monitoring/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── meta.ts │ │ │ │ │ │ └── metricsQueries.ts │ │ │ │ │ ├── OptimizerTrace/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Overview/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── meta.ts │ │ │ │ │ │ └── metricsQueries.ts │ │ │ │ │ ├── QueryEditor/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── ResourceManager/ │ │ │ │ │ │ ├── context-impl.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── SearchLogs/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── SlowQuery/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── Statement/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── SystemReport/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ ├── TopSQL/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── meta.ts │ │ │ │ │ └── UserProfile/ │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── meta.ts │ │ │ │ ├── client/ │ │ │ │ │ ├── apiBasePath.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ ├── dashboardApp/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── layout/ │ │ │ │ │ │ ├── main/ │ │ │ │ │ │ │ ├── Sider/ │ │ │ │ │ │ │ │ ├── Banner.module.less │ │ │ │ │ │ │ │ ├── Banner.tsx │ │ │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── signin/ │ │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ ├── main.tsx │ │ │ │ │ └── nprogress.less │ │ │ │ ├── diagnoseReportApp/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── DiagnosisReport.tsx │ │ │ │ │ │ └── DiagnosisTable.tsx │ │ │ │ │ ├── index.css │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── react-app-env.d.ts │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── types.ts │ │ │ │ ├── react-app-env.d.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── override.less │ │ │ │ │ └── style.less │ │ │ │ └── utils/ │ │ │ │ ├── appOptions.ts │ │ │ │ ├── auth.ts │ │ │ │ ├── authSSO.ts │ │ │ │ ├── distro/ │ │ │ │ │ ├── assetsRes.ts │ │ │ │ │ ├── stringsRes.ts │ │ │ │ │ └── strings_res.json │ │ │ │ ├── publicPathPrefix.ts │ │ │ │ ├── registry.ts │ │ │ │ └── store.ts │ │ │ └── tsconfig.json │ │ └── tidb-dashboard-lib/ │ │ ├── builder.js │ │ ├── gulpfile.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── apps/ │ │ │ │ ├── ClusterInfo/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── DiskTable.tsx │ │ │ │ │ │ ├── HostTable.tsx │ │ │ │ │ │ ├── InstanceTable.tsx │ │ │ │ │ │ ├── Statistics.module.less │ │ │ │ │ │ ├── Statistics.tsx │ │ │ │ │ │ ├── StoreLocation.tsx │ │ │ │ │ │ └── StoreLocationTree/ │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ ├── index.stories.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── List.module.less │ │ │ │ │ │ └── List.tsx │ │ │ │ │ ├── status/ │ │ │ │ │ │ └── status.ts │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ ├── Configuration/ │ │ │ │ │ ├── InlineEditor.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ ├── ContinuousProfiling/ │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── ConProfSettingForm.tsx │ │ │ │ │ │ ├── Detail.tsx │ │ │ │ │ │ ├── List.module.less │ │ │ │ │ │ ├── List.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ └── telemetry.ts │ │ │ │ ├── Deadlock/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── DeadlockChainGraph.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── Detail.tsx │ │ │ │ │ │ ├── List.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ ├── DebugAPI/ │ │ │ │ │ ├── apilist/ │ │ │ │ │ │ ├── ApiForm.tsx │ │ │ │ │ │ ├── ApiList.module.less │ │ │ │ │ │ ├── ApiList.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── widgets/ │ │ │ │ │ │ ├── Database.tsx │ │ │ │ │ │ ├── Enum.tsx │ │ │ │ │ │ ├── Host.tsx │ │ │ │ │ │ ├── Table.tsx │ │ │ │ │ │ ├── TableID.tsx │ │ │ │ │ │ ├── Text.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── useLimitSelection.ts │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ ├── Diagnose/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── DiagnosisTable.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── DiagnoseGenerator.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ └── tableColumns.tsx │ │ │ │ ├── InstanceProfiling/ │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── Detail.tsx │ │ │ │ │ │ ├── List.module.less │ │ │ │ │ │ ├── List.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ ├── combineTargetStats.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── KeyViz/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── KeyViz.less │ │ │ │ │ │ ├── KeyViz.tsx │ │ │ │ │ │ ├── KeyVizSettingForm.tsx │ │ │ │ │ │ └── KeyVizToolbar.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── heatmap/ │ │ │ │ │ │ ├── axis/ │ │ │ │ │ │ │ ├── histogram.ts │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── label-axis.ts │ │ │ │ │ │ ├── buffer.ts │ │ │ │ │ │ ├── chart.ts │ │ │ │ │ │ ├── color.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── legend.ts │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── telemetry.ts │ │ │ │ ├── Monitoring/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── Monitoring.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ └── telemetry.ts │ │ │ │ ├── OptimizerTrace/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── LogicalOperatorTree.tsx │ │ │ │ │ │ ├── OperatorTree.module.less │ │ │ │ │ │ ├── PhysicalCostTree.tsx │ │ │ │ │ │ └── PhysicalOperatorTree.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── examples/ │ │ │ │ │ │ ├── new-format.json │ │ │ │ │ │ └── old-format.json │ │ │ │ │ ├── index.module.less │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ ├── Overview/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── Instances.tsx │ │ │ │ │ │ ├── Metrics.tsx │ │ │ │ │ │ ├── MonitorAlert.tsx │ │ │ │ │ │ └── Styles.module.less │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ └── telemetry.ts │ │ │ │ ├── QueryEditor/ │ │ │ │ │ ├── Editor.module.less │ │ │ │ │ ├── Editor.tsx │ │ │ │ │ ├── ResultTable.module.less │ │ │ │ │ ├── ResultTable.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── editorThemes/ │ │ │ │ │ │ ├── oneHalfDark.js │ │ │ │ │ │ └── oneHalfLight.js │ │ │ │ │ ├── index.module.less │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ ├── ResourceManager/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── Configuration.tsx │ │ │ │ │ │ ├── EstimateCapacity.tsx │ │ │ │ │ │ ├── Metrics.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── uilts/ │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── metricQueries.ts │ │ │ │ │ └── url-state.ts │ │ │ │ ├── SQLAdvisor/ │ │ │ │ │ ├── component/ │ │ │ │ │ │ ├── IndexInsightList.tsx │ │ │ │ │ │ ├── IndexInsightListWithRegister.module.less │ │ │ │ │ │ ├── IndexInsightListWithRegister.tsx │ │ │ │ │ │ ├── IndexInsightTable.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── Detail/ │ │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── List/ │ │ │ │ │ │ │ ├── List.module.less │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ ├── types/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── utils/ │ │ │ │ │ ├── dbaasSecuritySetting.ts │ │ │ │ │ └── suggestedCommandMaps.tsx │ │ │ │ ├── SearchLogs/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── Icon.tsx │ │ │ │ │ │ ├── LogRow.module.less │ │ │ │ │ │ ├── LogRow.tsx │ │ │ │ │ │ ├── SearchHeader.tsx │ │ │ │ │ │ ├── SearchProgress.tsx │ │ │ │ │ │ ├── SearchResult.tsx │ │ │ │ │ │ ├── Styles.module.less │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── LogSearch.tsx │ │ │ │ │ │ ├── LogSearchDetail.tsx │ │ │ │ │ │ ├── LogSearchHistory.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ └── index.ts │ │ │ │ ├── SlowQuery/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── LimitTimeRange.tsx │ │ │ │ │ │ ├── SlowQueriesTable.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── Detail/ │ │ │ │ │ │ │ ├── DetailTabBasic.tsx │ │ │ │ │ │ │ ├── DetailTabCopr.tsx │ │ │ │ │ │ │ ├── DetailTabRuV2.tsx │ │ │ │ │ │ │ ├── DetailTabTime.tsx │ │ │ │ │ │ │ ├── DetailTabTxn.tsx │ │ │ │ │ │ │ ├── DetailTabs.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── List/ │ │ │ │ │ │ │ ├── DownloadDBFileModal.tsx │ │ │ │ │ │ │ ├── List.module.less │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ ├── detail-url-state.ts │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── list-url-state.ts │ │ │ │ │ ├── tableColumns.tsx │ │ │ │ │ ├── telemetry.ts │ │ │ │ │ ├── useSchemaColumns.ts │ │ │ │ │ └── useSlowQueryTableController.ts │ │ │ │ ├── Statement/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── StatementsTable.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── Detail/ │ │ │ │ │ │ │ ├── PlanBind.module.less │ │ │ │ │ │ │ ├── PlanBind.tsx │ │ │ │ │ │ │ ├── PlanDetail.tsx │ │ │ │ │ │ │ ├── PlanDetailTabBasic.tsx │ │ │ │ │ │ │ ├── PlanDetailTabCopr.tsx │ │ │ │ │ │ │ ├── PlanDetailTabTime.tsx │ │ │ │ │ │ │ ├── PlanDetailTabTxn.tsx │ │ │ │ │ │ │ ├── PlanDetailTabs.tsx │ │ │ │ │ │ │ ├── SlowQueryTab.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── List/ │ │ │ │ │ │ │ ├── List.module.less │ │ │ │ │ │ │ ├── StatementSettingForm.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ ├── tableColumns.tsx │ │ │ │ │ ├── telemetry.ts │ │ │ │ │ ├── useSchemaColumns.ts │ │ │ │ │ └── useStatementTableController.ts │ │ │ │ ├── SystemReport/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── ReportHistory.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── ReportGenerator.tsx │ │ │ │ │ │ ├── ReportStatus.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ ├── TopSQL/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── Filter/ │ │ │ │ │ │ ├── InstanceSelect.tsx │ │ │ │ │ │ ├── common.module.less │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ └── List/ │ │ │ │ │ │ ├── List.module.less │ │ │ │ │ │ ├── List.tsx │ │ │ │ │ │ ├── ListChart.tsx │ │ │ │ │ │ ├── ListDetail/ │ │ │ │ │ │ │ ├── ListDetail.tsx │ │ │ │ │ │ │ ├── ListDetailContent.tsx │ │ │ │ │ │ │ ├── ListDetailTable.tsx │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── ListTable.tsx │ │ │ │ │ │ ├── SettingsForm.module.less │ │ │ │ │ │ ├── SettingsForm.tsx │ │ │ │ │ │ └── legendAction.ts │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ ├── specialRecord.ts │ │ │ │ │ ├── telemetry.ts │ │ │ │ │ └── useRecordSelection.ts │ │ │ │ ├── TopSlowQuery/ │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── CountChart.tsx │ │ │ │ │ │ ├── List.module.less │ │ │ │ │ │ ├── List.tsx │ │ │ │ │ │ └── ListTable.tsx │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── uilts/ │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── telemetry.ts │ │ │ │ │ └── url-state.ts │ │ │ │ ├── UserProfile/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── Form.Language.tsx │ │ │ │ │ │ ├── Form.PrometheusAddr.tsx │ │ │ │ │ │ ├── Form.SSO.tsx │ │ │ │ │ │ ├── Form.Session.tsx │ │ │ │ │ │ ├── Form.Version.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── translations/ │ │ │ │ │ │ ├── en.yaml │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.yaml │ │ │ │ │ └── utils/ │ │ │ │ │ └── helper.ts │ │ │ │ └── index.ts │ │ │ ├── client/ │ │ │ │ ├── clinic-extensions.d.ts │ │ │ │ ├── index.ts │ │ │ │ └── models.ts │ │ │ ├── components/ │ │ │ │ ├── AnimatedSkeleton/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── AppearAnimate/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── AutoRefreshButton/ │ │ │ │ │ ├── AutoRefreshButton.tsx │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.ts │ │ │ │ ├── Bar/ │ │ │ │ │ ├── Bar.module.less │ │ │ │ │ ├── Bar.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── BaseSelect/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ ├── index.stories.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── BinaryPlanTable/ │ │ │ │ │ ├── BinaryPlanColsSelector.tsx │ │ │ │ │ ├── index.module.less │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── sample-data/ │ │ │ │ │ ├── binary-plan.json │ │ │ │ │ └── detail-res.json │ │ │ │ ├── Blink/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Card/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── CardTable/ │ │ │ │ │ ├── GroupHeader.tsx │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── CardTabs/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── ColumnsSelector/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── CopyLink/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── DatePicker/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── DateTime/ │ │ │ │ │ ├── Calendar.tsx │ │ │ │ │ ├── Long.tsx │ │ │ │ │ ├── calendarPlugin.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── Descriptions/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── DrawerFooter/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── ErrorBar/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Expand/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Head/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── HighlightSQL/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── InstanceSelect/ │ │ │ │ │ ├── DropOverlay.tsx │ │ │ │ │ ├── TableWithFilter.module.less │ │ │ │ │ ├── TableWithFilter.tsx │ │ │ │ │ ├── ValueDisplay.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── InstanceStatusBadge/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── LanguageDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── LimitTimeRange/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── MultiSelect/ │ │ │ │ │ ├── DropOverlay.tsx │ │ │ │ │ ├── Plain.tsx │ │ │ │ │ ├── index.stories.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Ngm/ │ │ │ │ │ ├── NgmNotStarted.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── ParamsPageWrapper/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── PlanText/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Pre/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Root/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TextWithInfo/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TextWrap/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── TimePicker/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TimeRangeSelector/ │ │ │ │ │ ├── WithZoomOut.tsx │ │ │ │ │ ├── hook.tsx │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Toolbar/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── TxtDownloadLink/ │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── ValueWithTooltip/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── VisualPlan/ │ │ │ │ │ ├── DetailDrawer.tsx │ │ │ │ │ ├── VisualPlan.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── translations/ │ │ │ │ │ ├── en.yaml │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.yaml │ │ │ │ └── index.ts │ │ │ ├── hooks/ │ │ │ │ ├── useLocationChange.ts │ │ │ │ ├── useQueryParams.ts │ │ │ │ └── useURLTimeRange.ts │ │ │ ├── index.ts │ │ │ ├── react-app-env.d.ts │ │ │ ├── types/ │ │ │ │ ├── index.ts │ │ │ │ └── reqConfig.ts │ │ │ └── utils/ │ │ │ ├── charts.ts │ │ │ ├── distro.ts │ │ │ ├── format.ts │ │ │ ├── i18n.ts │ │ │ ├── index.ts │ │ │ ├── instanceTable.ts │ │ │ ├── local-download.ts │ │ │ ├── openLink.ts │ │ │ ├── prometheus/ │ │ │ │ ├── data.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── query.ts │ │ │ ├── routing.ts │ │ │ ├── selectionWithFilter.ts │ │ │ ├── sqlFormatter/ │ │ │ │ └── index.ts │ │ │ ├── store.ts │ │ │ ├── tableColumnFactory.tsx │ │ │ ├── tableColumns.tsx │ │ │ ├── telemetry.ts │ │ │ ├── timezone.ts │ │ │ ├── useCache.ts │ │ │ ├── useCacheItemIndex.ts │ │ │ ├── useChange.ts │ │ │ ├── useClientRequest.ts │ │ │ ├── useOrderState.ts │ │ │ ├── useQueryParams.ts │ │ │ ├── useVersionedLocalStorageState.ts │ │ │ └── wdyr.ts │ │ └── tsconfig.json │ ├── pnpm-workspace.yaml │ └── tsconfig.json ├── ui-v2/ │ ├── .changeset/ │ │ ├── README.md │ │ └── config.json │ ├── .gitignore │ ├── .husky/ │ │ └── pre-commit │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── api-specs/ │ │ └── azores.json │ ├── eslint.config.js │ ├── orval.config.ts │ ├── package.json │ ├── packages/ │ │ ├── api/ │ │ │ ├── client/ │ │ │ │ ├── .gitignore │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── package.json │ │ │ │ ├── rollup.config.js │ │ │ │ ├── src/ │ │ │ │ │ ├── http/ │ │ │ │ │ │ └── client.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── server/ │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── azores/ │ │ │ │ ├── handlers/ │ │ │ │ │ ├── apiKeyServiceCreateApiKey.ts │ │ │ │ │ ├── apiKeyServiceDeleteApiKey.ts │ │ │ │ │ ├── apiKeyServiceGetApiKey.ts │ │ │ │ │ ├── apiKeyServiceGetTemErrorDetail.ts │ │ │ │ │ ├── apiKeyServiceListApiKeys.ts │ │ │ │ │ ├── apiKeyServiceResetSecretKey.ts │ │ │ │ │ ├── apiKeyServiceUpdateApiKey.ts │ │ │ │ │ ├── clusterBRServiceCreateBackupTask.ts │ │ │ │ │ ├── clusterBRServiceCreateRestoreTask.ts │ │ │ │ │ ├── clusterBRServiceDetectCluster.ts │ │ │ │ │ ├── clusterBRServiceGetClusterBackupPolicy.ts │ │ │ │ │ ├── clusterBRServiceListClusterBRTasks.ts │ │ │ │ │ ├── clusterBRServiceListClusterBackupRecords.ts │ │ │ │ │ ├── clusterServiceDeleteProcess.ts │ │ │ │ │ ├── clusterServiceGetProcessList.ts │ │ │ │ │ ├── credentialServiceCreateCredential.ts │ │ │ │ │ ├── credentialServiceDeleteCredential.ts │ │ │ │ │ ├── credentialServiceDownloadRSAKey.ts │ │ │ │ │ ├── credentialServiceGenerateRSAKey.ts │ │ │ │ │ ├── credentialServiceGetCredential.ts │ │ │ │ │ ├── credentialServiceListCredentials.ts │ │ │ │ │ ├── credentialServiceUpdateCredential.ts │ │ │ │ │ ├── credentialServiceValidateConnection.ts │ │ │ │ │ ├── diagnosisServiceAddSqlLimit.ts │ │ │ │ │ ├── diagnosisServiceBindSqlPlan.ts │ │ │ │ │ ├── diagnosisServiceCheckSqlLimitSupport.ts │ │ │ │ │ ├── diagnosisServiceCheckSqlPlanSupport.ts │ │ │ │ │ ├── diagnosisServiceDownloadSlowQueryList.ts │ │ │ │ │ ├── diagnosisServiceGetResourceGroupList.ts │ │ │ │ │ ├── diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfo.ts │ │ │ │ │ ├── diagnosisServiceGetSlowQueryAvailableAdvancedFilters.ts │ │ │ │ │ ├── diagnosisServiceGetSlowQueryAvailableFields.ts │ │ │ │ │ ├── diagnosisServiceGetSlowQueryDetail.ts │ │ │ │ │ ├── diagnosisServiceGetSlowQueryList.ts │ │ │ │ │ ├── diagnosisServiceGetSqlLimitList.ts │ │ │ │ │ ├── diagnosisServiceGetSqlPlanBindingList.ts │ │ │ │ │ ├── diagnosisServiceGetSqlPlanList.ts │ │ │ │ │ ├── diagnosisServiceGetTopSqlAvailableAdvancedFilterInfo.ts │ │ │ │ │ ├── diagnosisServiceGetTopSqlAvailableAdvancedFilters.ts │ │ │ │ │ ├── diagnosisServiceGetTopSqlAvailableFields.ts │ │ │ │ │ ├── diagnosisServiceGetTopSqlConfigs.ts │ │ │ │ │ ├── diagnosisServiceGetTopSqlDetail.ts │ │ │ │ │ ├── diagnosisServiceGetTopSqlList.ts │ │ │ │ │ ├── diagnosisServiceRemoveSqlLimit.ts │ │ │ │ │ ├── diagnosisServiceUnbindSqlPlan.ts │ │ │ │ │ ├── diagnosisServiceUpdateTopSqlConfigs.ts │ │ │ │ │ ├── globalBRServiceCreateBackupPolicy.ts │ │ │ │ │ ├── globalBRServiceDeleteBRTask.ts │ │ │ │ │ ├── globalBRServiceDeleteBackupPolicy.ts │ │ │ │ │ ├── globalBRServiceGetBRSummary.ts │ │ │ │ │ ├── globalBRServiceGetBackupPolicy.ts │ │ │ │ │ ├── globalBRServiceListBRTasks.ts │ │ │ │ │ ├── globalBRServiceListBackupPolicies.ts │ │ │ │ │ ├── globalBRServicePreCheckBackupPolicy.ts │ │ │ │ │ ├── globalBRServiceStartBRTask.ts │ │ │ │ │ ├── globalBRServiceStopBRTask.ts │ │ │ │ │ ├── globalBRServiceUpdateBackupPolicy.ts │ │ │ │ │ ├── hostServiceBatchDelete.ts │ │ │ │ │ ├── hostServiceCheck.ts │ │ │ │ │ ├── hostServiceCreateHosts.ts │ │ │ │ │ ├── hostServiceDelete.ts │ │ │ │ │ ├── hostServiceDownloadHostTemplate.ts │ │ │ │ │ ├── hostServiceDownloadListHosts.ts │ │ │ │ │ ├── hostServiceFix.ts │ │ │ │ │ ├── hostServiceGetDisks.ts │ │ │ │ │ ├── hostServiceGetHost.ts │ │ │ │ │ ├── hostServiceGetTiDBProcesses.ts │ │ │ │ │ ├── hostServiceHostConfirm.ts │ │ │ │ │ ├── hostServiceImport.ts │ │ │ │ │ ├── hostServiceImportTask.ts │ │ │ │ │ ├── hostServiceListHosts.ts │ │ │ │ │ ├── hostServiceReport.ts │ │ │ │ │ ├── hostServiceUpdateHost.ts │ │ │ │ │ ├── labelServiceBatchCreateLabels.ts │ │ │ │ │ ├── labelServiceBindLabel.ts │ │ │ │ │ ├── labelServiceBindResource.ts │ │ │ │ │ ├── labelServiceCreateLabel.ts │ │ │ │ │ ├── labelServiceDeleteLabel.ts │ │ │ │ │ ├── labelServiceGetLabel.ts │ │ │ │ │ ├── labelServiceGetLabelWithBindings.ts │ │ │ │ │ ├── labelServiceListLabelKeys.ts │ │ │ │ │ ├── labelServiceListLabels.ts │ │ │ │ │ ├── labelServiceListLabelsByResourceType.ts │ │ │ │ │ ├── labelServiceListLabelsWithBindings.ts │ │ │ │ │ ├── labelServiceUpdateLabel.ts │ │ │ │ │ ├── licenseServiceActivateFreeLicense.ts │ │ │ │ │ ├── licenseServiceActivateLicense.ts │ │ │ │ │ ├── licenseServiceGetDeviceCode.ts │ │ │ │ │ ├── licenseServiceGetLicense.ts │ │ │ │ │ ├── locationServiceCreateLocations.ts │ │ │ │ │ ├── locationServiceDeleteLocation.ts │ │ │ │ │ ├── locationServiceGetLocations.ts │ │ │ │ │ ├── locationServiceListLocations.ts │ │ │ │ │ ├── locationServiceUpdateLocations.ts │ │ │ │ │ ├── metricsServiceGetClusterMetricData.ts │ │ │ │ │ ├── metricsServiceGetClusterMetricInstance.ts │ │ │ │ │ ├── metricsServiceGetHostMetricData.ts │ │ │ │ │ ├── metricsServiceGetMetrics.ts │ │ │ │ │ ├── metricsServiceGetOverviewStatus.ts │ │ │ │ │ ├── metricsServiceGetTopMetricConfig.ts │ │ │ │ │ ├── metricsServiceGetTopMetricData.ts │ │ │ │ │ ├── roleServiceCreateRole.ts │ │ │ │ │ ├── roleServiceDeleteRole.ts │ │ │ │ │ ├── roleServiceListRoles.ts │ │ │ │ │ ├── roleServiceUpdateRole.ts │ │ │ │ │ ├── tagServiceBatchCreateTags.ts │ │ │ │ │ ├── tagServiceBindResource.ts │ │ │ │ │ ├── tagServiceBindTag.ts │ │ │ │ │ ├── tagServiceCreateTag.ts │ │ │ │ │ ├── tagServiceDeleteTag.ts │ │ │ │ │ ├── tagServiceGetTag.ts │ │ │ │ │ ├── tagServiceGetTagWithBindings.ts │ │ │ │ │ ├── tagServiceListTagKeys.ts │ │ │ │ │ ├── tagServiceListTags.ts │ │ │ │ │ ├── tagServiceListTagsByResourceType.ts │ │ │ │ │ ├── tagServiceListTagsWithBindings.ts │ │ │ │ │ ├── tagServiceUpdateTag.ts │ │ │ │ │ ├── tiupsServiceCreateTiups.ts │ │ │ │ │ ├── tiupsServiceDeleteTiups.ts │ │ │ │ │ ├── tiupsServiceGetTiups.ts │ │ │ │ │ ├── tiupsServiceGetTiupsCluster.ts │ │ │ │ │ ├── tiupsServiceListTiups.ts │ │ │ │ │ ├── tiupsServiceUpdateTiups.ts │ │ │ │ │ ├── userServiceChangePassword.ts │ │ │ │ │ ├── userServiceCreateUser.ts │ │ │ │ │ ├── userServiceDeleteUser.ts │ │ │ │ │ ├── userServiceGetUser.ts │ │ │ │ │ ├── userServiceGetUserProfile.ts │ │ │ │ │ ├── userServiceListUserRoles.ts │ │ │ │ │ ├── userServiceListUsers.ts │ │ │ │ │ ├── userServiceLogin.ts │ │ │ │ │ ├── userServiceLogout.ts │ │ │ │ │ ├── userServiceResetPassword.ts │ │ │ │ │ ├── userServiceUpdateUser.ts │ │ │ │ │ └── userServiceValidateSession.ts │ │ │ │ ├── index.context.ts │ │ │ │ ├── index.schemas.ts │ │ │ │ ├── index.ts │ │ │ │ ├── index.validator.ts │ │ │ │ ├── index.zod.ts │ │ │ │ └── sample-res/ │ │ │ │ ├── metrics-config-cluster-overview.json │ │ │ │ ├── metrics-config-cluster.json │ │ │ │ ├── metrics-config-host.json │ │ │ │ ├── metrics-config-overview.json │ │ │ │ ├── metrics-data-cpu-usage.json │ │ │ │ ├── slow-query-detail.json │ │ │ │ ├── slow-query-list.json │ │ │ │ ├── statement-list.json │ │ │ │ ├── statement-plans-detail.json │ │ │ │ ├── statement-plans-list.json │ │ │ │ └── statement-slow-query-list.json │ │ │ ├── tsconfig.json │ │ │ └── wrangler.toml │ │ ├── libs/ │ │ │ ├── 1-charts/ │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── README.md │ │ │ │ ├── package.json │ │ │ │ ├── rollup.config.js │ │ │ │ ├── src/ │ │ │ │ │ ├── chart-theme-switch.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── sample-data.ts │ │ │ │ │ ├── series-chart.tsx │ │ │ │ │ ├── series-render.tsx │ │ │ │ │ ├── style.scss │ │ │ │ │ └── type.ts │ │ │ │ └── tsconfig.json │ │ │ ├── 1-icons/ │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── package.json │ │ │ │ ├── rollup.config.js │ │ │ │ ├── src/ │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── 1-utils/ │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── package.json │ │ │ │ ├── rollup.config.js │ │ │ │ ├── src/ │ │ │ │ │ ├── delay.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ ├── format.ts │ │ │ │ │ ├── i18n.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── memory-state/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── reset-filters-state.ts │ │ │ │ │ ├── prom.ts │ │ │ │ │ ├── time-range.ts │ │ │ │ │ └── url-state/ │ │ │ │ │ ├── advanced-filters-url-state.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── pagination-url-state.ts │ │ │ │ │ ├── pro-table-pagination-state.ts │ │ │ │ │ ├── pro-table-sort-state.ts │ │ │ │ │ ├── search-url-state.ts │ │ │ │ │ ├── sort-url-state.ts │ │ │ │ │ ├── timerange-url-state.ts │ │ │ │ │ └── use-url-state.tsx │ │ │ │ └── tsconfig.json │ │ │ ├── 2-primitive-ui/ │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── package.json │ │ │ │ ├── rollup.config.js │ │ │ │ ├── src/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── uikit-theme-provider.tsx │ │ │ │ └── tsconfig.json │ │ │ ├── 3-biz-ui/ │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── package.json │ │ │ │ ├── rollup.config.js │ │ │ │ ├── src/ │ │ │ │ │ ├── action-drawer.tsx │ │ │ │ │ ├── advanced-filters/ │ │ │ │ │ │ ├── filter-setting.tsx │ │ │ │ │ │ ├── filters-modal.tsx │ │ │ │ │ │ ├── filters-setting.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── locales.ts │ │ │ │ │ ├── auto-refresh-button/ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── progress.tsx │ │ │ │ │ ├── charts-multi-select.tsx │ │ │ │ │ ├── cols-multi-select.tsx │ │ │ │ │ ├── custom-json-view.tsx │ │ │ │ │ ├── filter-multi-select.tsx │ │ │ │ │ ├── highlight-sql.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── info-table.tsx │ │ │ │ │ ├── loading-skeleton.tsx │ │ │ │ │ ├── plan-table/ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── parser.ts │ │ │ │ │ │ └── sample-data/ │ │ │ │ │ │ ├── plan-v1-1.txt │ │ │ │ │ │ ├── plan-v1-2.txt │ │ │ │ │ │ ├── plan-v2-1.txt │ │ │ │ │ │ └── slow-query-detail.json │ │ │ │ │ ├── sql-with-hover.tsx │ │ │ │ │ ├── status-indicator.tsx │ │ │ │ │ └── time-range-picker/ │ │ │ │ │ ├── custom.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── tsconfig.json │ │ │ └── 4-apps/ │ │ │ ├── CHANGELOG.md │ │ │ ├── package.json │ │ │ ├── rollup.config.js │ │ │ ├── src/ │ │ │ │ ├── _re-exports/ │ │ │ │ │ ├── biz-ui.ts │ │ │ │ │ ├── charts-css.ts │ │ │ │ │ ├── charts.ts │ │ │ │ │ ├── primitive-ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── _shared/ │ │ │ │ │ ├── cols-factory.tsx │ │ │ │ │ ├── sql-history/ │ │ │ │ │ │ ├── components/ │ │ │ │ │ │ │ ├── chart.tsx │ │ │ │ │ │ │ ├── filters.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── time-range-clip-alert.tsx │ │ │ │ │ │ ├── ctx/ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── locales/ │ │ │ │ │ │ │ ├── en.json │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── zh.json │ │ │ │ │ │ ├── shared-state/ │ │ │ │ │ │ │ └── memory-state.ts │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ └── use-data.ts │ │ │ │ │ ├── sql-limit/ │ │ │ │ │ │ ├── components/ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── setting.tsx │ │ │ │ │ │ │ └── table.tsx │ │ │ │ │ │ ├── ctx/ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── locales/ │ │ │ │ │ │ │ ├── en.json │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── zh.json │ │ │ │ │ │ ├── shared-state/ │ │ │ │ │ │ │ └── memory-state.ts │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ └── use-data.ts │ │ │ │ │ └── state-filters.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── metric/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── chart-actions-menu.tsx │ │ │ │ │ │ ├── chart-body.tsx │ │ │ │ │ │ ├── chart-header.tsx │ │ │ │ │ │ ├── charts-select.tsx │ │ │ │ │ │ └── loading-card.tsx │ │ │ │ │ ├── ctx/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── locales/ │ │ │ │ │ │ ├── en.json │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zh.json │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── azores-cluster/ │ │ │ │ │ │ │ ├── filters.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── panel.tsx │ │ │ │ │ │ ├── azores-cluster-overview/ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── panel.tsx │ │ │ │ │ │ ├── azores-host/ │ │ │ │ │ │ │ ├── filters.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── panel.tsx │ │ │ │ │ │ ├── azores-metric-detail/ │ │ │ │ │ │ │ ├── body.tsx │ │ │ │ │ │ │ ├── drawer.tsx │ │ │ │ │ │ │ ├── filters.tsx │ │ │ │ │ │ │ └── modal.tsx │ │ │ │ │ │ ├── azores-overview/ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── panel.tsx │ │ │ │ │ │ └── normal/ │ │ │ │ │ │ ├── chart-card.tsx │ │ │ │ │ │ ├── filters.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── panel.tsx │ │ │ │ │ ├── shared-state/ │ │ │ │ │ │ ├── memory-state.ts │ │ │ │ │ │ └── url-state.ts │ │ │ │ │ └── utils/ │ │ │ │ │ ├── common.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── type.ts │ │ │ │ │ └── use-data.ts │ │ │ │ ├── slow-query/ │ │ │ │ │ ├── ctx/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── locales/ │ │ │ │ │ │ ├── en.json │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── preset.ts │ │ │ │ │ │ └── zh.json │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── advanced-filter-info-model.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── slowquery-model.ts │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ │ ├── detail-basic.tsx │ │ │ │ │ │ │ ├── detail-copr.tsx │ │ │ │ │ │ │ ├── detail-tabs.tsx │ │ │ │ │ │ │ ├── detail-time.tsx │ │ │ │ │ │ │ ├── detail-txn.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── plan.tsx │ │ │ │ │ │ │ ├── query.tsx │ │ │ │ │ │ │ ├── related-statement-button.tsx │ │ │ │ │ │ │ ├── related-statement-link.tsx │ │ │ │ │ │ │ ├── sql-history.tsx │ │ │ │ │ │ │ └── sql-limit.tsx │ │ │ │ │ │ └── list/ │ │ │ │ │ │ ├── advanced-filters-modal.tsx │ │ │ │ │ │ ├── cols-select.tsx │ │ │ │ │ │ ├── cols.tsx │ │ │ │ │ │ ├── filters-with-advanced.tsx │ │ │ │ │ │ ├── filters.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── refresh-button.tsx │ │ │ │ │ │ ├── table.tsx │ │ │ │ │ │ └── time-range-clip-alert.tsx │ │ │ │ │ ├── shared-state/ │ │ │ │ │ │ ├── detail-url-state.ts │ │ │ │ │ │ ├── list-url-state.ts │ │ │ │ │ │ └── memory-state.ts │ │ │ │ │ └── utils/ │ │ │ │ │ ├── constants.ts │ │ │ │ │ └── use-data.ts │ │ │ │ └── statement/ │ │ │ │ ├── ctx/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── locales/ │ │ │ │ │ ├── en.json │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── preset.ts │ │ │ │ │ └── zh.json │ │ │ │ ├── models/ │ │ │ │ │ ├── advanced-filter-info-model.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── statement-config-model.ts │ │ │ │ │ └── statement-model.ts │ │ │ │ ├── pages/ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── plan-detail/ │ │ │ │ │ │ │ ├── detail-basic.tsx │ │ │ │ │ │ │ ├── detail-copr.tsx │ │ │ │ │ │ │ ├── detail-tabs.tsx │ │ │ │ │ │ │ ├── detail-time.tsx │ │ │ │ │ │ │ ├── detail-txn.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── plan.tsx │ │ │ │ │ │ ├── plans-list/ │ │ │ │ │ │ │ ├── bind-sql-cell.tsx │ │ │ │ │ │ │ ├── cols.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── plan-check-cell.tsx │ │ │ │ │ │ │ ├── slow-query-cell.tsx │ │ │ │ │ │ │ └── table.tsx │ │ │ │ │ │ ├── sql-history.tsx │ │ │ │ │ │ ├── sql-limit.tsx │ │ │ │ │ │ ├── stmt-basic.tsx │ │ │ │ │ │ └── stmt-sql.tsx │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── advanced-filters-modal.tsx │ │ │ │ │ │ ├── cols-select.tsx │ │ │ │ │ │ ├── cols.tsx │ │ │ │ │ │ ├── disabled-status.tsx │ │ │ │ │ │ ├── filters-with-advanced.tsx │ │ │ │ │ │ ├── filters.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── refresh-button.tsx │ │ │ │ │ │ ├── stmt-setting-button.tsx │ │ │ │ │ │ ├── table.tsx │ │ │ │ │ │ └── time-range-fix-alert.tsx │ │ │ │ │ └── setting/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── shared-state/ │ │ │ │ │ ├── detail-url-state.ts │ │ │ │ │ ├── list-url-state.ts │ │ │ │ │ └── memory-state.ts │ │ │ │ └── utils/ │ │ │ │ ├── constants.ts │ │ │ │ └── use-data.ts │ │ │ └── tsconfig.json │ │ ├── portals/ │ │ │ └── test/ │ │ │ ├── CHANGELOG.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── public/ │ │ │ │ └── swagger.index-advisor.json │ │ │ ├── src/ │ │ │ │ ├── App.css │ │ │ │ ├── App.tsx │ │ │ │ ├── apps/ │ │ │ │ │ ├── metric/ │ │ │ │ │ │ ├── mock-api-app-provider.tsx │ │ │ │ │ │ └── sample-data/ │ │ │ │ │ │ ├── normal-configs.ts │ │ │ │ │ │ └── qps-type.json │ │ │ │ │ ├── slow-query/ │ │ │ │ │ │ └── mock-api-app-provider.tsx │ │ │ │ │ └── statement/ │ │ │ │ │ ├── mock-api-app-provider.tsx │ │ │ │ │ └── stmt-types.ts │ │ │ │ ├── index.css │ │ │ │ ├── main.tsx │ │ │ │ ├── providers/ │ │ │ │ │ ├── react-query-provider.tsx │ │ │ │ │ └── url-state-provider.tsx │ │ │ │ ├── router/ │ │ │ │ │ ├── devtools.tsx │ │ │ │ │ ├── provider.tsx │ │ │ │ │ └── router.ts │ │ │ │ ├── routes/ │ │ │ │ │ ├── __root.tsx │ │ │ │ │ ├── _apps-layout/ │ │ │ │ │ │ ├── metrics/ │ │ │ │ │ │ │ ├── azores-cluster-overview.lazy.tsx │ │ │ │ │ │ │ ├── azores-cluster.lazy.tsx │ │ │ │ │ │ │ ├── azores-host.lazy.tsx │ │ │ │ │ │ │ ├── azores-overview.lazy.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── normal.lazy.tsx │ │ │ │ │ │ ├── metrics.lazy.tsx │ │ │ │ │ │ ├── slow-query/ │ │ │ │ │ │ │ ├── detail.lazy.tsx │ │ │ │ │ │ │ └── index.lazy.tsx │ │ │ │ │ │ ├── slow-query.lazy.tsx │ │ │ │ │ │ ├── statement/ │ │ │ │ │ │ │ ├── detail.lazy.tsx │ │ │ │ │ │ │ └── index.lazy.tsx │ │ │ │ │ │ └── statement.lazy.tsx │ │ │ │ │ ├── _apps-layout.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── login.lazy.tsx │ │ │ │ └── vite-env.d.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ └── tsconfig.app.json │ ├── pnpm-workspace.yaml │ └── scripts/ │ ├── gen-locales.ts │ └── gogocode-test/ │ ├── 1.js │ └── 2.js └── util/ ├── assertutil/ │ ├── 1_main_test.go │ ├── json.go │ ├── json_test.go │ └── mock.go ├── client/ │ ├── httpclient/ │ │ ├── 1_main_test.go │ │ ├── client.go │ │ ├── config.go │ │ ├── info.go │ │ ├── request.go │ │ ├── request_resty.go │ │ ├── response.go │ │ └── response_test.go │ ├── pdclient/ │ │ ├── 1_main_test.go │ │ ├── etcd_client.go │ │ ├── fixture/ │ │ │ └── pd_server.go │ │ ├── pd_api.go │ │ ├── pd_api_client.go │ │ ├── pd_api_highlevel.go │ │ ├── pd_api_highlevel_test.go │ │ └── pd_api_test.go │ ├── schedulingclient/ │ │ └── status_client.go │ ├── ticdcclient/ │ │ └── status_client.go │ ├── tidbclient/ │ │ ├── sql_client.go │ │ ├── status_client.go │ │ ├── tidbproto/ │ │ │ ├── 1_main_test.go │ │ │ ├── codec.go │ │ │ ├── codec_test.go │ │ │ └── model.go │ │ └── tidbproxy/ │ │ └── proxy.go │ ├── tiflashclient/ │ │ └── status_client.go │ ├── tikvclient/ │ │ └── status_client.go │ ├── tiproxyclient/ │ │ └── status_client.go │ └── tsoclient/ │ └── status_client.go ├── csvutil/ │ ├── 1_main_test.go │ ├── writer.go │ └── writer_test.go ├── distro/ │ ├── 1_main_test.go │ ├── distro.go │ └── distro_test.go ├── featureflag/ │ ├── 1_main_test.go │ ├── featureflag.go │ ├── featureflag_test.go │ ├── registry.go │ └── registry_test.go ├── fxprinter/ │ └── fxprinter.go ├── gormutil/ │ ├── datatype/ │ │ ├── 1_main_test.go │ │ ├── int.go │ │ ├── int_test.go │ │ ├── int_withdb_test.go │ │ ├── nullable/ │ │ │ └── nullable.go │ │ ├── timestamp.go │ │ ├── timestamp_test.go │ │ └── timestamp_withdb_test.go │ └── virtualview/ │ ├── 1_main_test.go │ ├── schema.go │ ├── schema_test.go │ ├── virtualview.go │ └── virtualview_test.go ├── israce/ │ ├── no_race.go │ └── race.go ├── jsonserde/ │ ├── 1_main_test.go │ ├── convert.go │ ├── default.go │ ├── default_test.go │ └── ginadapter/ │ ├── 1_main_test.go │ ├── binding.go │ ├── binding_test.go │ ├── render.go │ └── render_test.go ├── lifecyclectx/ │ ├── fx.go │ └── lifecyclectxtest/ │ └── test.go ├── netutil/ │ ├── 1_main_test.go │ ├── parse.go │ └── parse_test.go ├── nocopy/ │ └── nocopy.go ├── proxy/ │ ├── 1_main_test.go │ ├── proxy.go │ ├── proxy_test.go │ └── upstream.go ├── reflectutil/ │ ├── 1_main_test.go │ ├── field.go │ ├── field_test.go │ ├── tag.go │ └── tag_test.go ├── rest/ │ ├── 1_main_test.go │ ├── context_helpers.go │ ├── context_helpers_test.go │ ├── empty_resp.go │ ├── error.go │ ├── error_resp.go │ ├── error_resp_test.go │ ├── error_test.go │ └── fileswap/ │ ├── 1_main_test.go │ ├── server.go │ └── server_test.go ├── sqlitestore/ │ └── sqlitestore.go ├── testutil/ │ ├── db.go │ ├── gintest/ │ │ └── context.go │ ├── http_server.go │ ├── httpmockutil/ │ │ └── responder.go │ └── testdefault/ │ └── main.go ├── timeutil/ │ ├── 1_main_test.go │ ├── format.go │ └── format_test.go ├── tlsutil/ │ ├── 1_main_test.go │ ├── config_ext.go │ └── config_ext_test.go ├── topo/ │ ├── 1_main_test.go │ ├── mock_TopologyProvider.go │ ├── model.go │ ├── model_desc.go │ ├── model_info.go │ ├── pdtopo/ │ │ ├── 1_main_test.go │ │ ├── monitor.go │ │ ├── pd.go │ │ ├── pd_test.go │ │ ├── provider_pd.go │ │ ├── std_comp.go │ │ ├── store.go │ │ ├── store_test.go │ │ └── tidb.go │ ├── provider.go │ ├── provider_cached.go │ └── provider_cached_test.go └── ziputil/ └── zip_writer.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "files": [ "README.md" ], "imageSize": 50, "commit": false, "contributors": [ { "login": "Fullstop000", "name": "Fullstop000", "avatar_url": "https://avatars1.githubusercontent.com/u/12471960?v=4", "profile": "https://github.com/Fullstop000", "contributions": [ "code" ] }, { "login": "rleungx", "name": "Ryan Leung", "avatar_url": "https://avatars3.githubusercontent.com/u/35896542?v=4", "profile": "http://rleungx.github.io", "contributions": [ "code" ] }, { "login": "zzh-wisdom", "name": "zzh-wisdom", "avatar_url": "https://avatars2.githubusercontent.com/u/52516344?v=4", "profile": "https://github.com/zzh-wisdom", "contributions": [ "code" ] }, { "login": "STRRL", "name": "STRRL", "avatar_url": "https://avatars0.githubusercontent.com/u/20221408?v=4", "profile": "https://github.com/STRRL", "contributions": [ "code" ] }, { "login": "SSebo", "name": "SSebo", "avatar_url": "https://avatars0.githubusercontent.com/u/5784607?v=4", "profile": "https://github.com/SSebo", "contributions": [ "code" ] }, { "login": "Yisaer", "name": "Song Gao", "avatar_url": "https://avatars1.githubusercontent.com/u/13427348?v=4", "profile": "https://yisaer.github.io/", "contributions": [ "code" ] }, { "login": "gauss1314", "name": "gauss1314", "avatar_url": "https://avatars2.githubusercontent.com/u/3862518?v=4", "profile": "https://github.com/gauss1314", "contributions": [ "code" ] }, { "login": "leiysky", "name": "lei yu", "avatar_url": "https://avatars2.githubusercontent.com/u/22445410?v=4", "profile": "https://github.com/leiysky", "contributions": [ "code" ] }, { "login": "niedhui", "name": "niedhui", "avatar_url": "https://avatars0.githubusercontent.com/u/66329?v=4", "profile": "https://github.com/niedhui", "contributions": [ "code" ] }, { "login": "weihanglo", "name": "Weihang Lo", "avatar_url": "https://avatars2.githubusercontent.com/u/14314532?v=4", "profile": "https://weihanglo.tw/", "contributions": [ "code" ] }, { "login": "yikeke", "name": "Keke Yi", "avatar_url": "https://avatars1.githubusercontent.com/u/40977455?v=4", "profile": "https://github.com/yikeke", "contributions": [ "content" ] }, { "login": "qxhy123", "name": "Michael.Yang", "avatar_url": "https://avatars2.githubusercontent.com/u/518969?v=4", "profile": "https://github.com/qxhy123", "contributions": [ "code" ] }, { "login": "Rustin-Liu", "name": "二手掉包工程师", "avatar_url": "https://avatars0.githubusercontent.com/u/29879298?v=4", "profile": "http://www.rustin.cn", "contributions": [ "code" ] }, { "login": "ericsyh", "name": "Eric Shen", "avatar_url": "https://avatars3.githubusercontent.com/u/10498732?v=4", "profile": "https://github.com/ericsyh", "contributions": [ "code" ] } ], "contributorsPerLine": 10, "contributorsSortAlphabetically": true, "contributorTemplate": "\">\" width=\"<%= options.imageSize %>px;\" alt=\"\"/>", "projectName": "tidb-dashboard", "projectOwner": "pingcap", "repoType": "github", "repoHost": "https://github.com", "skipCi": true, "commitConvention": "none" } ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: "\U0001F41B Bug Report" about: Something isn't working as expected title: "" labels: type/bug assignees: "" --- ## Bug Report Please answer these questions before submitting your issue. Thanks! **What did you do?** **What did you expect to see?** **What did you see instead?** **What version of TiDB Dashboard are you using (`./tidb-dashboard --version`)?** ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: "\U0001F680 Feature Request" about: I have a suggestion title: "" labels: status/discussion, type/feature-request assignees: "" --- ## Feature Request **Is your feature request related to a problem? Please describe:** **Describe the feature you'd like:** **Describe alternatives you've considered:** **Teachability, Documentation, Adoption, Migration Strategy:** ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: "\U0001F914 Question" about: I have a question label: "type/question" --- ## Question ================================================ FILE: .github/autolabeler.yml ================================================ area/frontend: - /ui area/backend: - *.go ================================================ FILE: .github/codecov.yml ================================================ comment: layout: "diff,flags,footer" ================================================ FILE: .github/stale.yml ================================================ daysUntilStale: 60 daysUntilClose: 7 exemptLabels: - type/feature-request - type/pinned staleLabel: type/stale ================================================ FILE: .github/workflows/build.yaml ================================================ name: Build on: push: branches: - master pull_request: branches: - master - release-* jobs: backend: name: Backend runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: "go.mod" - name: Load go module cache uses: actions/cache@v5 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Load golangci-lint cache uses: actions/cache@v5 with: path: ~/.cache/golangci-lint key: ${{ runner.os }}-golint restore-keys: | ${{ runner.os }}-golint - name: Lint and build run: | make dev - name: Check uncommitted lint changes run: | git diff --exit-code frontend: name: Frontend runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 # https://pnpm.io/continuous-integration#github-actions - name: Setup PNPM uses: pnpm/action-setup@v5 with: version: 8 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "22" cache: "pnpm" cache-dependency-path: "ui/pnpm-lock.yaml" - uses: actions/setup-go@v6 with: go-version-file: "go.mod" - name: Load go module cache uses: actions/cache@v5 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Install ui packages run: | make ui_deps - name: Check format run: | pnpm fmt-check || (echo "::error ::Please format your code by using 'pnpm fmt-fix'"; exit 1) working-directory: ui - name: Build UI run: | make ui env: NO_MINIMIZE: true CI: true ================================================ FILE: .github/workflows/manual-create-pd-pr.yaml ================================================ name: Create PD PR Manually on: workflow_dispatch: inputs: release_version: description: 'Release version, e.g. v7.6.0-f7bbcdcf' required: true pd_branchs: description: 'PD branch, e.g. ["master", "release-7.6"]' default: '["master"]' required: true jobs: pd_pr: runs-on: ubuntu-latest strategy: fail-fast: false matrix: # https://stackoverflow.com/questions/69781005/combine-dynamic-github-workflow-matrix-with-input-values-and-predefined-values branch: ${{ fromJson(github.event.inputs.pd_branchs) }} name: Create PD PR - ${{ matrix.branch }} steps: - name: Check out PD code base uses: actions/checkout@v4 with: repository: tikv/pd ref: ${{ matrix.branch }} - uses: actions/setup-go@v3 with: go-version: '1.25.7' - name: Load go module cache uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-pd-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go-pd- - name: Update TiDB Dashboard in PD code base run: | scripts/update-dashboard.sh ${{ github.event.inputs.release_version }} - name: Commit PD code base change id: git_commit run: | git diff git config user.name "baurine" git config user.email "2008.hbl@gmail.com" git add . if git status | grep -q "Changes to be committed" then git commit --signoff --message "chore(dashboard): update TiDB Dashboard to ${{ github.event.inputs.release_version }}" echo "::set-output name=committed::1" else echo "No changes detected, skipped" fi - name: Set build ID id: build_id run: echo "::set-output name=id::$(date +%s)" - name: Create PR based on PD code base id: cpr uses: peter-evans/create-pull-request@v3 if: steps.git_commit.outputs.committed == 1 with: push-to-fork: baurine/pd token: ${{ secrets.PAT_TO_PUSH_PD_FORK }} branch: update-tidb-dashboard/${{ matrix.branch }}-${{ github.event.inputs.release_version }}-${{ steps.build_id.outputs.id }} title: 'chore(dashboard): update TiDB Dashboard to ${{ github.event.inputs.release_version }} [${{ matrix.branch }}]' body: | ### What problem does this PR solve? Issue Number: ref #4257 Update TiDB Dashboard to [${{ github.event.inputs.release_version }}](https://github.com/pingcap/tidb-dashboard/releases/tag/${{ github.event.inputs.release_version }}). ### Release note ```release-note None ``` - name: Check outputs if: steps.git_commit.outputs.committed == 1 run: | echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: push: tags: - "v*" - "!v*-alpha" jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 # https://pnpm.io/continuous-integration#github-actions - name: Setup PNPM uses: pnpm/action-setup@v2 with: version: 8 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "22" cache: "pnpm" cache-dependency-path: "ui/pnpm-lock.yaml" - uses: actions/setup-go@v3 with: go-version: "1.25.7" - name: Load go module cache uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Build UI env: REACT_APP_MIXPANEL_TOKEN: ${{ secrets.REACT_APP_MIXPANEL_TOKEN }} run: | make ui - name: Pack UI assets for release working-directory: ui/packages/tidb-dashboard-for-op/dist run: | zip -r ../static-assets.zip . # TODO: generate changelog # - name: Generate Changelog # id: build_changelog # uses: mikepenz/release-changelog-builder-action@v4.1.0 - name: Create release id: create_release uses: fleskesvor/create-release@feature/support-target-commitish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: Internal Version ${{ github.ref }} draft: false prerelease: false # body: ${{steps.build_changelog.outputs.changelog}} - name: Upload UI assets uses: actions/upload-release-asset@v1.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./ui/packages/tidb-dashboard-for-op/static-assets.zip asset_name: static-assets.zip asset_content_type: application/zip - name: Generate embedded UI assets run: | NO_ASSET_BUILD_TAG=1 scripts/embed_ui_assets.sh cp pkg/uiserver/embedded_assets_handler.go embedded_assets_handler.go - name: Pack embedded assets for release run: | zip -r embedded-assets-golang.zip ./embedded_assets_handler.go - name: Upload embedded UI assets uses: actions/upload-release-asset@v1.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./embedded-assets-golang.zip asset_name: embedded-assets-golang.zip asset_content_type: application/zip ================================================ FILE: .github/workflows/test-docker-image.yaml ================================================ name: Test Docker Image on: push: branches: - master pull_request: branches: - master concurrency: group: ${{ github.ref }}-${{ github.workflow }} cancel-in-progress: true jobs: test-build-docker-image: runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} strategy: matrix: platform: [linux/amd64, linux/arm64] remote_dockerfile: # download from: https://github.com/PingCAP-QE/artifacts/tree/main/dockerfiles/cd/builders/tidb-dashboard - Dockerfile # builder is rocky linux 8 - centos7/Dockerfile # builder is centos 7 fail-fast: true steps: - name: Checkout code uses: actions/checkout@v4 - name: Read VERSION file id: getversion run: echo "::set-output name=version::$(git describe --tags --dirty --always)" - name: Download Dockerfile run: | wget "https://github.com/PingCAP-QE/artifacts/raw/refs/heads/main/dockerfiles/cd/builders/tidb-dashboard/${{ matrix.remote_dockerfile }}" -O Dockerfile - name: Build uses: docker/build-push-action@v6 with: push: false context: . file: Dockerfile platforms: ${{ matrix.platform }} tags: pingcap/tidb-dashboard:${{ steps.getversion.outputs.version }} ================================================ FILE: .github/workflows/test.yaml ================================================ name: Test on: push: branches: - master pull_request: branches: - master - release-* jobs: backend_ut: name: Backend - Unit runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout code uses: actions/checkout@v4 - uses: actions/setup-go@v3 with: go-version: "1.25.7" - name: Load go module cache uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Run unit test run: | make unit_test # - name: Upload coverage to Codecov # uses: codecov/codecov-action@v2 # with: # files: ./coverage/unit_test.txt # fail_ci_if_error: true # flags: backend_ut # verbose: true backend_integration: runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: # tidb_version: [nightly, ^6.0, ^5.4, ^5.0] tidb_version: [nightly, ^6.0] name: Backend - Integration - TiDB@${{ matrix.tidb_version }} steps: - name: Checkout code uses: actions/checkout@v4 - uses: actions/setup-go@v3 with: go-version: "1.25.7" - name: Load go module cache uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Load TiUP cache uses: actions/cache@v3 with: path: ~/.tiup/components key: ${{ runner.os }}-tiup-${{ matrix.tidb_version }} - name: Run integration test run: | make integration_test TIDB_VERSION=${{ matrix.tidb_version }} # - name: Upload coverage to Codecov # uses: codecov/codecov-action@v2 # with: # files: ./coverage/integration_${{ matrix.tidb_version }}.txt # fail_ci_if_error: true # flags: backend_integration # verbose: true # e2e_test: # runs-on: ubuntu-latest # timeout-minutes: 30 # strategy: # fail-fast: false # matrix: # # test latest features and compatibility of lower version # include: # - feature_version: 6.0.0 # You must ensure feature_version and tidb_version is matching! # tidb_version: nightly # without_ngm: true # - feature_version: 6.0.0 # tidb_version: ^6.0 # without_ngm: true # - feature_version: 5.4.0 # tidb_version: ^5.4 # without_ngm: true # - feature_version: 5.0.0 # tidb_version: ^5.0 # without_ngm: true # name: E2E - TiDB@${{ matrix.tidb_version }}${{ !matrix.without_ngm && '+ngm' || '' }} # steps: # - name: Checkout code # uses: actions/checkout@v4 # # https://pnpm.io/continuous-integration#github-actions # - name: Setup PNPM # uses: pnpm/action-setup@v2.2.2 # with: # version: 7 # - name: Setup Node.js # uses: actions/setup-node@v4 # with: # node-version: "16" # cache: "pnpm" # cache-dependency-path: "ui/pnpm-lock.yaml" # - name: Load cypress cache # uses: actions/cache@v3 # id: cypress-cache # with: # path: ~/.cache/Cypress # key: ${{ runner.os }}-cypress-${{ hashFiles('**/pnpm-lock.yaml') }} # restore-keys: | # ${{ runner.os }}-cypress- # - uses: actions/setup-go@v3 # with: # go-version: "1.21" # - name: Load go module cache # uses: actions/cache@v3 # with: # path: | # ~/.cache/go-build # ~/go/pkg/mod # key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} # restore-keys: | # ${{ runner.os }}-go- # - name: Load TiUP cache # uses: actions/cache@v3 # with: # path: ~/.tiup/components # key: ${{ runner.os }}-tiup-${{ matrix.tidb_version }} # - name: Install and run TiUP in the background # run: | # chmod u+x scripts/start_tiup.sh # scripts/start_tiup.sh ${{ matrix.tidb_version }} ${{ matrix.without_ngm }} # - name: Build UI # run: | # make ui # env: # NO_MINIMIZE: true # CI: true # E2E_TEST: true # - name: Wait TiUP Playground # run: | # chmod u+x scripts/wait_tiup_playground.sh # scripts/wait_tiup_playground.sh 15 20 # - name: Debug TiUP # run: | # source /home/runner/.profile # tiup --version # ls /home/runner/.tiup/components/playground/ # DATA_PATH=$(ls /home/runner/.tiup/data/) # echo $DATA_PATH # echo "==== TiDB Log ====" # head -n 3 /home/runner/.tiup/data/$DATA_PATH/tidb-0/tidb.log # echo "==== TiKV Log ====" # head -n 3 /home/runner/.tiup/data/$DATA_PATH/tikv-0/tikv.log # echo "==== PD Log ====" # head -n 3 /home/runner/.tiup/data/$DATA_PATH/pd-0/pd.log # - name: Build and run backend in the background # run: | # make # make run & # env: # UI: 1 # FEATURE_VERSION: ${{ matrix.feature_version }} # - name: Run E2E Features Test # run: make e2e_test # env: # SERVER_URL: http://127.0.0.1:12333/dashboard/ # CI: true # FEATURE_VERSION: ${{ matrix.feature_version }} # TIDB_VERSION: ${{ matrix.tidb_version }} # CYPRESS_ALLOW_SCREENSHOT: true # WITHOUT_NGM: ${{ matrix.without_ngm }} # - name: Upload coverage to Codecov # uses: codecov/codecov-action@v2 # with: # files: ./ui/packages/tidb-dashboard-for-op/.nyc_output/out.json # fail_ci_if_error: false # flags: e2e_test # verbose: true ================================================ FILE: .github/workflows/upload-e2e-snapshots.yaml ================================================ # Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. name: Upload E2E Snapshots on: workflow_dispatch: inputs: ref: description: "The branch, tag or SHA to create snapshots" required: true spec: description: "Specify the spec files to run, example: `topsql/topsql.spec.ts`" required: true jobs: e2e_test_snapshots: name: Take E2E Test Snapshots runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false matrix: # test latest features and compatibility of lower version include: - feature_version: 6.0.0 tidb_version: latest - feature_version: 5.4.0 tidb_version: v5.4.0 - feature_version: 5.0.0 tidb_version: v5.0.0 steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.ref }} # https://pnpm.io/continuous-integration#github-actions - name: Setup PNPM uses: pnpm/action-setup@v2 with: version: 8 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "22" cache: "pnpm" cache-dependency-path: "ui/pnpm-lock.yaml" - uses: actions/setup-go@v3 with: go-version: "1.25.7" - name: Load go module cache uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Load TiUP cache uses: actions/cache@v3 with: path: ~/.tiup/components key: ${{ runner.os }}-tiup restore-keys: | ${{ runner.os }}-tiup - name: Install and run TiUP in the background run: | chmod u+x scripts/start_tiup.sh scripts/start_tiup.sh ${{ matrix.tidb_version }} false - name: Build UI run: | make ui env: NO_MINIMIZE: true CI: true - name: Wait TiUP Playground run: | chmod u+x scripts/wait_tiup_playground.sh scripts/wait_tiup_playground.sh 15 20 - name: Debug TiUP run: | source /home/runner/.profile tiup --version ls /home/runner/.tiup/components/playground/ DATA_PATH=$(ls /home/runner/.tiup/data/) echo $DATA_PATH echo "==== TiDB Log ====" head -n 3 /home/runner/.tiup/data/$DATA_PATH/tidb-0/tidb.log echo "==== TiKV Log ====" head -n 3 /home/runner/.tiup/data/$DATA_PATH/tikv-0/tikv.log echo "==== PD Log ====" head -n 3 /home/runner/.tiup/data/$DATA_PATH/pd-0/pd.log - name: Build and run backend in the background run: | make make run & env: UI: 1 FEATURE_VERSION: ${{ matrix.feature_version }} - name: Delete Previous Snapshots run: rm -rf ${{ github.workspace }}/ui/packages/tidb-dashboard-for-op/cypress/snapshots - name: Run E2E Features Test run: make e2e_test_specify env: SERVER_URL: http://127.0.0.1:12333/dashboard/ CI: true FEATURE_VERSION: ${{ matrix.feature_version }} TIDB_VERSION: ${{ matrix.tidb_version }} CYPRESS_ALLOW_SCREENSHOT: true E2E_SPEC: cypress/integration/${{ github.event.inputs.spec }} - name: Archive Test Video if: always() uses: actions/upload-artifact@v2 with: name: e2e-video-${{ matrix.feature_version }} path: ${{ github.workspace }}/ui/packages/tidb-dashboard-for-op/cypress/videos/**/* - name: Upload snapshots artifact uses: actions/upload-artifact@v2 if: always() with: name: e2e-snapshots-${{ matrix.feature_version }} path: ${{ github.workspace }}/ui/packages/tidb-dashboard-for-op/cypress/snapshots/**/* ================================================ FILE: .gitignore ================================================ .env /bin # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out coverage/ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 .idea # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # VSCode .vscode/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser # ui .pnpm-debug.log ================================================ FILE: .golangci.yml ================================================ version: "2" linters: enable: - asciicheck - dogsled - durationcheck - errorlint - exhaustive - godot - goheader - gosec - importas - misspell - nestif - prealloc - predeclared - revive - unconvert - whitespace settings: exhaustive: default-signifies-exhaustive: true godot: exclude: - ^\s*@ goheader: template: Copyright {{ YEAR }} PingCAP, Inc. Licensed under Apache-2.0. revive: rules: - name: var-naming severity: warning disabled: false exclude: [""] arguments: - [ "utils" ] # AllowList - [ "VM" ] # DenyList - - skip-package-name-checks: true gosec: excludes: - G115 # Type conversion which leads to integer overflow exclusions: generated: lax presets: - comments - std-error-handling rules: - path: ^pkg/|^cmd/ linters: - errorlint - exhaustive - nestif - wastedassign - path: _test.go linters: - gosec test: "G705" paths: - swaggerspec - pkg/uiserver - ui - third_party$ - builtin$ - examples$ formatters: enable: - gofumpt - goimports settings: goimports: local-prefixes: - github.com/pingcap/tidb-dashboard exclusions: generated: lax paths: - swaggerspec - pkg/uiserver - ui - third_party$ - builtin$ - examples$ ================================================ FILE: .prettierrc ================================================ { "semi": false, "trailingComma": "none", "singleQuote": true } ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to TiDB Dashboard Thanks for your interest in contributing to TiDB Dashboard! This document outlines some of the conventions on building, running, and testing TiDB Dashboard, the development workflow, commit message formatting, contact points and other resources. If you need any help (for example, mentoring getting started or understanding the codebase), feel free to reach out on the [TiDB Internals forum](https://internals.tidb.io/). ## Setting up a development workspace The following steps are describing how to develop TiDB Dashboard by running a self-built and standalone TiDB Dashboard server along with a separated TiDB cluster ([TiDB] + [TiKV] + [PD]). TiDB Dashboard cannot work without a TiDB cluster. Although TiDB Dashboard can also be integrated into [PD], this form is not convenient for developing. Thus we will not cover it here. ### Step 1. Start a TiDB cluster [TiUP](https://docs.pingcap.com/tidb/stable/tiup-overview) is the official component manager for [TiDB]. It can help you set up a local TiDB cluster in a few minutes. Download and install TiUP: ```bash curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh ``` Declare the global environment variable: > **Note:** > > After the installation, TiUP displays the absolute path of the corresponding `profile` file. You need to modify the following `source` command according to the path. ```bash source ~/.bash_profile ``` Start a local TiDB cluster: ```bash tiup playground ``` You might notice that there is already a TiDB Dashboard integrated into the PD started by TiUP. For development purpose, it will not be used intentionally. ### Step 2. Prepare Dev Prerequisites The followings are required for developing TiDB Dashboard: - git - Version control - make - Build tool (run common workflows) - [Golang 1.21+](https://golang.org/) - To compile the server. - [Node.js 22](https://nodejs.org/) - To compile the front-end. - [PNPM 8](https://pnpm.io/) - To manage front-end dependencies. - [Java 8+](https://www.java.com/ES/download/) - To generate JavaScript API client by OpenAPI specification. ### Step 3. Build and Run TiDB Dashboard > Make sure that `tiup playground` is running on the background. Package frontend and backend into a single binary: ```bash # Build a binary into `bin/tidb-dashboard`. make package # Run. make run ``` You can access TiDB Dashboard now: [http://127.0.0.1:12333/dashboard](http://127.0.0.1:12333/dashboard) #### Develop Frontend and Backend Separately 1. Build and run TiDB Dashboard back-end server: ```bash # In tidb-dashboard directory: make dev && make run ``` 2. Build and run front-end server in a new terminal: ```bash # In tidb-dashboard directory: cd ui pnpm i # install all dependencies pnpm dev ``` 3. That's it! You can access TiDB Dashboard now: [http://127.0.0.1:3001](http://127.0.0.1:3001) ### Step 4. Run E2E Tests (optional) When back-end server and front-end server are both started, E2E tests can be run by: ```bash cd ui/packages/tidb-dashboard-for-op pnpm open:cypress ``` > Now we have only a few e2e tests. Contributions are welcome! ## Additional Guides ### Style Guidelines This project follows the [Uber's Golang style guide](https://github.com/uber-go/guide/blob/master/style.md). Please refer to [its documentation](https://github.com/uber-go/guide/blob/master/style.md) for details. ### Swagger UI We use [Swagger] to generate the API server and corresponding clients. Swagger provides a web UI in which you can see all TiDB Dashboard API endpoints and specifications, or even send API requests. Swagger UI is available at http://localhost:12333/dashboard/api/swagger after the above Step 3 is finished. ### Build and run docker image locally If you want to develop docker image locally 🤔. 1. Ensure the Docker Buildx is installed on your local machine. > Docker Buildx is included in Docker Desktop for Windows, macOS, and Linux. > Docker Linux packages also include Docker Buildx when installed using the DEB or RPM packages. 2. Build the docker image. ```bash # On repository root directory (only build locally, no push remote), run: make docker-build-image-locally-amd64 # Or, if you want to build the image for arm64 platform (only build locally, no push remote), run: make docker-build-image-locally-arm64 # Or, if you want to build cross-platform image and push it to your dev docker registry, run: REPOSITORY=your-tidb-dashboard-repository make docker-build-and-push-image # Or, if you want to build centos7 based image, run: DOCKERFILE=./dockerfiles/centos7.Dockerfile make docker-build-image-locally-arm64 # Finally, if you update npm modules or go modules, and want to disable docker layer cache to force rebuild, set NO_CACHE="--pull --no-cache" before make command. For example: NO_CACHE="--pull --no-cache" make docker-build-image-locally-amd64 ``` 3. Run newly build image with docker-compose. > Please make sure that `tiup playground` is not running on the background. ```bash # On repository root directory, run: docker-compose -f ./dockerfiles/docker-compose.yml up ``` 4. Access TiDB Dashboard at [http://localhost:12333/dashboard](http://localhost:12333/dashboard). > The old Dashboard **_in PD_** can be accessed at [http://localhost:2379/dashboard](http://localhost:2379/dashboard). ### How to update TiDB Dashboard in PD To update the TiDB Dashboard in PD, we need to release a TiDB Dashboard version and submit a PR to PD. 1. In a release branch, likes `release-7.6`, run `make tag` to add a new tag for TiDB Dashboard. The new tag is like `v7.6.x-`. 1. Push the new tag to the remote repository, it will trigger the CI to release a new TiDB Dashboard version. 1. After the new version is created, go to https://github.com/pingcap/tidb-dashboard/actions/workflows/manual-create-pd-pr.yaml, click `Run workflow` button, fill the required parameters and submit the workflow, it will trigger the CI to create a PR to PD which will update the TiDB Dashboard version in PD. ## Contribution flow This is a rough outline of what a contributor's workflow looks like: - Create a Git branch from where you want to base your work. This is usually master. - Write code, add test cases, and commit your work (see below for message format). - Run lints and / or formatters. - Backend: ```bash # In tidb-dashboard directory: make dev ``` - Frontend: ```bash # In ui directory: pnpm fmt-check ``` > Recommended to install [Prettier plugin](https://prettier.io/docs/en/editors.html) for your editor so that there will be auto format on save. - Run tests and make sure all tests pass. - Push your changes to a branch in your fork of the repository and submit a pull request. - Your PR will be reviewed by two maintainers, who may request some changes. - Once you've made changes, your PR must be re-reviewed and approved. - If the PR becomes out of date, you can use GitHub's 'update branch' button. - If there are conflicts, you can rebase (or merge) and resolve them locally. Then force push to your PR branch. You do not need to get re-review just for resolving conflicts, but you should request re-review if there are significant changes. - Our CI system automatically tests all pull requests. - If all tests passed and got an approval, reviewers will merge your PR. Thanks for your contributions! ### Finding something to work on For beginners, we have prepared many suitable tasks for you. Checkout our [help wanted issues](https://github.com/pingcap/tidb-dashboard/issues?q=is%3Aopen+label%3Astatus%2Fhelp-wanted+sort%3Aupdated-desc) for a list, in which we have also marked the difficulty level. If you are planning something big, for example, relates to multiple components or changes current behaviors, make sure to open an issue to discuss with us before going on. ### Format of the commit message We follow a rough convention for commit messages that is designed to answer two questions: what changed and why. The subject line should feature the what and the body of the commit should describe the why. ```plain cluster: add comment for variable declaration. Improve documentation. ``` The format can be described more formally as follows: ```plain : ``` If the change affects more than one subsystem, you can use comma to separate them like `keyviz, cluster: foo`. If the change affects many subsystems, you can use `*` instead, like `*: foo`. The body of the commit message should describe why the change was made and at a high level, how the code works. [pd]: https://github.com/pingcap/pd [tidb]: https://github.com/pingcap/tidb [tikv]: https://github.com/tikv/tikv [tiup]: https://tiup.io [swagger]: https://swagger.io ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ DASHBOARD_PKG := github.com/pingcap/tidb-dashboard BUILD_TAGS ?= PNPM_INSTALL_TAGS ?= LDFLAGS ?= FEATURE_VERSION ?= 999.999.999 WITHOUT_NGM ?= false E2E_SPEC ?= UI ?= RELEASE_VERSION := $(shell git describe --tags --dirty --always) GITHASH := $(shell git rev-parse HEAD) LDFLAGS += -X "$(DASHBOARD_PKG)/pkg/utils/version.InternalVersion=$(RELEASE_VERSION)" LDFLAGS += -X "$(DASHBOARD_PKG)/pkg/utils/version.Standalone=Yes" LDFLAGS += -X "$(DASHBOARD_PKG)/pkg/utils/version.PDVersion=N/A" LDFLAGS += -X "$(DASHBOARD_PKG)/pkg/utils/version.BuildTime=$(shell date -u '+%Y-%m-%d %I:%M:%S')" LDFLAGS += -X "$(DASHBOARD_PKG)/pkg/utils/version.BuildGitHash=$(GITHASH)" TIDB_VERSION ?= latest # Docker build variables. REPOSITORY ?= pingcap/tidb-dashboard IMAGE ?= $(REPOSITORY):$(RELEASE_VERSION) AMD64 := linux/amd64 ARM64 := linux/arm64 PLATFORMS := $(AMD64),$(ARM64) DOCKERFILE ?= ./dockerfiles/alpine316.Dockerfile # If you want to build with no cache (after update go module, npm module, etc.), set "NO_CACHE=--pull --no-cache". NO_CACHE ?= BUILD_GOEXPERIMENT ?= BUILD_CGO_ENABLED ?= ifeq ("${ENABLE_FIPS}", "1") BUILD_TAGS += boringcrypto BUILD_GOEXPERIMENT = GOEXPERIMENT=boringcrypto BUILD_CGO_ENABLED = CGO_ENABLED=1 endif default: server .PHONY: clean clean: rm -rf ./coverage .PHONY: install_tools install_tools: scripts/install_go_tools.sh .PHONY: lint lint: scripts/lint.sh .PHONY: test test: clean unit_test integration_test .PHONY: unit_test unit_test: @mkdir -p ./coverage go test -v ./pkg/... ./util/... .PHONY: integration_test integration_test: @mkdir -p ./coverage @TIDB_VERSION=${TIDB_VERSION} tests/run.sh .PHONY: e2e_test e2e_test: @if $(WITHOUT_NGM); then\ make e2e_without_ngm_test;\ else\ make e2e_compat_features_test;\ make e2e_common_features_test;\ fi .PHONY: e2e_compat_features_test e2e_compat_features_test: cd ui &&\ pnpm i &&\ cd packages/tidb-dashboard-for-op &&\ pnpm run:e2e-test:compat-features --env FEATURE_VERSION=$(FEATURE_VERSION) TIDB_VERSION=$(TIDB_VERSION) .PHONY: e2e_common_features_test e2e_common_features_test: cd ui &&\ pnpm i &&\ cd packages/tidb-dashboard-for-op &&\ pnpm run:e2e-test:common-features --env TIDB_VERSION=$(TIDB_VERSION) .PHONY: e2e_without_ngm_test e2e_without_ngm_test: cd ui &&\ pnpm i &&\ cd packages/tidb-dashboard-for-op &&\ pnpm run:e2e-test:without-ngm --env TIDB_VERSION=$(TIDB_VERSION) WITHOUT_NGM=$(WITHOUT_NGM) .PHONY: e2e_test_specify e2e_test_specify: cd ui &&\ pnpm i &&\ cd packages/tidb-dashboard-for-op &&\ pnpm run:e2e-test:specify --env TIDB_VERSION=$(TIDB_VERSION) -- --spec $(E2E_SPEC) .PHONY: dev dev: lint default .PHONY: ui_deps ui_deps: install_tools cd ui &&\ pnpm i ${PNPM_INSTALL_TAGS} .PHONY: ui ui: ui_deps cd ui &&\ pnpm build .PHONY: go_generate go_generate: export PATH := $(shell pwd)/bin:$(PATH) go_generate: scripts/generate_swagger_spec.sh go generate -x ./... .PHONY: server ifeq ($(UI),1) BUILD_TAGS += ui_server endif server: install_tools go_generate ifeq ($(UI),1) scripts/embed_ui_assets.sh endif $(BUILD_GOEXPERIMENT) $(BUILD_CGO_ENABLED) go build -o bin/tidb-dashboard -ldflags '$(LDFLAGS)' -tags "${BUILD_TAGS}" cmd/tidb-dashboard/main.go .PHONY: embed_ui_assets embed_ui_assets: ui scripts/embed_ui_assets.sh .PHONY: package # Build frontend and backend server, and then packages them into a single binary. package: BUILD_TAGS += ui_server package: embed_ui_assets server .PHONY: docker-build-and-push-image # For locally dev, set IMAGE to your dev docker registry. docker-build-and-push-image: clean docker buildx build ${NO_CACHE} --push -t $(IMAGE) --platform $(PLATFORMS) -f $(DOCKERFILE) . .PHONY: docker-build-image-locally-amd64 docker-build-image-locally-amd64: clean docker buildx build ${NO_CACHE} --load -t $(IMAGE) --platform $(AMD64) -f $(DOCKERFILE) . docker run --rm $(IMAGE) -v .PHONY: docker-build-image-locally-arm64 docker-build-image-locally-arm64: clean docker buildx build ${NO_CACHE} --load -t $(IMAGE) --platform $(ARM64) -f $(DOCKERFILE) . docker run --rm $(IMAGE) -v .PHONY: tag tag: node scripts/create_release_tag.js .PHONY: run # please ensure that tiup playground is running in the background. run: bin/tidb-dashboard --debug --experimental --feature-version "$(FEATURE_VERSION)" --host 0.0.0.0 ================================================ FILE: OWNERS ================================================ # See the OWNERS docs at https://go.k8s.io/owners approvers: - baurine - breezewish - crazycs520 - Deardrops - HunDunDM - iosmanthus - shhdgit - zhongzc - XuHuaiyu - zimulala - nolouch - yibin87 - StinsonZhao - bosn reviewers: - Fullstop000 - Renkai - XuHuaiyu - zimulala - nolouch - yibin87 ================================================ FILE: README.md ================================================ # TiDB Dashboard [![GitHub license](https://img.shields.io/github/license/pingcap/tidb-dashboard?style=flat-square)](https://github.com/pingcap/tidb-dashboard/blob/master/LICENSE) TiDB Dashboard is a Web UI for monitoring, diagnosing and managing the TiDB cluster. ## Documentation - [Product User Manual (English)](https://docs.pingcap.com/tidb/stable/dashboard-intro) - [Product User Manual (Chinese)](https://docs.pingcap.com/zh/tidb/stable/dashboard-intro) - [FAQ (English)](https://docs.pingcap.com/tidb/stable/dashboard-faq) - [FAQ (Chinese)](https://docs.pingcap.com/zh/tidb/stable/dashboard-faq) ## Question, Suggestion Feel free to [open GitHub issues](https://github.com/pingcap/tidb-dashboard/issues/new/choose) for questions, support and suggestions. You may also consider to reach out on the [TiDB Internals forum](https://internals.tidb.io/) if you encounter any problems about TiDB development. For Chinese users, you can visit the PingCAP official user forum [AskTUG.com] to make life easier. ## Getting Started The most easy way to use TiDB Dashboard with an existing TiDB cluster is to use the one embedded into [PD]: . You need PD master branch or 4.0+ version to use TiDB Dashboard. Note: The TiDB Dashboard inside PD may be not up to date. To play with latest TiDB Dashboard, build it from source (see next section). ## Contributing & Developing Checkout our [help wanted issues](https://github.com/pingcap/tidb-dashboard/issues?q=is%3Aopen+label%3Astatus%2Fhelp-wanted+sort%3Aupdated-desc) for a list of recommended tasks, in which we have also marked the difficulty level. See [CONTRIBUTING.md](./CONTRIBUTING.md) for a detailed step-by-step contributing guide, or steps to build TiDB Dashboard from source. If you need any help, feel free to reach out on the [TiDB Internals forum](https://internals.tidb.io/). Thank you to all the people who already contributed to TiDB Dashboard!
## 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: ![](etc/arch_overview.svg) ## 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; 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 = ; close(SVG); unlink $svgfile; my $svg = join('', @svg); # Dot's SVG output is # # # # ... # # # # Change it to # # # $svg_javascript # # # ... # # # # Fix width, height; drop viewBox. $svg =~ s/(?s) above first my $svg_javascript = SvgJavascript(); my $viewport = "\n"; $svg =~ s/ above . $svg =~ s/(.*)(<\/svg>)/$1<\/g>$2/; $svg =~ s/$svgfile") || die "open $svgfile: $!"; print SVG $svg; close(SVG); } } sub SvgJavascript { return <<'EOF'; EOF } # Provides a map from fullname to shortname for cases where the # shortname is ambiguous. The symlist has both the fullname and # shortname for all symbols, which is usually fine, but sometimes -- # such as overloaded functions -- two different fullnames can map to # the same shortname. In that case, we use the address of the # function to disambiguate the two. This function fills in a map that # maps fullnames to modified shortnames in such cases. If a fullname # is not present in the map, the 'normal' shortname provided by the # symlist is the appropriate one to use. sub FillFullnameToShortnameMap { my $symbols = shift; my $fullname_to_shortname_map = shift; my $shortnames_seen_once = {}; my $shortnames_seen_more_than_once = {}; foreach my $symlist (values(%{$symbols})) { # TODO(csilvers): deal with inlined symbols too. my $shortname = $symlist->[0]; my $fullname = $symlist->[2]; if ($fullname !~ /<[0-9a-fA-F]+>$/) { # fullname doesn't end in an address next; # the only collisions we care about are when addresses differ } if (defined($shortnames_seen_once->{$shortname}) && $shortnames_seen_once->{$shortname} ne $fullname) { $shortnames_seen_more_than_once->{$shortname} = 1; } else { $shortnames_seen_once->{$shortname} = $fullname; } } foreach my $symlist (values(%{$symbols})) { my $shortname = $symlist->[0]; my $fullname = $symlist->[2]; # TODO(csilvers): take in a list of addresses we care about, and only # store in the map if $symlist->[1] is in that list. Saves space. next if defined($fullname_to_shortname_map->{$fullname}); if (defined($shortnames_seen_more_than_once->{$shortname})) { if ($fullname =~ /<0*([^>]*)>$/) { # fullname has address at end of it $fullname_to_shortname_map->{$fullname} = "$shortname\@$1"; } } } } # Return a small number that identifies the argument. # Multiple calls with the same argument will return the same number. # Calls with different arguments will return different numbers. sub ShortIdFor { my $key = shift; my $id = $main::uniqueid{$key}; if (!defined($id)) { $id = keys(%main::uniqueid) + 1; $main::uniqueid{$key} = $id; } return $id; } # Translate a stack of addresses into a stack of symbols sub TranslateStack { my $symbols = shift; my $fullname_to_shortname_map = shift; my $k = shift; my @addrs = split(/\n/, $k); my @result = (); for (my $i = 0; $i <= $#addrs; $i++) { my $a = $addrs[$i]; # Skip large addresses since they sometimes show up as fake entries on RH9 if (length($a) > 8 && $a gt "7fffffffffffffff") { next; } if ($main::opt_disasm || $main::opt_list) { # We want just the address for the key push(@result, $a); next; } my $symlist = $symbols->{$a}; if (!defined($symlist)) { $symlist = [$a, "", $a]; } # We can have a sequence of symbols for a particular entry # (more than one symbol in the case of inlining). Callers # come before callees in symlist, so walk backwards since # the translated stack should contain callees before callers. for (my $j = $#{$symlist}; $j >= 2; $j -= 3) { my $func = $symlist->[$j-2]; my $fileline = $symlist->[$j-1]; my $fullfunc = $symlist->[$j]; if (defined($fullname_to_shortname_map->{$fullfunc})) { $func = $fullname_to_shortname_map->{$fullfunc}; } if ($j > 2) { $func = "$func (inline)"; } # Do not merge nodes corresponding to Callback::Run since that # causes confusing cycles in dot display. Instead, we synthesize # a unique name for this frame per caller. if ($func =~ m/Callback.*::Run$/) { my $caller = ($i > 0) ? $addrs[$i-1] : 0; $func = "Run#" . ShortIdFor($caller); } if ($main::opt_addresses) { push(@result, "$a $func $fileline"); } elsif ($main::opt_lines) { if ($func eq '??' && $fileline eq '??:0') { push(@result, "$a"); } else { push(@result, "$func $fileline"); } } elsif ($main::opt_functions) { if ($func eq '??') { push(@result, "$a"); } else { push(@result, $func); } } elsif ($main::opt_files) { if ($fileline eq '??:0' || $fileline eq '') { push(@result, "$a"); } else { my $f = $fileline; $f =~ s/:\d+$//; push(@result, $f); } } else { push(@result, $a); last; # Do not print inlined info } } } # print join(",", @addrs), " => ", join(",", @result), "\n"; return @result; } # Generate percent string for a number and a total sub Percent { my $num = shift; my $tot = shift; if ($tot != 0) { return sprintf("%.1f%%", $num * 100.0 / $tot); } else { return ($num == 0) ? "nan" : (($num > 0) ? "+inf" : "-inf"); } } # Generate pretty-printed form of number sub Unparse { my $num = shift; if ($main::profile_type eq 'heap' || $main::profile_type eq 'growth') { if ($main::opt_inuse_objects || $main::opt_alloc_objects) { return sprintf("%d", $num); } else { if ($main::opt_show_bytes) { return sprintf("%d", $num); } else { return sprintf("%.1f", $num / 1048576.0); } } } elsif ($main::profile_type eq 'contention' && !$main::opt_contentions) { return sprintf("%.3f", $num / 1e9); # Convert nanoseconds to seconds } else { return sprintf("%d", $num); } } # Alternate pretty-printed form: 0 maps to "." sub UnparseAlt { my $num = shift; if ($num == 0) { return "."; } else { return Unparse($num); } } # Alternate pretty-printed form: 0 maps to "" sub HtmlPrintNumber { my $num = shift; if ($num == 0) { return ""; } else { return Unparse($num); } } # Return output units sub Units { if ($main::profile_type eq 'heap' || $main::profile_type eq 'growth') { if ($main::opt_inuse_objects || $main::opt_alloc_objects) { return "objects"; } else { if ($main::opt_show_bytes) { return "B"; } else { return "MB"; } } } elsif ($main::profile_type eq 'contention' && !$main::opt_contentions) { return "seconds"; } else { return "samples"; } } ##### Profile manipulation code ##### # Generate flattened profile: # If count is charged to stack [a,b,c,d], in generated profile, # it will be charged to [a] sub FlatProfile { my $profile = shift; my $result = {}; foreach my $k (keys(%{$profile})) { my $count = $profile->{$k}; my @addrs = split(/\n/, $k); if ($#addrs >= 0) { AddEntry($result, $addrs[0], $count); } } return $result; } # Generate cumulative profile: # If count is charged to stack [a,b,c,d], in generated profile, # it will be charged to [a], [b], [c], [d] sub CumulativeProfile { my $profile = shift; my $result = {}; foreach my $k (keys(%{$profile})) { my $count = $profile->{$k}; my @addrs = split(/\n/, $k); foreach my $a (@addrs) { AddEntry($result, $a, $count); } } return $result; } # If the second-youngest PC on the stack is always the same, returns # that pc. Otherwise, returns undef. sub IsSecondPcAlwaysTheSame { my $profile = shift; my $second_pc = undef; foreach my $k (keys(%{$profile})) { my @addrs = split(/\n/, $k); if ($#addrs < 1) { return undef; } if (not defined $second_pc) { $second_pc = $addrs[1]; } else { if ($second_pc ne $addrs[1]) { return undef; } } } return $second_pc; } sub ExtractSymbolNameInlineStack { my $symbols = shift; my $address = shift; my @stack = (); if (exists $symbols->{$address}) { my @localinlinestack = @{$symbols->{$address}}; for (my $i = $#localinlinestack; $i > 0; $i-=3) { my $file = $localinlinestack[$i-1]; my $fn = $localinlinestack[$i-0]; if ($file eq "?" || $file eq ":0") { $file = "??:0"; } if ($fn eq '??') { # If we can't get the symbol name, at least use the file information. $fn = $file; } my $suffix = "[inline]"; if ($i == 2) { $suffix = ""; } push (@stack, $fn.$suffix); } } else { # If we can't get a symbol name, at least fill in the address. push (@stack, $address); } return @stack; } sub ExtractSymbolLocation { my $symbols = shift; my $address = shift; # 'addr2line' outputs "??:0" for unknown locations; we do the # same to be consistent. my $location = "??:0:unknown"; if (exists $symbols->{$address}) { my $file = $symbols->{$address}->[1]; if ($file eq "?") { $file = "??:0" } $location = $file . ":" . $symbols->{$address}->[0]; } return $location; } # Extracts a graph of calls. sub ExtractCalls { my $symbols = shift; my $profile = shift; my $calls = {}; while( my ($stack_trace, $count) = each %$profile ) { my @address = split(/\n/, $stack_trace); my $destination = ExtractSymbolLocation($symbols, $address[0]); AddEntry($calls, $destination, $count); for (my $i = 1; $i <= $#address; $i++) { my $source = ExtractSymbolLocation($symbols, $address[$i]); my $call = "$source -> $destination"; AddEntry($calls, $call, $count); $destination = $source; } } return $calls; } sub FilterFrames { my $symbols = shift; my $profile = shift; if ($main::opt_retain eq '' && $main::opt_exclude eq '') { return $profile; } my $result = {}; foreach my $k (keys(%{$profile})) { my $count = $profile->{$k}; my @addrs = split(/\n/, $k); my @path = (); foreach my $a (@addrs) { my $sym; if (exists($symbols->{$a})) { $sym = $symbols->{$a}->[0]; } else { $sym = $a; } if ($main::opt_retain ne '' && $sym !~ m/$main::opt_retain/) { next; } if ($main::opt_exclude ne '' && $sym =~ m/$main::opt_exclude/) { next; } push(@path, $a); } if (scalar(@path) > 0) { my $reduced_path = join("\n", @path); AddEntry($result, $reduced_path, $count); } } return $result; } sub PrintCollapsedStacks { my $symbols = shift; my $profile = shift; while (my ($stack_trace, $count) = each %$profile) { my @address = split(/\n/, $stack_trace); my @names = reverse ( map { ExtractSymbolNameInlineStack($symbols, $_) } @address ); printf("%s %d\n", join(";", @names), $count); } } sub RemoveUninterestingFrames { my $symbols = shift; my $profile = shift; # List of function names to skip my %skip = (); my $skip_regexp = 'NOMATCH'; if ($main::profile_type eq 'heap' || $main::profile_type eq 'growth') { foreach my $name ('@JEMALLOC_PREFIX@calloc', 'cfree', '@JEMALLOC_PREFIX@malloc', 'je_malloc_default', 'newImpl', 'void* newImpl', 'fallbackNewImpl', 'void* fallbackNewImpl', '@JEMALLOC_PREFIX@free', '@JEMALLOC_PREFIX@memalign', '@JEMALLOC_PREFIX@posix_memalign', '@JEMALLOC_PREFIX@aligned_alloc', 'pvalloc', '@JEMALLOC_PREFIX@valloc', '@JEMALLOC_PREFIX@realloc', '@JEMALLOC_PREFIX@mallocx', '@JEMALLOC_PREFIX@rallocx', 'do_rallocx', '@JEMALLOC_PREFIX@xallocx', '@JEMALLOC_PREFIX@dallocx', '@JEMALLOC_PREFIX@sdallocx', '@JEMALLOC_PREFIX@sdallocx_noflags', 'tc_calloc', 'tc_cfree', 'tc_malloc', 'tc_free', 'tc_memalign', 'tc_posix_memalign', 'tc_pvalloc', 'tc_valloc', 'tc_realloc', 'tc_new', 'tc_delete', 'tc_newarray', 'tc_deletearray', 'tc_new_nothrow', 'tc_newarray_nothrow', 'do_malloc', '::do_malloc', # new name -- got moved to an unnamed ns '::do_malloc_or_cpp_alloc', 'DoSampledAllocation', 'simple_alloc::allocate', '__malloc_alloc_template::allocate', '__builtin_delete', '__builtin_new', '__builtin_vec_delete', '__builtin_vec_new', 'operator new', 'operator new[]', # The entry to our memory-allocation routines on OS X 'malloc_zone_malloc', 'malloc_zone_calloc', 'malloc_zone_valloc', 'malloc_zone_realloc', 'malloc_zone_memalign', 'malloc_zone_free', # These mark the beginning/end of our custom sections '__start_google_malloc', '__stop_google_malloc', '__start_malloc_hook', '__stop_malloc_hook') { $skip{$name} = 1; $skip{"_" . $name} = 1; # Mach (OS X) adds a _ prefix to everything } # TODO: Remove TCMalloc once everything has been # moved into the tcmalloc:: namespace and we have flushed # old code out of the system. $skip_regexp = "TCMalloc|^tcmalloc::"; } elsif ($main::profile_type eq 'contention') { foreach my $vname ('base::RecordLockProfileData', 'base::SubmitMutexProfileData', 'base::SubmitSpinLockProfileData', 'Mutex::Unlock', 'Mutex::UnlockSlow', 'Mutex::ReaderUnlock', 'MutexLock::~MutexLock', 'SpinLock::Unlock', 'SpinLock::SlowUnlock', 'SpinLockHolder::~SpinLockHolder') { $skip{$vname} = 1; } } elsif ($main::profile_type eq 'cpu') { # Drop signal handlers used for CPU profile collection # TODO(dpeng): this should not be necessary; it's taken # care of by the general 2nd-pc mechanism below. foreach my $name ('ProfileData::Add', # historical 'ProfileData::prof_handler', # historical 'CpuProfiler::prof_handler', '__FRAME_END__', '__pthread_sighandler', '__restore') { $skip{$name} = 1; } } else { # Nothing skipped for unknown types } if ($main::profile_type eq 'cpu') { # If all the second-youngest program counters are the same, # this STRONGLY suggests that it is an artifact of measurement, # i.e., stack frames pushed by the CPU profiler signal handler. # Hence, we delete them. # (The topmost PC is read from the signal structure, not from # the stack, so it does not get involved.) while (my $second_pc = IsSecondPcAlwaysTheSame($profile)) { my $result = {}; my $func = ''; if (exists($symbols->{$second_pc})) { $second_pc = $symbols->{$second_pc}->[0]; } print STDERR "Removing $second_pc from all stack traces.\n"; foreach my $k (keys(%{$profile})) { my $count = $profile->{$k}; my @addrs = split(/\n/, $k); splice @addrs, 1, 1; my $reduced_path = join("\n", @addrs); AddEntry($result, $reduced_path, $count); } $profile = $result; } } my $result = {}; foreach my $k (keys(%{$profile})) { my $count = $profile->{$k}; my @addrs = split(/\n/, $k); my @path = (); foreach my $a (@addrs) { if (exists($symbols->{$a})) { my $func = $symbols->{$a}->[0]; if ($skip{$func} || ($func =~ m/$skip_regexp/)) { # Throw away the portion of the backtrace seen so far, under the # assumption that previous frames were for functions internal to the # allocator. @path = (); next; } } push(@path, $a); } my $reduced_path = join("\n", @path); AddEntry($result, $reduced_path, $count); } $result = FilterFrames($symbols, $result); return $result; } # Reduce profile to granularity given by user sub ReduceProfile { my $symbols = shift; my $profile = shift; my $result = {}; my $fullname_to_shortname_map = {}; FillFullnameToShortnameMap($symbols, $fullname_to_shortname_map); foreach my $k (keys(%{$profile})) { my $count = $profile->{$k}; my @translated = TranslateStack($symbols, $fullname_to_shortname_map, $k); my @path = (); my %seen = (); $seen{''} = 1; # So that empty keys are skipped foreach my $e (@translated) { # To avoid double-counting due to recursion, skip a stack-trace # entry if it has already been seen if (!$seen{$e}) { $seen{$e} = 1; push(@path, $e); } } my $reduced_path = join("\n", @path); AddEntry($result, $reduced_path, $count); } return $result; } # Does the specified symbol array match the regexp? sub SymbolMatches { my $sym = shift; my $re = shift; if (defined($sym)) { for (my $i = 0; $i < $#{$sym}; $i += 3) { if ($sym->[$i] =~ m/$re/ || $sym->[$i+1] =~ m/$re/) { return 1; } } } return 0; } # Focus only on paths involving specified regexps sub FocusProfile { my $symbols = shift; my $profile = shift; my $focus = shift; my $result = {}; foreach my $k (keys(%{$profile})) { my $count = $profile->{$k}; my @addrs = split(/\n/, $k); foreach my $a (@addrs) { # Reply if it matches either the address/shortname/fileline if (($a =~ m/$focus/) || SymbolMatches($symbols->{$a}, $focus)) { AddEntry($result, $k, $count); last; } } } return $result; } # Focus only on paths not involving specified regexps sub IgnoreProfile { my $symbols = shift; my $profile = shift; my $ignore = shift; my $result = {}; foreach my $k (keys(%{$profile})) { my $count = $profile->{$k}; my @addrs = split(/\n/, $k); my $matched = 0; foreach my $a (@addrs) { # Reply if it matches either the address/shortname/fileline if (($a =~ m/$ignore/) || SymbolMatches($symbols->{$a}, $ignore)) { $matched = 1; last; } } if (!$matched) { AddEntry($result, $k, $count); } } return $result; } # Get total count in profile sub TotalProfile { my $profile = shift; my $result = 0; foreach my $k (keys(%{$profile})) { $result += $profile->{$k}; } return $result; } # Add A to B sub AddProfile { my $A = shift; my $B = shift; my $R = {}; # add all keys in A foreach my $k (keys(%{$A})) { my $v = $A->{$k}; AddEntry($R, $k, $v); } # add all keys in B foreach my $k (keys(%{$B})) { my $v = $B->{$k}; AddEntry($R, $k, $v); } return $R; } # Merges symbol maps sub MergeSymbols { my $A = shift; my $B = shift; my $R = {}; foreach my $k (keys(%{$A})) { $R->{$k} = $A->{$k}; } if (defined($B)) { foreach my $k (keys(%{$B})) { $R->{$k} = $B->{$k}; } } return $R; } # Add A to B sub AddPcs { my $A = shift; my $B = shift; my $R = {}; # add all keys in A foreach my $k (keys(%{$A})) { $R->{$k} = 1 } # add all keys in B foreach my $k (keys(%{$B})) { $R->{$k} = 1 } return $R; } # Subtract B from A sub SubtractProfile { my $A = shift; my $B = shift; my $R = {}; foreach my $k (keys(%{$A})) { my $v = $A->{$k} - GetEntry($B, $k); if ($v < 0 && $main::opt_drop_negative) { $v = 0; } AddEntry($R, $k, $v); } if (!$main::opt_drop_negative) { # Take care of when subtracted profile has more entries foreach my $k (keys(%{$B})) { if (!exists($A->{$k})) { AddEntry($R, $k, 0 - $B->{$k}); } } } return $R; } # Get entry from profile; zero if not present sub GetEntry { my $profile = shift; my $k = shift; if (exists($profile->{$k})) { return $profile->{$k}; } else { return 0; } } # Add entry to specified profile sub AddEntry { my $profile = shift; my $k = shift; my $n = shift; if (!exists($profile->{$k})) { $profile->{$k} = 0; } $profile->{$k} += $n; } # Add a stack of entries to specified profile, and add them to the $pcs # list. sub AddEntries { my $profile = shift; my $pcs = shift; my $stack = shift; my $count = shift; my @k = (); foreach my $e (split(/\s+/, $stack)) { my $pc = HexExtend($e); $pcs->{$pc} = 1; push @k, $pc; } AddEntry($profile, (join "\n", @k), $count); } ##### Code to profile a server dynamically ##### sub CheckSymbolPage { my $url = SymbolPageURL(); my $command = ShellEscape(@URL_FETCHER, $url); open(SYMBOL, "$command |") or error($command); my $line = ; $line =~ s/\r//g; # turn windows-looking lines into unix-looking lines close(SYMBOL); unless (defined($line)) { error("$url doesn't exist\n"); } if ($line =~ /^num_symbols:\s+(\d+)$/) { if ($1 == 0) { error("Stripped binary. No symbols available.\n"); } } else { error("Failed to get the number of symbols from $url\n"); } } sub IsProfileURL { my $profile_name = shift; if (-f $profile_name) { printf STDERR "Using local file $profile_name.\n"; return 0; } return 1; } sub ParseProfileURL { my $profile_name = shift; if (!defined($profile_name) || $profile_name eq "") { return (); } # Split profile URL - matches all non-empty strings, so no test. $profile_name =~ m,^(https?://)?([^/]+)(.*?)(/|$PROFILES)?$,; my $proto = $1 || "http://"; my $hostport = $2; my $prefix = $3; my $profile = $4 || "/"; my $baseurl = "$proto$hostport$prefix"; return ($hostport, $baseurl, $profile); } # We fetch symbols from the first profile argument. sub SymbolPageURL { my ($hostport, $baseURL, $path) = ParseProfileURL($main::pfile_args[0]); return "$baseURL$SYMBOL_PAGE"; } sub FetchProgramName() { my ($hostport, $baseURL, $path) = ParseProfileURL($main::pfile_args[0]); my $url = "$baseURL$PROGRAM_NAME_PAGE"; my $command_line = ShellEscape(@URL_FETCHER, $url); open(CMDLINE, "$command_line |") or error($command_line); my $cmdline = ; $cmdline =~ s/\r//g; # turn windows-looking lines into unix-looking lines close(CMDLINE); error("Failed to get program name from $url\n") unless defined($cmdline); $cmdline =~ s/\x00.+//; # Remove argv[1] and latters. $cmdline =~ s!\n!!g; # Remove LFs. return $cmdline; } # Gee, curl's -L (--location) option isn't reliable at least # with its 7.12.3 version. Curl will forget to post data if # there is a redirection. This function is a workaround for # curl. Redirection happens on borg hosts. sub ResolveRedirectionForCurl { my $url = shift; my $command_line = ShellEscape(@URL_FETCHER, "--head", $url); open(CMDLINE, "$command_line |") or error($command_line); while () { s/\r//g; # turn windows-looking lines into unix-looking lines if (/^Location: (.*)/) { $url = $1; } } close(CMDLINE); return $url; } # Add a timeout flat to URL_FETCHER. Returns a new list. sub AddFetchTimeout { my $timeout = shift; my @fetcher = @_; if (defined($timeout)) { if (join(" ", @fetcher) =~ m/\bcurl -s/) { push(@fetcher, "--max-time", sprintf("%d", $timeout)); } elsif (join(" ", @fetcher) =~ m/\brpcget\b/) { push(@fetcher, sprintf("--deadline=%d", $timeout)); } } return @fetcher; } # Reads a symbol map from the file handle name given as $1, returning # the resulting symbol map. Also processes variables relating to symbols. # Currently, the only variable processed is 'binary=' which updates # $main::prog to have the correct program name. sub ReadSymbols { my $in = shift; my $map = {}; while (<$in>) { s/\r//g; # turn windows-looking lines into unix-looking lines # Removes all the leading zeroes from the symbols, see comment below. if (m/^0x0*([0-9a-f]+)\s+(.+)/) { $map->{$1} = $2; } elsif (m/^---/) { last; } elsif (m/^([a-z][^=]*)=(.*)$/ ) { my ($variable, $value) = ($1, $2); for ($variable, $value) { s/^\s+//; s/\s+$//; } if ($variable eq "binary") { if ($main::prog ne $UNKNOWN_BINARY && $main::prog ne $value) { printf STDERR ("Warning: Mismatched binary name '%s', using '%s'.\n", $main::prog, $value); } $main::prog = $value; } else { printf STDERR ("Ignoring unknown variable in symbols list: " . "'%s' = '%s'\n", $variable, $value); } } } return $map; } sub URLEncode { my $str = shift; $str =~ s/([^A-Za-z0-9\-_.!~*'()])/ sprintf "%%%02x", ord $1 /eg; return $str; } sub AppendSymbolFilterParams { my $url = shift; my @params = (); if ($main::opt_retain ne '') { push(@params, sprintf("retain=%s", URLEncode($main::opt_retain))); } if ($main::opt_exclude ne '') { push(@params, sprintf("exclude=%s", URLEncode($main::opt_exclude))); } if (scalar @params > 0) { $url = sprintf("%s?%s", $url, join("&", @params)); } return $url; } # Fetches and processes symbols to prepare them for use in the profile output # code. If the optional 'symbol_map' arg is not given, fetches symbols from # $SYMBOL_PAGE for all PC values found in profile. Otherwise, the raw symbols # are assumed to have already been fetched into 'symbol_map' and are simply # extracted and processed. sub FetchSymbols { my $pcset = shift; my $symbol_map = shift; my %seen = (); my @pcs = grep { !$seen{$_}++ } keys(%$pcset); # uniq if (!defined($symbol_map)) { my $post_data = join("+", sort((map {"0x" . "$_"} @pcs))); open(POSTFILE, ">$main::tmpfile_sym"); print POSTFILE $post_data; close(POSTFILE); my $url = SymbolPageURL(); my $command_line; if (join(" ", @URL_FETCHER) =~ m/\bcurl -s/) { $url = ResolveRedirectionForCurl($url); $url = AppendSymbolFilterParams($url); $command_line = ShellEscape(@URL_FETCHER, "-d", "\@$main::tmpfile_sym", $url); } else { $url = AppendSymbolFilterParams($url); $command_line = (ShellEscape(@URL_FETCHER, "--post", $url) . " < " . ShellEscape($main::tmpfile_sym)); } # We use c++filt in case $SYMBOL_PAGE gives us mangled symbols. my $escaped_cppfilt = ShellEscape($obj_tool_map{"c++filt"}); open(SYMBOL, "$command_line | $escaped_cppfilt |") or error($command_line); $symbol_map = ReadSymbols(*SYMBOL{IO}); close(SYMBOL); } my $symbols = {}; foreach my $pc (@pcs) { my $fullname; # For 64 bits binaries, symbols are extracted with 8 leading zeroes. # Then /symbol reads the long symbols in as uint64, and outputs # the result with a "0x%08llx" format which get rid of the zeroes. # By removing all the leading zeroes in both $pc and the symbols from # /symbol, the symbols match and are retrievable from the map. my $shortpc = $pc; $shortpc =~ s/^0*//; # Each line may have a list of names, which includes the function # and also other functions it has inlined. They are separated (in # PrintSymbolizedProfile), by --, which is illegal in function names. my $fullnames; if (defined($symbol_map->{$shortpc})) { $fullnames = $symbol_map->{$shortpc}; } else { $fullnames = "0x" . $pc; # Just use addresses } my $sym = []; $symbols->{$pc} = $sym; foreach my $fullname (split("--", $fullnames)) { my $name = ShortFunctionName($fullname); push(@{$sym}, $name, "?", $fullname); } } return $symbols; } sub BaseName { my $file_name = shift; $file_name =~ s!^.*/!!; # Remove directory name return $file_name; } sub MakeProfileBaseName { my ($binary_name, $profile_name) = @_; my ($hostport, $baseURL, $path) = ParseProfileURL($profile_name); my $binary_shortname = BaseName($binary_name); return sprintf("%s.%s.%s", $binary_shortname, $main::op_time, $hostport); } sub FetchDynamicProfile { my $binary_name = shift; my $profile_name = shift; my $fetch_name_only = shift; my $encourage_patience = shift; if (!IsProfileURL($profile_name)) { return $profile_name; } else { my ($hostport, $baseURL, $path) = ParseProfileURL($profile_name); if ($path eq "" || $path eq "/") { # Missing type specifier defaults to cpu-profile $path = $PROFILE_PAGE; } my $profile_file = MakeProfileBaseName($binary_name, $profile_name); my $url = "$baseURL$path"; my $fetch_timeout = undef; if ($path =~ m/$PROFILE_PAGE|$PMUPROFILE_PAGE/) { if ($path =~ m/[?]/) { $url .= "&"; } else { $url .= "?"; } $url .= sprintf("seconds=%d", $main::opt_seconds); $fetch_timeout = $main::opt_seconds * 1.01 + 60; # Set $profile_type for consumption by PrintSymbolizedProfile. $main::profile_type = 'cpu'; } else { # For non-CPU profiles, we add a type-extension to # the target profile file name. my $suffix = $path; $suffix =~ s,/,.,g; $profile_file .= $suffix; # Set $profile_type for consumption by PrintSymbolizedProfile. if ($path =~ m/$HEAP_PAGE/) { $main::profile_type = 'heap'; } elsif ($path =~ m/$GROWTH_PAGE/) { $main::profile_type = 'growth'; } elsif ($path =~ m/$CONTENTION_PAGE/) { $main::profile_type = 'contention'; } } my $profile_dir = $ENV{"JEPROF_TMPDIR"} || ($ENV{HOME} . "/jeprof"); if (! -d $profile_dir) { mkdir($profile_dir) || die("Unable to create profile directory $profile_dir: $!\n"); } my $tmp_profile = "$profile_dir/.tmp.$profile_file"; my $real_profile = "$profile_dir/$profile_file"; if ($fetch_name_only > 0) { return $real_profile; } my @fetcher = AddFetchTimeout($fetch_timeout, @URL_FETCHER); my $cmd = ShellEscape(@fetcher, $url) . " > " . ShellEscape($tmp_profile); if ($path =~ m/$PROFILE_PAGE|$PMUPROFILE_PAGE|$CENSUSPROFILE_PAGE/){ print STDERR "Gathering CPU profile from $url for $main::opt_seconds seconds to\n ${real_profile}\n"; if ($encourage_patience) { print STDERR "Be patient...\n"; } } else { print STDERR "Fetching $path profile from $url to\n ${real_profile}\n"; } (system($cmd) == 0) || error("Failed to get profile: $cmd: $!\n"); (system("mv", $tmp_profile, $real_profile) == 0) || error("Unable to rename profile\n"); print STDERR "Wrote profile to $real_profile\n"; $main::collected_profile = $real_profile; return $main::collected_profile; } } # Collect profiles in parallel sub FetchDynamicProfiles { my $items = scalar(@main::pfile_args); my $levels = log($items) / log(2); if ($items == 1) { $main::profile_files[0] = FetchDynamicProfile($main::prog, $main::pfile_args[0], 0, 1); } else { # math rounding issues if ((2 ** $levels) < $items) { $levels++; } my $count = scalar(@main::pfile_args); for (my $i = 0; $i < $count; $i++) { $main::profile_files[$i] = FetchDynamicProfile($main::prog, $main::pfile_args[$i], 1, 0); } print STDERR "Fetching $count profiles, Be patient...\n"; FetchDynamicProfilesRecurse($levels, 0, 0); $main::collected_profile = join(" \\\n ", @main::profile_files); } } # Recursively fork a process to get enough processes # collecting profiles sub FetchDynamicProfilesRecurse { my $maxlevel = shift; my $level = shift; my $position = shift; if (my $pid = fork()) { $position = 0 | ($position << 1); TryCollectProfile($maxlevel, $level, $position); wait; } else { $position = 1 | ($position << 1); TryCollectProfile($maxlevel, $level, $position); cleanup(); exit(0); } } # Collect a single profile sub TryCollectProfile { my $maxlevel = shift; my $level = shift; my $position = shift; if ($level >= ($maxlevel - 1)) { if ($position < scalar(@main::pfile_args)) { FetchDynamicProfile($main::prog, $main::pfile_args[$position], 0, 0); } } else { FetchDynamicProfilesRecurse($maxlevel, $level+1, $position); } } ##### Parsing code ##### # Provide a small streaming-read module to handle very large # cpu-profile files. Stream in chunks along a sliding window. # Provides an interface to get one 'slot', correctly handling # endian-ness differences. A slot is one 32-bit or 64-bit word # (depending on the input profile). We tell endianness and bit-size # for the profile by looking at the first 8 bytes: in cpu profiles, # the second slot is always 3 (we'll accept anything that's not 0). BEGIN { package CpuProfileStream; sub new { my ($class, $file, $fname) = @_; my $self = { file => $file, base => 0, stride => 512 * 1024, # must be a multiple of bitsize/8 slots => [], unpack_code => "", # N for big-endian, V for little perl_is_64bit => 1, # matters if profile is 64-bit }; bless $self, $class; # Let unittests adjust the stride if ($main::opt_test_stride > 0) { $self->{stride} = $main::opt_test_stride; } # Read the first two slots to figure out bitsize and endianness. my $slots = $self->{slots}; my $str; read($self->{file}, $str, 8); # Set the global $address_length based on what we see here. # 8 is 32-bit (8 hexadecimal chars); 16 is 64-bit (16 hexadecimal chars). $address_length = ($str eq (chr(0)x8)) ? 16 : 8; if ($address_length == 8) { if (substr($str, 6, 2) eq chr(0)x2) { $self->{unpack_code} = 'V'; # Little-endian. } elsif (substr($str, 4, 2) eq chr(0)x2) { $self->{unpack_code} = 'N'; # Big-endian } else { ::error("$fname: header size >= 2**16\n"); } @$slots = unpack($self->{unpack_code} . "*", $str); } else { # If we're a 64-bit profile, check if we're a 64-bit-capable # perl. Otherwise, each slot will be represented as a float # instead of an int64, losing precision and making all the # 64-bit addresses wrong. We won't complain yet, but will # later if we ever see a value that doesn't fit in 32 bits. my $has_q = 0; eval { $has_q = pack("Q", "1") ? 1 : 1; }; if (!$has_q) { $self->{perl_is_64bit} = 0; } read($self->{file}, $str, 8); if (substr($str, 4, 4) eq chr(0)x4) { # We'd love to use 'Q', but it's a) not universal, b) not endian-proof. $self->{unpack_code} = 'V'; # Little-endian. } elsif (substr($str, 0, 4) eq chr(0)x4) { $self->{unpack_code} = 'N'; # Big-endian } else { ::error("$fname: header size >= 2**32\n"); } my @pair = unpack($self->{unpack_code} . "*", $str); # Since we know one of the pair is 0, it's fine to just add them. @$slots = (0, $pair[0] + $pair[1]); } return $self; } # Load more data when we access slots->get(X) which is not yet in memory. sub overflow { my ($self) = @_; my $slots = $self->{slots}; $self->{base} += $#$slots + 1; # skip over data we're replacing my $str; read($self->{file}, $str, $self->{stride}); if ($address_length == 8) { # the 32-bit case # This is the easy case: unpack provides 32-bit unpacking primitives. @$slots = unpack($self->{unpack_code} . "*", $str); } else { # We need to unpack 32 bits at a time and combine. my @b32_values = unpack($self->{unpack_code} . "*", $str); my @b64_values = (); for (my $i = 0; $i < $#b32_values; $i += 2) { # TODO(csilvers): if this is a 32-bit perl, the math below # could end up in a too-large int, which perl will promote # to a double, losing necessary precision. Deal with that. # Right now, we just die. my ($lo, $hi) = ($b32_values[$i], $b32_values[$i+1]); if ($self->{unpack_code} eq 'N') { # big-endian ($lo, $hi) = ($hi, $lo); } my $value = $lo + $hi * (2**32); if (!$self->{perl_is_64bit} && # check value is exactly represented (($value % (2**32)) != $lo || int($value / (2**32)) != $hi)) { ::error("Need a 64-bit perl to process this 64-bit profile.\n"); } push(@b64_values, $value); } @$slots = @b64_values; } } # Access the i-th long in the file (logically), or -1 at EOF. sub get { my ($self, $idx) = @_; my $slots = $self->{slots}; while ($#$slots >= 0) { if ($idx < $self->{base}) { # The only time we expect a reference to $slots[$i - something] # after referencing $slots[$i] is reading the very first header. # Since $stride > |header|, that shouldn't cause any lookback # errors. And everything after the header is sequential. print STDERR "Unexpected look-back reading CPU profile"; return -1; # shrug, don't know what better to return } elsif ($idx > $self->{base} + $#$slots) { $self->overflow(); } else { return $slots->[$idx - $self->{base}]; } } # If we get here, $slots is [], which means we've reached EOF return -1; # unique since slots is supposed to hold unsigned numbers } } # Reads the top, 'header' section of a profile, and returns the last # line of the header, commonly called a 'header line'. The header # section of a profile consists of zero or more 'command' lines that # are instructions to jeprof, which jeprof executes when reading the # header. All 'command' lines start with a %. After the command # lines is the 'header line', which is a profile-specific line that # indicates what type of profile it is, and perhaps other global # information about the profile. For instance, here's a header line # for a heap profile: # heap profile: 53: 38236 [ 5525: 1284029] @ heapprofile # For historical reasons, the CPU profile does not contain a text- # readable header line. If the profile looks like a CPU profile, # this function returns "". If no header line could be found, this # function returns undef. # # The following commands are recognized: # %warn -- emit the rest of this line to stderr, prefixed by 'WARNING:' # # The input file should be in binmode. sub ReadProfileHeader { local *PROFILE = shift; my $firstchar = ""; my $line = ""; read(PROFILE, $firstchar, 1); seek(PROFILE, -1, 1); # unread the firstchar if ($firstchar !~ /[[:print:]]/) { # is not a text character return ""; } while (defined($line = )) { $line =~ s/\r//g; # turn windows-looking lines into unix-looking lines if ($line =~ /^%warn\s+(.*)/) { # 'warn' command # Note this matches both '%warn blah\n' and '%warn\n'. print STDERR "WARNING: $1\n"; # print the rest of the line } elsif ($line =~ /^%/) { print STDERR "Ignoring unknown command from profile header: $line"; } else { # End of commands, must be the header line. return $line; } } return undef; # got to EOF without seeing a header line } sub IsSymbolizedProfileFile { my $file_name = shift; if (!(-e $file_name) || !(-r $file_name)) { return 0; } # Check if the file contains a symbol-section marker. open(TFILE, "<$file_name"); binmode TFILE; my $firstline = ReadProfileHeader(*TFILE); close(TFILE); if (!$firstline) { return 0; } $SYMBOL_PAGE =~ m,[^/]+$,; # matches everything after the last slash my $symbol_marker = $&; return $firstline =~ /^--- *$symbol_marker/; } # Parse profile generated by common/profiler.cc and return a reference # to a map: # $result->{version} Version number of profile file # $result->{period} Sampling period (in microseconds) # $result->{profile} Profile object # $result->{threads} Map of thread IDs to profile objects # $result->{map} Memory map info from profile # $result->{pcs} Hash of all PC values seen, key is hex address sub ReadProfile { my $prog = shift; my $fname = shift; my $result; # return value $CONTENTION_PAGE =~ m,[^/]+$,; # matches everything after the last slash my $contention_marker = $&; $GROWTH_PAGE =~ m,[^/]+$,; # matches everything after the last slash my $growth_marker = $&; $SYMBOL_PAGE =~ m,[^/]+$,; # matches everything after the last slash my $symbol_marker = $&; $PROFILE_PAGE =~ m,[^/]+$,; # matches everything after the last slash my $profile_marker = $&; $HEAP_PAGE =~ m,[^/]+$,; # matches everything after the last slash my $heap_marker = $&; # Look at first line to see if it is a heap or a CPU profile. # CPU profile may start with no header at all, and just binary data # (starting with \0\0\0\0) -- in that case, don't try to read the # whole firstline, since it may be gigabytes(!) of data. open(PROFILE, "<$fname") || error("$fname: $!\n"); binmode PROFILE; # New perls do UTF-8 processing my $header = ReadProfileHeader(*PROFILE); if (!defined($header)) { # means "at EOF" error("Profile is empty.\n"); } my $symbols; if ($header =~ m/^--- *$symbol_marker/o) { # Verify that the user asked for a symbolized profile if (!$main::use_symbolized_profile) { # we have both a binary and symbolized profiles, abort error("FATAL ERROR: Symbolized profile\n $fname\ncannot be used with " . "a binary arg. Try again without passing\n $prog\n"); } # Read the symbol section of the symbolized profile file. $symbols = ReadSymbols(*PROFILE{IO}); # Read the next line to get the header for the remaining profile. $header = ReadProfileHeader(*PROFILE) || ""; } if ($header =~ m/^--- *($heap_marker|$growth_marker)/o) { # Skip "--- ..." line for profile types that have their own headers. $header = ReadProfileHeader(*PROFILE) || ""; } $main::profile_type = ''; if ($header =~ m/^heap profile:.*$growth_marker/o) { $main::profile_type = 'growth'; $result = ReadHeapProfile($prog, *PROFILE, $header); } elsif ($header =~ m/^heap profile:/) { $main::profile_type = 'heap'; $result = ReadHeapProfile($prog, *PROFILE, $header); } elsif ($header =~ m/^heap/) { $main::profile_type = 'heap'; $result = ReadThreadedHeapProfile($prog, $fname, $header); } elsif ($header =~ m/^--- *$contention_marker/o) { $main::profile_type = 'contention'; $result = ReadSynchProfile($prog, *PROFILE); } elsif ($header =~ m/^--- *Stacks:/) { print STDERR "Old format contention profile: mistakenly reports " . "condition variable signals as lock contentions.\n"; $main::profile_type = 'contention'; $result = ReadSynchProfile($prog, *PROFILE); } elsif ($header =~ m/^--- *$profile_marker/) { # the binary cpu profile data starts immediately after this line $main::profile_type = 'cpu'; $result = ReadCPUProfile($prog, $fname, *PROFILE); } else { if (defined($symbols)) { # a symbolized profile contains a format we don't recognize, bail out error("$fname: Cannot recognize profile section after symbols.\n"); } # no ascii header present -- must be a CPU profile $main::profile_type = 'cpu'; $result = ReadCPUProfile($prog, $fname, *PROFILE); } close(PROFILE); # if we got symbols along with the profile, return those as well if (defined($symbols)) { $result->{symbols} = $symbols; } return $result; } # Subtract one from caller pc so we map back to call instr. # However, don't do this if we're reading a symbolized profile # file, in which case the subtract-one was done when the file # was written. # # We apply the same logic to all readers, though ReadCPUProfile uses an # independent implementation. sub FixCallerAddresses { my $stack = shift; # --raw/http: Always subtract one from pc's, because PrintSymbolizedProfile() # dumps unadjusted profiles. { $stack =~ /(\s)/; my $delimiter = $1; my @addrs = split(' ', $stack); my @fixedaddrs; $#fixedaddrs = $#addrs; if ($#addrs >= 0) { $fixedaddrs[0] = $addrs[0]; } for (my $i = 1; $i <= $#addrs; $i++) { $fixedaddrs[$i] = AddressSub($addrs[$i], "0x1"); } return join $delimiter, @fixedaddrs; } } # CPU profile reader sub ReadCPUProfile { my $prog = shift; my $fname = shift; # just used for logging local *PROFILE = shift; my $version; my $period; my $i; my $profile = {}; my $pcs = {}; # Parse string into array of slots. my $slots = CpuProfileStream->new(*PROFILE, $fname); # Read header. The current header version is a 5-element structure # containing: # 0: header count (always 0) # 1: header "words" (after this one: 3) # 2: format version (0) # 3: sampling period (usec) # 4: unused padding (always 0) if ($slots->get(0) != 0 ) { error("$fname: not a profile file, or old format profile file\n"); } $i = 2 + $slots->get(1); $version = $slots->get(2); $period = $slots->get(3); # Do some sanity checking on these header values. if ($version > (2**32) || $period > (2**32) || $i > (2**32) || $i < 5) { error("$fname: not a profile file, or corrupted profile file\n"); } # Parse profile while ($slots->get($i) != -1) { my $n = $slots->get($i++); my $d = $slots->get($i++); if ($d > (2**16)) { # TODO(csilvers): what's a reasonable max-stack-depth? my $addr = sprintf("0%o", $i * ($address_length == 8 ? 4 : 8)); print STDERR "At index $i (address $addr):\n"; error("$fname: stack trace depth >= 2**32\n"); } if ($slots->get($i) == 0) { # End of profile data marker $i += $d; last; } # Make key out of the stack entries my @k = (); for (my $j = 0; $j < $d; $j++) { my $pc = $slots->get($i+$j); # Subtract one from caller pc so we map back to call instr. $pc--; $pc = sprintf("%0*x", $address_length, $pc); $pcs->{$pc} = 1; push @k, $pc; } AddEntry($profile, (join "\n", @k), $n); $i += $d; } # Parse map my $map = ''; seek(PROFILE, $i * 4, 0); read(PROFILE, $map, (stat PROFILE)[7]); my $r = {}; $r->{version} = $version; $r->{period} = $period; $r->{profile} = $profile; $r->{libs} = ParseLibraries($prog, $map, $pcs); $r->{pcs} = $pcs; return $r; } sub HeapProfileIndex { my $index = 1; if ($main::opt_inuse_space) { $index = 1; } elsif ($main::opt_inuse_objects) { $index = 0; } elsif ($main::opt_alloc_space) { $index = 3; } elsif ($main::opt_alloc_objects) { $index = 2; } return $index; } sub ReadMappedLibraries { my $fh = shift; my $map = ""; # Read the /proc/self/maps data while (<$fh>) { s/\r//g; # turn windows-looking lines into unix-looking lines $map .= $_; } return $map; } sub ReadMemoryMap { my $fh = shift; my $map = ""; # Read /proc/self/maps data as formatted by DumpAddressMap() my $buildvar = ""; while () { s/\r//g; # turn windows-looking lines into unix-looking lines # Parse "build=" specification if supplied if (m/^\s*build=(.*)\n/) { $buildvar = $1; } # Expand "$build" variable if available $_ =~ s/\$build\b/$buildvar/g; $map .= $_; } return $map; } sub AdjustSamples { my ($sample_adjustment, $sampling_algorithm, $n1, $s1, $n2, $s2) = @_; if ($sample_adjustment) { if ($sampling_algorithm == 2) { # Remote-heap version 2 # The sampling frequency is the rate of a Poisson process. # This means that the probability of sampling an allocation of # size X with sampling rate Y is 1 - exp(-X/Y) if ($n1 != 0) { my $ratio = (($s1*1.0)/$n1)/($sample_adjustment); my $scale_factor = 1/(1 - exp(-$ratio)); $n1 *= $scale_factor; $s1 *= $scale_factor; } if ($n2 != 0) { my $ratio = (($s2*1.0)/$n2)/($sample_adjustment); my $scale_factor = 1/(1 - exp(-$ratio)); $n2 *= $scale_factor; $s2 *= $scale_factor; } } else { # Remote-heap version 1 my $ratio; $ratio = (($s1*1.0)/$n1)/($sample_adjustment); if ($ratio < 1) { $n1 /= $ratio; $s1 /= $ratio; } $ratio = (($s2*1.0)/$n2)/($sample_adjustment); if ($ratio < 1) { $n2 /= $ratio; $s2 /= $ratio; } } } return ($n1, $s1, $n2, $s2); } sub ReadHeapProfile { my $prog = shift; local *PROFILE = shift; my $header = shift; my $index = HeapProfileIndex(); # Find the type of this profile. The header line looks like: # heap profile: 1246: 8800744 [ 1246: 8800744] @ /266053 # There are two pairs , the first inuse objects/space, and the # second allocated objects/space. This is followed optionally by a profile # type, and if that is present, optionally by a sampling frequency. # For remote heap profiles (v1): # The interpretation of the sampling frequency is that the profiler, for # each sample, calculates a uniformly distributed random integer less than # the given value, and records the next sample after that many bytes have # been allocated. Therefore, the expected sample interval is half of the # given frequency. By default, if not specified, the expected sample # interval is 128KB. Only remote-heap-page profiles are adjusted for # sample size. # For remote heap profiles (v2): # The sampling frequency is the rate of a Poisson process. This means that # the probability of sampling an allocation of size X with sampling rate Y # is 1 - exp(-X/Y) # For version 2, a typical header line might look like this: # heap profile: 1922: 127792360 [ 1922: 127792360] @ _v2/524288 # the trailing number (524288) is the sampling rate. (Version 1 showed # double the 'rate' here) my $sampling_algorithm = 0; my $sample_adjustment = 0; chomp($header); my $type = "unknown"; if ($header =~ m"^heap profile:\s*(\d+):\s+(\d+)\s+\[\s*(\d+):\s+(\d+)\](\s*@\s*([^/]*)(/(\d+))?)?") { if (defined($6) && ($6 ne '')) { $type = $6; my $sample_period = $8; # $type is "heapprofile" for profiles generated by the # heap-profiler, and either "heap" or "heap_v2" for profiles # generated by sampling directly within tcmalloc. It can also # be "growth" for heap-growth profiles. The first is typically # found for profiles generated locally, and the others for # remote profiles. if (($type eq "heapprofile") || ($type !~ /heap/) ) { # No need to adjust for the sampling rate with heap-profiler-derived data $sampling_algorithm = 0; } elsif ($type =~ /_v2/) { $sampling_algorithm = 2; # version 2 sampling if (defined($sample_period) && ($sample_period ne '')) { $sample_adjustment = int($sample_period); } } else { $sampling_algorithm = 1; # version 1 sampling if (defined($sample_period) && ($sample_period ne '')) { $sample_adjustment = int($sample_period)/2; } } } else { # We detect whether or not this is a remote-heap profile by checking # that the total-allocated stats ($n2,$s2) are exactly the # same as the in-use stats ($n1,$s1). It is remotely conceivable # that a non-remote-heap profile may pass this check, but it is hard # to imagine how that could happen. # In this case it's so old it's guaranteed to be remote-heap version 1. my ($n1, $s1, $n2, $s2) = ($1, $2, $3, $4); if (($n1 == $n2) && ($s1 == $s2)) { # This is likely to be a remote-heap based sample profile $sampling_algorithm = 1; } } } if ($sampling_algorithm > 0) { # For remote-heap generated profiles, adjust the counts and sizes to # account for the sample rate (we sample once every 128KB by default). if ($sample_adjustment == 0) { # Turn on profile adjustment. $sample_adjustment = 128*1024; print STDERR "Adjusting heap profiles for 1-in-128KB sampling rate\n"; } else { printf STDERR ("Adjusting heap profiles for 1-in-%d sampling rate\n", $sample_adjustment); } if ($sampling_algorithm > 1) { # We don't bother printing anything for the original version (version 1) printf STDERR "Heap version $sampling_algorithm\n"; } } my $profile = {}; my $pcs = {}; my $map = ""; while () { s/\r//g; # turn windows-looking lines into unix-looking lines if (/^MAPPED_LIBRARIES:/) { $map .= ReadMappedLibraries(*PROFILE); last; } if (/^--- Memory map:/) { $map .= ReadMemoryMap(*PROFILE); last; } # Read entry of the form: # : [: ] @ a1 a2 a3 ... an s/^\s*//; s/\s*$//; if (m/^\s*(\d+):\s+(\d+)\s+\[\s*(\d+):\s+(\d+)\]\s+@\s+(.*)$/) { my $stack = $5; my ($n1, $s1, $n2, $s2) = ($1, $2, $3, $4); my @counts = AdjustSamples($sample_adjustment, $sampling_algorithm, $n1, $s1, $n2, $s2); AddEntries($profile, $pcs, FixCallerAddresses($stack), $counts[$index]); } } my $r = {}; $r->{version} = "heap"; $r->{period} = 1; $r->{profile} = $profile; $r->{libs} = ParseLibraries($prog, $map, $pcs); $r->{pcs} = $pcs; return $r; } sub ReadThreadedHeapProfile { my ($prog, $fname, $header) = @_; my $index = HeapProfileIndex(); my $sampling_algorithm = 0; my $sample_adjustment = 0; chomp($header); my $type = "unknown"; # Assuming a very specific type of header for now. if ($header =~ m"^heap_v2/(\d+)") { $type = "_v2"; $sampling_algorithm = 2; $sample_adjustment = int($1); } if ($type ne "_v2" || !defined($sample_adjustment)) { die "Threaded heap profiles require v2 sampling with a sample rate\n"; } my $profile = {}; my $thread_profiles = {}; my $pcs = {}; my $map = ""; my $stack = ""; while () { s/\r//g; if (/^MAPPED_LIBRARIES:/) { $map .= ReadMappedLibraries(*PROFILE); last; } if (/^--- Memory map:/) { $map .= ReadMemoryMap(*PROFILE); last; } # Read entry of the form: # @ a1 a2 ... an # t*: : [: ] # t1: : [: ] # ... # tn: : [: ] s/^\s*//; s/\s*$//; if (m/^@\s+(.*)$/) { $stack = $1; } elsif (m/^\s*(t(\*|\d+)):\s+(\d+):\s+(\d+)\s+\[\s*(\d+):\s+(\d+)\]$/) { if ($stack eq "") { # Still in the header, so this is just a per-thread summary. next; } my $thread = $2; my ($n1, $s1, $n2, $s2) = ($3, $4, $5, $6); my @counts = AdjustSamples($sample_adjustment, $sampling_algorithm, $n1, $s1, $n2, $s2); if ($thread eq "*") { AddEntries($profile, $pcs, FixCallerAddresses($stack), $counts[$index]); } else { if (!exists($thread_profiles->{$thread})) { $thread_profiles->{$thread} = {}; } AddEntries($thread_profiles->{$thread}, $pcs, FixCallerAddresses($stack), $counts[$index]); } } } my $r = {}; $r->{version} = "heap"; $r->{period} = 1; $r->{profile} = $profile; $r->{threads} = $thread_profiles; $r->{libs} = ParseLibraries($prog, $map, $pcs); $r->{pcs} = $pcs; return $r; } sub ReadSynchProfile { my $prog = shift; local *PROFILE = shift; my $header = shift; my $map = ''; my $profile = {}; my $pcs = {}; my $sampling_period = 1; my $cyclespernanosec = 2.8; # Default assumption for old binaries my $seen_clockrate = 0; my $line; my $index = 0; if ($main::opt_total_delay) { $index = 0; } elsif ($main::opt_contentions) { $index = 1; } elsif ($main::opt_mean_delay) { $index = 2; } while ( $line = ) { $line =~ s/\r//g; # turn windows-looking lines into unix-looking lines if ( $line =~ /^\s*(\d+)\s+(\d+) \@\s*(.*?)\s*$/ ) { my ($cycles, $count, $stack) = ($1, $2, $3); # Convert cycles to nanoseconds $cycles /= $cyclespernanosec; # Adjust for sampling done by application $cycles *= $sampling_period; $count *= $sampling_period; my @values = ($cycles, $count, $cycles / $count); AddEntries($profile, $pcs, FixCallerAddresses($stack), $values[$index]); } elsif ( $line =~ /^(slow release).*thread \d+ \@\s*(.*?)\s*$/ || $line =~ /^\s*(\d+) \@\s*(.*?)\s*$/ ) { my ($cycles, $stack) = ($1, $2); if ($cycles !~ /^\d+$/) { next; } # Convert cycles to nanoseconds $cycles /= $cyclespernanosec; # Adjust for sampling done by application $cycles *= $sampling_period; AddEntries($profile, $pcs, FixCallerAddresses($stack), $cycles); } elsif ( $line =~ m/^([a-z][^=]*)=(.*)$/ ) { my ($variable, $value) = ($1,$2); for ($variable, $value) { s/^\s+//; s/\s+$//; } if ($variable eq "cycles/second") { $cyclespernanosec = $value / 1e9; $seen_clockrate = 1; } elsif ($variable eq "sampling period") { $sampling_period = $value; } elsif ($variable eq "ms since reset") { # Currently nothing is done with this value in jeprof # So we just silently ignore it for now } elsif ($variable eq "discarded samples") { # Currently nothing is done with this value in jeprof # So we just silently ignore it for now } else { printf STDERR ("Ignoring unnknown variable in /contention output: " . "'%s' = '%s'\n",$variable,$value); } } else { # Memory map entry $map .= $line; } } if (!$seen_clockrate) { printf STDERR ("No cycles/second entry in profile; Guessing %.1f GHz\n", $cyclespernanosec); } my $r = {}; $r->{version} = 0; $r->{period} = $sampling_period; $r->{profile} = $profile; $r->{libs} = ParseLibraries($prog, $map, $pcs); $r->{pcs} = $pcs; return $r; } # Given a hex value in the form "0x1abcd" or "1abcd", return either # "0001abcd" or "000000000001abcd", depending on the current (global) # address length. sub HexExtend { my $addr = shift; $addr =~ s/^(0x)?0*//; my $zeros_needed = $address_length - length($addr); if ($zeros_needed < 0) { printf STDERR "Warning: address $addr is longer than address length $address_length\n"; return $addr; } return ("0" x $zeros_needed) . $addr; } ##### Symbol extraction ##### # Aggressively search the lib_prefix values for the given library # If all else fails, just return the name of the library unmodified. # If the lib_prefix is "/my/path,/other/path" and $file is "/lib/dir/mylib.so" # it will search the following locations in this order, until it finds a file: # /my/path/lib/dir/mylib.so # /other/path/lib/dir/mylib.so # /my/path/dir/mylib.so # /other/path/dir/mylib.so # /my/path/mylib.so # /other/path/mylib.so # /lib/dir/mylib.so (returned as last resort) sub FindLibrary { my $file = shift; my $suffix = $file; # Search for the library as described above do { foreach my $prefix (@prefix_list) { my $fullpath = $prefix . $suffix; if (-e $fullpath) { return $fullpath; } } } while ($suffix =~ s|^/[^/]+/|/|); return $file; } # Return path to library with debugging symbols. # For libc libraries, the copy in /usr/lib/debug contains debugging symbols sub DebuggingLibrary { my $file = shift; if ($file !~ m|^/|) { return undef; } # Find debug symbol file if it's named after the library's name. if (-f "/usr/lib/debug$file") { if($main::opt_debug) { print STDERR "found debug info for $file in /usr/lib/debug$file\n"; } return "/usr/lib/debug$file"; } elsif (-f "/usr/lib/debug$file.debug") { if($main::opt_debug) { print STDERR "found debug info for $file in /usr/lib/debug$file.debug\n"; } return "/usr/lib/debug$file.debug"; } if(!$main::opt_debug_syms_by_id) { if($main::opt_debug) { print STDERR "no debug symbols found for $file\n" }; return undef; } # Find debug file if it's named after the library's build ID. my $readelf = ''; if (!$main::gave_up_on_elfutils) { $readelf = qx/eu-readelf -n ${file}/; if ($?) { print STDERR "Cannot run eu-readelf. To use --debug-syms-by-id you must be on Linux, with elfutils installed.\n"; $main::gave_up_on_elfutils = 1; return undef; } my $buildID = $1 if $readelf =~ /Build ID: ([A-Fa-f0-9]+)/s; if (defined $buildID && length $buildID > 0) { my $symbolFile = '/usr/lib/debug/.build-id/' . substr($buildID, 0, 2) . '/' . substr($buildID, 2) . '.debug'; if (-e $symbolFile) { if($main::opt_debug) { print STDERR "found debug symbol file $symbolFile for $file\n" }; return $symbolFile; } else { if($main::opt_debug) { print STDERR "no debug symbol file found for $file, build ID: $buildID\n" }; return undef; } } } if($main::opt_debug) { print STDERR "no debug symbols found for $file, build ID unknown\n" }; return undef; } # Parse text section header of a library using objdump sub ParseTextSectionHeaderFromObjdump { my $lib = shift; my $size = undef; my $vma; my $file_offset; # Get objdump output from the library file to figure out how to # map between mapped addresses and addresses in the library. my $cmd = ShellEscape($obj_tool_map{"objdump"}, "-h", $lib); open(OBJDUMP, "$cmd |") || error("$cmd: $!\n"); while () { s/\r//g; # turn windows-looking lines into unix-looking lines # Idx Name Size VMA LMA File off Algn # 10 .text 00104b2c 420156f0 420156f0 000156f0 2**4 # For 64-bit objects, VMA and LMA will be 16 hex digits, size and file # offset may still be 8. But AddressSub below will still handle that. my @x = split; if (($#x >= 6) && ($x[1] eq '.text')) { $size = $x[2]; $vma = $x[3]; $file_offset = $x[5]; last; } } close(OBJDUMP); if (!defined($size)) { return undef; } my $r = {}; $r->{size} = $size; $r->{vma} = $vma; $r->{file_offset} = $file_offset; return $r; } # Parse text section header of a library using otool (on OS X) sub ParseTextSectionHeaderFromOtool { my $lib = shift; my $size = undef; my $vma = undef; my $file_offset = undef; # Get otool output from the library file to figure out how to # map between mapped addresses and addresses in the library. my $command = ShellEscape($obj_tool_map{"otool"}, "-l", $lib); open(OTOOL, "$command |") || error("$command: $!\n"); my $cmd = ""; my $sectname = ""; my $segname = ""; foreach my $line () { $line =~ s/\r//g; # turn windows-looking lines into unix-looking lines # Load command <#> # cmd LC_SEGMENT # [...] # Section # sectname __text # segname __TEXT # addr 0x000009f8 # size 0x00018b9e # offset 2552 # align 2^2 (4) # We will need to strip off the leading 0x from the hex addresses, # and convert the offset into hex. if ($line =~ /Load command/) { $cmd = ""; $sectname = ""; $segname = ""; } elsif ($line =~ /Section/) { $sectname = ""; $segname = ""; } elsif ($line =~ /cmd (\w+)/) { $cmd = $1; } elsif ($line =~ /sectname (\w+)/) { $sectname = $1; } elsif ($line =~ /segname (\w+)/) { $segname = $1; } elsif (!(($cmd eq "LC_SEGMENT" || $cmd eq "LC_SEGMENT_64") && $sectname eq "__text" && $segname eq "__TEXT")) { next; } elsif ($line =~ /\baddr 0x([0-9a-fA-F]+)/) { $vma = $1; } elsif ($line =~ /\bsize 0x([0-9a-fA-F]+)/) { $size = $1; } elsif ($line =~ /\boffset ([0-9]+)/) { $file_offset = sprintf("%016x", $1); } if (defined($vma) && defined($size) && defined($file_offset)) { last; } } close(OTOOL); if (!defined($vma) || !defined($size) || !defined($file_offset)) { return undef; } my $r = {}; $r->{size} = $size; $r->{vma} = $vma; $r->{file_offset} = $file_offset; return $r; } sub ParseTextSectionHeader { # obj_tool_map("otool") is only defined if we're in a Mach-O environment if (defined($obj_tool_map{"otool"})) { my $r = ParseTextSectionHeaderFromOtool(@_); if (defined($r)){ return $r; } } # If otool doesn't work, or we don't have it, fall back to objdump return ParseTextSectionHeaderFromObjdump(@_); } # Split /proc/pid/maps dump into a list of libraries sub ParseLibraries { return if $main::use_symbol_page; # We don't need libraries info. my $prog = Cwd::abs_path(shift); my $map = shift; my $pcs = shift; my $result = []; my $h = "[a-f0-9]+"; my $zero_offset = HexExtend("0"); my $buildvar = ""; foreach my $l (split("\n", $map)) { if ($l =~ m/^\s*build=(.*)$/) { $buildvar = $1; } my $start; my $finish; my $offset; my $lib; if ($l =~ /^($h)-($h)\s+..x.\s+($h)\s+\S+:\S+\s+\d+\s+(\S+\.(so|dll|dylib|bundle)((\.\d+)+\w*(\.\d+){0,3})?)$/i) { # Full line from /proc/self/maps. Example: # 40000000-40015000 r-xp 00000000 03:01 12845071 /lib/ld-2.3.2.so $start = HexExtend($1); $finish = HexExtend($2); $offset = HexExtend($3); $lib = $4; $lib =~ s|\\|/|g; # turn windows-style paths into unix-style paths } elsif ($l =~ /^\s*($h)-($h):\s*(\S+\.so(\.\d+)*)/) { # Cooked line from DumpAddressMap. Example: # 40000000-40015000: /lib/ld-2.3.2.so $start = HexExtend($1); $finish = HexExtend($2); $offset = $zero_offset; $lib = $3; } elsif (($l =~ /^($h)-($h)\s+..x.\s+($h)\s+\S+:\S+\s+\d+\s+(\S+)$/i) && ($4 eq $prog)) { # PIEs and address space randomization do not play well with our # default assumption that main executable is at lowest # addresses. So we're detecting main executable in # /proc/self/maps as well. $start = HexExtend($1); $finish = HexExtend($2); $offset = HexExtend($3); $lib = $4; $lib =~ s|\\|/|g; # turn windows-style paths into unix-style paths } # FreeBSD 10.0 virtual memory map /proc/curproc/map as defined in # function procfs_doprocmap (sys/fs/procfs/procfs_map.c) # # Example: # 0x800600000 0x80061a000 26 0 0xfffff800035a0000 r-x 75 33 0x1004 COW NC vnode /libexec/ld-elf.s # o.1 NCH -1 elsif ($l =~ /^(0x$h)\s(0x$h)\s\d+\s\d+\s0x$h\sr-x\s\d+\s\d+\s0x\d+\s(COW|NCO)\s(NC|NNC)\svnode\s(\S+\.so(\.\d+)*)/) { $start = HexExtend($1); $finish = HexExtend($2); $offset = $zero_offset; $lib = FindLibrary($5); } else { next; } # Expand "$build" variable if available $lib =~ s/\$build\b/$buildvar/g; $lib = FindLibrary($lib); # Check for pre-relocated libraries, which use pre-relocated symbol tables # and thus require adjusting the offset that we'll use to translate # VM addresses into symbol table addresses. # Only do this if we're not going to fetch the symbol table from a # debugging copy of the library. if (!DebuggingLibrary($lib)) { my $text = ParseTextSectionHeader($lib); if (defined($text)) { my $vma_offset = AddressSub($text->{vma}, $text->{file_offset}); $offset = AddressAdd($offset, $vma_offset); } } if($main::opt_debug) { printf STDERR "$start:$finish ($offset) $lib\n"; } push(@{$result}, [$lib, $start, $finish, $offset]); } # Append special entry for additional library (not relocated) if ($main::opt_lib ne "") { my $text = ParseTextSectionHeader($main::opt_lib); if (defined($text)) { my $start = $text->{vma}; my $finish = AddressAdd($start, $text->{size}); push(@{$result}, [$main::opt_lib, $start, $finish, $start]); } } # Append special entry for the main program. This covers # 0..max_pc_value_seen, so that we assume pc values not found in one # of the library ranges will be treated as coming from the main # program binary. my $min_pc = HexExtend("0"); my $max_pc = $min_pc; # find the maximal PC value in any sample foreach my $pc (keys(%{$pcs})) { if (HexExtend($pc) gt $max_pc) { $max_pc = HexExtend($pc); } } push(@{$result}, [$prog, $min_pc, $max_pc, $zero_offset]); return $result; } # Add two hex addresses of length $address_length. # Run jeprof --test for unit test if this is changed. sub AddressAdd { my $addr1 = shift; my $addr2 = shift; my $sum; if ($address_length == 8) { # Perl doesn't cope with wraparound arithmetic, so do it explicitly: $sum = (hex($addr1)+hex($addr2)) % (0x10000000 * 16); return sprintf("%08x", $sum); } else { # Do the addition in 7-nibble chunks to trivialize carry handling. if ($main::opt_debug and $main::opt_test) { print STDERR "AddressAdd $addr1 + $addr2 = "; } my $a1 = substr($addr1,-7); $addr1 = substr($addr1,0,-7); my $a2 = substr($addr2,-7); $addr2 = substr($addr2,0,-7); $sum = hex($a1) + hex($a2); my $c = 0; if ($sum > 0xfffffff) { $c = 1; $sum -= 0x10000000; } my $r = sprintf("%07x", $sum); $a1 = substr($addr1,-7); $addr1 = substr($addr1,0,-7); $a2 = substr($addr2,-7); $addr2 = substr($addr2,0,-7); $sum = hex($a1) + hex($a2) + $c; $c = 0; if ($sum > 0xfffffff) { $c = 1; $sum -= 0x10000000; } $r = sprintf("%07x", $sum) . $r; $sum = hex($addr1) + hex($addr2) + $c; if ($sum > 0xff) { $sum -= 0x100; } $r = sprintf("%02x", $sum) . $r; if ($main::opt_debug and $main::opt_test) { print STDERR "$r\n"; } return $r; } } # Subtract two hex addresses of length $address_length. # Run jeprof --test for unit test if this is changed. sub AddressSub { my $addr1 = shift; my $addr2 = shift; my $diff; if ($address_length == 8) { # Perl doesn't cope with wraparound arithmetic, so do it explicitly: $diff = (hex($addr1)-hex($addr2)) % (0x10000000 * 16); return sprintf("%08x", $diff); } else { # Do the addition in 7-nibble chunks to trivialize borrow handling. # if ($main::opt_debug) { print STDERR "AddressSub $addr1 - $addr2 = "; } my $a1 = hex(substr($addr1,-7)); $addr1 = substr($addr1,0,-7); my $a2 = hex(substr($addr2,-7)); $addr2 = substr($addr2,0,-7); my $b = 0; if ($a2 > $a1) { $b = 1; $a1 += 0x10000000; } $diff = $a1 - $a2; my $r = sprintf("%07x", $diff); $a1 = hex(substr($addr1,-7)); $addr1 = substr($addr1,0,-7); $a2 = hex(substr($addr2,-7)) + $b; $addr2 = substr($addr2,0,-7); $b = 0; if ($a2 > $a1) { $b = 1; $a1 += 0x10000000; } $diff = $a1 - $a2; $r = sprintf("%07x", $diff) . $r; $a1 = hex($addr1); $a2 = hex($addr2) + $b; if ($a2 > $a1) { $a1 += 0x100; } $diff = $a1 - $a2; $r = sprintf("%02x", $diff) . $r; # if ($main::opt_debug) { print STDERR "$r\n"; } return $r; } } # Increment a hex addresses of length $address_length. # Run jeprof --test for unit test if this is changed. sub AddressInc { my $addr = shift; my $sum; if ($address_length == 8) { # Perl doesn't cope with wraparound arithmetic, so do it explicitly: $sum = (hex($addr)+1) % (0x10000000 * 16); return sprintf("%08x", $sum); } else { # Do the addition in 7-nibble chunks to trivialize carry handling. # We are always doing this to step through the addresses in a function, # and will almost never overflow the first chunk, so we check for this # case and exit early. # if ($main::opt_debug) { print STDERR "AddressInc $addr1 = "; } my $a1 = substr($addr,-7); $addr = substr($addr,0,-7); $sum = hex($a1) + 1; my $r = sprintf("%07x", $sum); if ($sum <= 0xfffffff) { $r = $addr . $r; # if ($main::opt_debug) { print STDERR "$r\n"; } return HexExtend($r); } else { $r = "0000000"; } $a1 = substr($addr,-7); $addr = substr($addr,0,-7); $sum = hex($a1) + 1; $r = sprintf("%07x", $sum) . $r; if ($sum <= 0xfffffff) { $r = $addr . $r; # if ($main::opt_debug) { print STDERR "$r\n"; } return HexExtend($r); } else { $r = "00000000000000"; } $sum = hex($addr) + 1; if ($sum > 0xff) { $sum -= 0x100; } $r = sprintf("%02x", $sum) . $r; # if ($main::opt_debug) { print STDERR "$r\n"; } return $r; } } # Extract symbols for all PC values found in profile sub ExtractSymbols { my $libs = shift; my $pcset = shift; my $symbols = {}; # Map each PC value to the containing library. To make this faster, # we sort libraries by their starting pc value (highest first), and # advance through the libraries as we advance the pc. Sometimes the # addresses of libraries may overlap with the addresses of the main # binary, so to make sure the libraries 'win', we iterate over the # libraries in reverse order (which assumes the binary doesn't start # in the middle of a library, which seems a fair assumption). my @pcs = (sort { $a cmp $b } keys(%{$pcset})); # pcset is 0-extended strings foreach my $lib (sort {$b->[1] cmp $a->[1]} @{$libs}) { my $libname = $lib->[0]; my $start = $lib->[1]; my $finish = $lib->[2]; my $offset = $lib->[3]; # Use debug library if it exists my $debug_libname = DebuggingLibrary($libname); if ($debug_libname) { $libname = $debug_libname; } # Get list of pcs that belong in this library. my $contained = []; my ($start_pc_index, $finish_pc_index); # Find smallest finish_pc_index such that $finish < $pc[$finish_pc_index]. for ($finish_pc_index = $#pcs + 1; $finish_pc_index > 0; $finish_pc_index--) { last if $pcs[$finish_pc_index - 1] le $finish; } # Find smallest start_pc_index such that $start <= $pc[$start_pc_index]. for ($start_pc_index = $finish_pc_index; $start_pc_index > 0; $start_pc_index--) { last if $pcs[$start_pc_index - 1] lt $start; } # This keeps PC values higher than $pc[$finish_pc_index] in @pcs, # in case there are overlaps in libraries and the main binary. @{$contained} = splice(@pcs, $start_pc_index, $finish_pc_index - $start_pc_index); # Map to symbols MapToSymbols($libname, AddressSub($start, $offset), $contained, $symbols); } return $symbols; } # Map list of PC values to symbols for a given image sub MapToSymbols { my $image = shift; my $offset = shift; my $pclist = shift; my $symbols = shift; my $debug = 0; # Ignore empty binaries if ($#{$pclist} < 0) { return; } # Figure out the addr2line command to use my $addr2line = $obj_tool_map{"addr2line"}; my $cmd = ShellEscape($addr2line, "-f", "-C", "-e", $image); if (exists $obj_tool_map{"addr2line_pdb"}) { $addr2line = $obj_tool_map{"addr2line_pdb"}; $cmd = ShellEscape($addr2line, "--demangle", "-f", "-C", "-e", $image); } # If "addr2line" isn't installed on the system at all, just use # nm to get what info we can (function names, but not line numbers). if (system(ShellEscape($addr2line, "--help") . " >$dev_null 2>&1") != 0) { MapSymbolsWithNM($image, $offset, $pclist, $symbols); return; } # "addr2line -i" can produce a variable number of lines per input # address, with no separator that allows us to tell when data for # the next address starts. So we find the address for a special # symbol (_fini) and interleave this address between all real # addresses passed to addr2line. The name of this special symbol # can then be used as a separator. $sep_address = undef; # May be filled in by MapSymbolsWithNM() my $nm_symbols = {}; MapSymbolsWithNM($image, $offset, $pclist, $nm_symbols); if (defined($sep_address)) { # Only add " -i" to addr2line if the binary supports it. # addr2line --help returns 0, but not if it sees an unknown flag first. if (system("$cmd -i --help >$dev_null 2>&1") == 0) { $cmd .= " -i"; } else { $sep_address = undef; # no need for sep_address if we don't support -i } } # Make file with all PC values with intervening 'sep_address' so # that we can reliably detect the end of inlined function list open(ADDRESSES, ">$main::tmpfile_sym") || error("$main::tmpfile_sym: $!\n"); if ($debug) { print("---- $image ---\n"); } for (my $i = 0; $i <= $#{$pclist}; $i++) { # addr2line always reads hex addresses, and does not need '0x' prefix. if ($debug) { printf STDERR ("%s\n", $pclist->[$i]); } printf ADDRESSES ("%s\n", AddressSub($pclist->[$i], $offset)); if (defined($sep_address)) { printf ADDRESSES ("%s\n", $sep_address); } } close(ADDRESSES); if ($debug) { print("----\n"); system("cat", $main::tmpfile_sym); print("----\n"); system("$cmd < " . ShellEscape($main::tmpfile_sym)); print("----\n"); } open(SYMBOLS, "$cmd <" . ShellEscape($main::tmpfile_sym) . " |") || error("$cmd: $!\n"); my $count = 0; # Index in pclist while () { # Read fullfunction and filelineinfo from next pair of lines s/\r?\n$//g; my $fullfunction = $_; $_ = ; s/\r?\n$//g; my $filelinenum = $_; if (defined($sep_address) && $fullfunction eq $sep_symbol) { # Terminating marker for data for this address $count++; next; } $filelinenum =~ s|\\|/|g; # turn windows-style paths into unix-style paths my $pcstr = $pclist->[$count]; my $function = ShortFunctionName($fullfunction); my $nms = $nm_symbols->{$pcstr}; if (defined($nms)) { if ($fullfunction eq '??') { # nm found a symbol for us. $function = $nms->[0]; $fullfunction = $nms->[2]; } else { # MapSymbolsWithNM tags each routine with its starting address, # useful in case the image has multiple occurrences of this # routine. (It uses a syntax that resembles template parameters, # that are automatically stripped out by ShortFunctionName().) # addr2line does not provide the same information. So we check # if nm disambiguated our symbol, and if so take the annotated # (nm) version of the routine-name. TODO(csilvers): this won't # catch overloaded, inlined symbols, which nm doesn't see. # Better would be to do a check similar to nm's, in this fn. if ($nms->[2] =~ m/^\Q$function\E/) { # sanity check it's the right fn $function = $nms->[0]; $fullfunction = $nms->[2]; } } } # Prepend to accumulated symbols for pcstr # (so that caller comes before callee) my $sym = $symbols->{$pcstr}; if (!defined($sym)) { $sym = []; $symbols->{$pcstr} = $sym; } unshift(@{$sym}, $function, $filelinenum, $fullfunction); if ($debug) { printf STDERR ("%s => [%s]\n", $pcstr, join(" ", @{$sym})); } if (!defined($sep_address)) { # Inlining is off, so this entry ends immediately $count++; } } close(SYMBOLS); } # Use nm to map the list of referenced PCs to symbols. Return true iff we # are able to read procedure information via nm. sub MapSymbolsWithNM { my $image = shift; my $offset = shift; my $pclist = shift; my $symbols = shift; # Get nm output sorted by increasing address my $symbol_table = GetProcedureBoundaries($image, "."); if (!%{$symbol_table}) { return 0; } # Start addresses are already the right length (8 or 16 hex digits). my @names = sort { $symbol_table->{$a}->[0] cmp $symbol_table->{$b}->[0] } keys(%{$symbol_table}); if ($#names < 0) { # No symbols: just use addresses foreach my $pc (@{$pclist}) { my $pcstr = "0x" . $pc; $symbols->{$pc} = [$pcstr, "?", $pcstr]; } return 0; } # Sort addresses so we can do a join against nm output my $index = 0; my $fullname = $names[0]; my $name = ShortFunctionName($fullname); foreach my $pc (sort { $a cmp $b } @{$pclist}) { # Adjust for mapped offset my $mpc = AddressSub($pc, $offset); while (($index < $#names) && ($mpc ge $symbol_table->{$fullname}->[1])){ $index++; $fullname = $names[$index]; $name = ShortFunctionName($fullname); } if ($mpc lt $symbol_table->{$fullname}->[1]) { $symbols->{$pc} = [$name, "?", $fullname]; } else { my $pcstr = "0x" . $pc; $symbols->{$pc} = [$pcstr, "?", $pcstr]; } } return 1; } sub ShortFunctionName { my $function = shift; while ($function =~ s/\([^()]*\)(\s*const)?//g) { } # Argument types while ($function =~ s/<[^<>]*>//g) { } # Remove template arguments $function =~ s/^.*\s+(\w+::)/$1/; # Remove leading type return $function; } # Trim overly long symbols found in disassembler output sub CleanDisassembly { my $d = shift; while ($d =~ s/\([^()%]*\)(\s*const)?//g) { } # Argument types, not (%rax) while ($d =~ s/(\w+)<[^<>]*>/$1/g) { } # Remove template arguments return $d; } # Clean file name for display sub CleanFileName { my ($f) = @_; $f =~ s|^/proc/self/cwd/||; $f =~ s|^\./||; return $f; } # Make address relative to section and clean up for display sub UnparseAddress { my ($offset, $address) = @_; $address = AddressSub($address, $offset); $address =~ s/^0x//; $address =~ s/^0*//; return $address; } ##### Miscellaneous ##### # Find the right versions of the above object tools to use. The # argument is the program file being analyzed, and should be an ELF # 32-bit or ELF 64-bit executable file. The location of the tools # is determined by considering the following options in this order: # 1) --tools option, if set # 2) JEPROF_TOOLS environment variable, if set # 3) the environment sub ConfigureObjTools { my $prog_file = shift; # Check for the existence of $prog_file because /usr/bin/file does not # predictably return error status in prod. (-e $prog_file) || error("$prog_file does not exist.\n"); my $file_type = undef; if (-e "/usr/bin/file") { # Follow symlinks (at least for systems where "file" supports that). my $escaped_prog_file = ShellEscape($prog_file); $file_type = `/usr/bin/file -L $escaped_prog_file 2>$dev_null || /usr/bin/file $escaped_prog_file`; } elsif ($^O == "MSWin32") { $file_type = "MS Windows"; } else { print STDERR "WARNING: Can't determine the file type of $prog_file"; } if ($file_type =~ /64-bit/) { # Change $address_length to 16 if the program file is ELF 64-bit. # We can't detect this from many (most?) heap or lock contention # profiles, since the actual addresses referenced are generally in low # memory even for 64-bit programs. $address_length = 16; } if ($file_type =~ /MS Windows/) { # For windows, we provide a version of nm and addr2line as part of # the opensource release, which is capable of parsing # Windows-style PDB executables. It should live in the path, or # in the same directory as jeprof. $obj_tool_map{"nm_pdb"} = "nm-pdb"; $obj_tool_map{"addr2line_pdb"} = "addr2line-pdb"; } if ($file_type =~ /Mach-O/) { # OS X uses otool to examine Mach-O files, rather than objdump. $obj_tool_map{"otool"} = "otool"; $obj_tool_map{"addr2line"} = "false"; # no addr2line $obj_tool_map{"objdump"} = "false"; # no objdump } # Go fill in %obj_tool_map with the pathnames to use: foreach my $tool (keys %obj_tool_map) { $obj_tool_map{$tool} = ConfigureTool($obj_tool_map{$tool}); } } # Returns the path of a caller-specified object tool. If --tools or # JEPROF_TOOLS are specified, then returns the full path to the tool # with that prefix. Otherwise, returns the path unmodified (which # means we will look for it on PATH). sub ConfigureTool { my $tool = shift; my $path; # --tools (or $JEPROF_TOOLS) is a comma separated list, where each # item is either a) a pathname prefix, or b) a map of the form # :. First we look for an entry of type (b) for our # tool. If one is found, we use it. Otherwise, we consider all the # pathname prefixes in turn, until one yields an existing file. If # none does, we use a default path. my $tools = $main::opt_tools || $ENV{"JEPROF_TOOLS"} || ""; if ($tools =~ m/(,|^)\Q$tool\E:([^,]*)/) { $path = $2; # TODO(csilvers): sanity-check that $path exists? Hard if it's relative. } elsif ($tools ne '') { foreach my $prefix (split(',', $tools)) { next if ($prefix =~ /:/); # ignore "tool:fullpath" entries in the list if (-x $prefix . $tool) { $path = $prefix . $tool; last; } } if (!$path) { error("No '$tool' found with prefix specified by " . "--tools (or \$JEPROF_TOOLS) '$tools'\n"); } } else { # ... otherwise use the version that exists in the same directory as # jeprof. If there's nothing there, use $PATH. $0 =~ m,[^/]*$,; # this is everything after the last slash my $dirname = $`; # this is everything up to and including the last slash if (-x "$dirname$tool") { $path = "$dirname$tool"; } else { $path = $tool; } } if ($main::opt_debug) { print STDERR "Using '$path' for '$tool'.\n"; } return $path; } sub ShellEscape { my @escaped_words = (); foreach my $word (@_) { my $escaped_word = $word; if ($word =~ m![^a-zA-Z0-9/.,_=-]!) { # check for anything not in whitelist $escaped_word =~ s/'/'\\''/; $escaped_word = "'$escaped_word'"; } push(@escaped_words, $escaped_word); } return join(" ", @escaped_words); } sub cleanup { unlink($main::tmpfile_sym); unlink(keys %main::tempnames); if ((scalar(@main::profile_files) > 0) && defined($main::collected_profile)) { my @profiles = split(" \\\n ", $main::collected_profile); foreach my $profile (@profiles) { unlink($profile); } } } sub sighandler { cleanup(); exit(1); } sub error { my $msg = shift; print STDERR $msg; cleanup(); exit(1); } # Run $nm_command and get all the resulting procedure boundaries whose # names match "$regexp" and returns them in a hashtable mapping from # procedure name to a two-element vector of [start address, end address] sub GetProcedureBoundariesViaNm { my $escaped_nm_command = shift; # shell-escaped my $regexp = shift; my $symbol_table = {}; open(NM, "$escaped_nm_command |") || error("$escaped_nm_command: $!\n"); my $last_start = "0"; my $routine = ""; while () { s/\r//g; # turn windows-looking lines into unix-looking lines if (m/^\s*([0-9a-f]+) (.) (..*)/) { my $start_val = $1; my $type = $2; my $this_routine = $3; # It's possible for two symbols to share the same address, if # one is a zero-length variable (like __start_google_malloc) or # one symbol is a weak alias to another (like __libc_malloc). # In such cases, we want to ignore all values except for the # actual symbol, which in nm-speak has type "T". The logic # below does this, though it's a bit tricky: what happens when # we have a series of lines with the same address, is the first # one gets queued up to be processed. However, it won't # *actually* be processed until later, when we read a line with # a different address. That means that as long as we're reading # lines with the same address, we have a chance to replace that # item in the queue, which we do whenever we see a 'T' entry -- # that is, a line with type 'T'. If we never see a 'T' entry, # we'll just go ahead and process the first entry (which never # got touched in the queue), and ignore the others. if ($start_val eq $last_start && $type =~ /t/i) { # We are the 'T' symbol at this address, replace previous symbol. $routine = $this_routine; next; } elsif ($start_val eq $last_start) { # We're not the 'T' symbol at this address, so ignore us. next; } if ($this_routine eq $sep_symbol) { $sep_address = HexExtend($start_val); } # Tag this routine with the starting address in case the image # has multiple occurrences of this routine. We use a syntax # that resembles template parameters that are automatically # stripped out by ShortFunctionName() $this_routine .= "<$start_val>"; if (defined($routine) && $routine =~ m/$regexp/) { $symbol_table->{$routine} = [HexExtend($last_start), HexExtend($start_val)]; } $last_start = $start_val; $routine = $this_routine; } elsif (m/^Loaded image name: (.+)/) { # The win32 nm workalike emits information about the binary it is using. if ($main::opt_debug) { print STDERR "Using Image $1\n"; } } elsif (m/^PDB file name: (.+)/) { # The win32 nm workalike emits information about the pdb it is using. if ($main::opt_debug) { print STDERR "Using PDB $1\n"; } } } close(NM); # Handle the last line in the nm output. Unfortunately, we don't know # how big this last symbol is, because we don't know how big the file # is. For now, we just give it a size of 0. # TODO(csilvers): do better here. if (defined($routine) && $routine =~ m/$regexp/) { $symbol_table->{$routine} = [HexExtend($last_start), HexExtend($last_start)]; } return $symbol_table; } # Gets the procedure boundaries for all routines in "$image" whose names # match "$regexp" and returns them in a hashtable mapping from procedure # name to a two-element vector of [start address, end address]. # Will return an empty map if nm is not installed or not working properly. sub GetProcedureBoundaries { my $image = shift; my $regexp = shift; # If $image doesn't start with /, then put ./ in front of it. This works # around an obnoxious bug in our probing of nm -f behavior. # "nm -f $image" is supposed to fail on GNU nm, but if: # # a. $image starts with [BbSsPp] (for example, bin/foo/bar), AND # b. you have a.out in your current directory (a not uncommon occurrence) # # then "nm -f $image" succeeds because -f only looks at the first letter of # the argument, which looks valid because it's [BbSsPp], and then since # there's no image provided, it looks for a.out and finds it. # # This regex makes sure that $image starts with . or /, forcing the -f # parsing to fail since . and / are not valid formats. $image =~ s#^[^/]#./$&#; # For libc libraries, the copy in /usr/lib/debug contains debugging symbols my $debugging = DebuggingLibrary($image); if ($debugging) { $image = $debugging; } my $nm = $obj_tool_map{"nm"}; my $cppfilt = $obj_tool_map{"c++filt"}; # nm can fail for two reasons: 1) $image isn't a debug library; 2) nm # binary doesn't support --demangle. In addition, for OS X we need # to use the -f flag to get 'flat' nm output (otherwise we don't sort # properly and get incorrect results). Unfortunately, GNU nm uses -f # in an incompatible way. So first we test whether our nm supports # --demangle and -f. my $demangle_flag = ""; my $cppfilt_flag = ""; my $to_devnull = ">$dev_null 2>&1"; if (system(ShellEscape($nm, "--demangle", $image) . $to_devnull) == 0) { # In this mode, we do "nm --demangle " $demangle_flag = "--demangle"; $cppfilt_flag = ""; } elsif (system(ShellEscape($cppfilt, $image) . $to_devnull) == 0) { # In this mode, we do "nm | c++filt" $cppfilt_flag = " | " . ShellEscape($cppfilt); }; my $flatten_flag = ""; if (system(ShellEscape($nm, "-f", $image) . $to_devnull) == 0) { $flatten_flag = "-f"; } # Finally, in the case $imagie isn't a debug library, we try again with # -D to at least get *exported* symbols. If we can't use --demangle, # we use c++filt instead, if it exists on this system. my @nm_commands = (ShellEscape($nm, "-n", $flatten_flag, $demangle_flag, $image) . " 2>$dev_null $cppfilt_flag", ShellEscape($nm, "-D", "-n", $flatten_flag, $demangle_flag, $image) . " 2>$dev_null $cppfilt_flag", # 6nm is for Go binaries ShellEscape("6nm", "$image") . " 2>$dev_null | sort", ); # If the executable is an MS Windows PDB-format executable, we'll # have set up obj_tool_map("nm_pdb"). In this case, we actually # want to use both unix nm and windows-specific nm_pdb, since # PDB-format executables can apparently include dwarf .o files. if (exists $obj_tool_map{"nm_pdb"}) { push(@nm_commands, ShellEscape($obj_tool_map{"nm_pdb"}, "--demangle", $image) . " 2>$dev_null"); } foreach my $nm_command (@nm_commands) { my $symbol_table = GetProcedureBoundariesViaNm($nm_command, $regexp); return $symbol_table if (%{$symbol_table}); } my $symbol_table = {}; return $symbol_table; } # The test vectors for AddressAdd/Sub/Inc are 8-16-nibble hex strings. # To make them more readable, we add underscores at interesting places. # This routine removes the underscores, producing the canonical representation # used by jeprof to represent addresses, particularly in the tested routines. sub CanonicalHex { my $arg = shift; return join '', (split '_',$arg); } # Unit test for AddressAdd: sub AddressAddUnitTest { my $test_data_8 = shift; my $test_data_16 = shift; my $error_count = 0; my $fail_count = 0; my $pass_count = 0; # print STDERR "AddressAddUnitTest: ", 1+$#{$test_data_8}, " tests\n"; # First a few 8-nibble addresses. Note that this implementation uses # plain old arithmetic, so a quick sanity check along with verifying what # happens to overflow (we want it to wrap): $address_length = 8; foreach my $row (@{$test_data_8}) { if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } my $sum = AddressAdd ($row->[0], $row->[1]); if ($sum ne $row->[2]) { printf STDERR "ERROR: %s != %s + %s = %s\n", $sum, $row->[0], $row->[1], $row->[2]; ++$fail_count; } else { ++$pass_count; } } printf STDERR "AddressAdd 32-bit tests: %d passes, %d failures\n", $pass_count, $fail_count; $error_count = $fail_count; $fail_count = 0; $pass_count = 0; # Now 16-nibble addresses. $address_length = 16; foreach my $row (@{$test_data_16}) { if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } my $sum = AddressAdd (CanonicalHex($row->[0]), CanonicalHex($row->[1])); my $expected = join '', (split '_',$row->[2]); if ($sum ne CanonicalHex($row->[2])) { printf STDERR "ERROR: %s != %s + %s = %s\n", $sum, $row->[0], $row->[1], $row->[2]; ++$fail_count; } else { ++$pass_count; } } printf STDERR "AddressAdd 64-bit tests: %d passes, %d failures\n", $pass_count, $fail_count; $error_count += $fail_count; return $error_count; } # Unit test for AddressSub: sub AddressSubUnitTest { my $test_data_8 = shift; my $test_data_16 = shift; my $error_count = 0; my $fail_count = 0; my $pass_count = 0; # print STDERR "AddressSubUnitTest: ", 1+$#{$test_data_8}, " tests\n"; # First a few 8-nibble addresses. Note that this implementation uses # plain old arithmetic, so a quick sanity check along with verifying what # happens to overflow (we want it to wrap): $address_length = 8; foreach my $row (@{$test_data_8}) { if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } my $sum = AddressSub ($row->[0], $row->[1]); if ($sum ne $row->[3]) { printf STDERR "ERROR: %s != %s - %s = %s\n", $sum, $row->[0], $row->[1], $row->[3]; ++$fail_count; } else { ++$pass_count; } } printf STDERR "AddressSub 32-bit tests: %d passes, %d failures\n", $pass_count, $fail_count; $error_count = $fail_count; $fail_count = 0; $pass_count = 0; # Now 16-nibble addresses. $address_length = 16; foreach my $row (@{$test_data_16}) { if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } my $sum = AddressSub (CanonicalHex($row->[0]), CanonicalHex($row->[1])); if ($sum ne CanonicalHex($row->[3])) { printf STDERR "ERROR: %s != %s - %s = %s\n", $sum, $row->[0], $row->[1], $row->[3]; ++$fail_count; } else { ++$pass_count; } } printf STDERR "AddressSub 64-bit tests: %d passes, %d failures\n", $pass_count, $fail_count; $error_count += $fail_count; return $error_count; } # Unit test for AddressInc: sub AddressIncUnitTest { my $test_data_8 = shift; my $test_data_16 = shift; my $error_count = 0; my $fail_count = 0; my $pass_count = 0; # print STDERR "AddressIncUnitTest: ", 1+$#{$test_data_8}, " tests\n"; # First a few 8-nibble addresses. Note that this implementation uses # plain old arithmetic, so a quick sanity check along with verifying what # happens to overflow (we want it to wrap): $address_length = 8; foreach my $row (@{$test_data_8}) { if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } my $sum = AddressInc ($row->[0]); if ($sum ne $row->[4]) { printf STDERR "ERROR: %s != %s + 1 = %s\n", $sum, $row->[0], $row->[4]; ++$fail_count; } else { ++$pass_count; } } printf STDERR "AddressInc 32-bit tests: %d passes, %d failures\n", $pass_count, $fail_count; $error_count = $fail_count; $fail_count = 0; $pass_count = 0; # Now 16-nibble addresses. $address_length = 16; foreach my $row (@{$test_data_16}) { if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } my $sum = AddressInc (CanonicalHex($row->[0])); if ($sum ne CanonicalHex($row->[4])) { printf STDERR "ERROR: %s != %s + 1 = %s\n", $sum, $row->[0], $row->[4]; ++$fail_count; } else { ++$pass_count; } } printf STDERR "AddressInc 64-bit tests: %d passes, %d failures\n", $pass_count, $fail_count; $error_count += $fail_count; return $error_count; } # Driver for unit tests. # Currently just the address add/subtract/increment routines for 64-bit. sub RunUnitTests { my $error_count = 0; # This is a list of tuples [a, b, a+b, a-b, a+1] my $unit_test_data_8 = [ [qw(aaaaaaaa 50505050 fafafafa 5a5a5a5a aaaaaaab)], [qw(50505050 aaaaaaaa fafafafa a5a5a5a6 50505051)], [qw(ffffffff aaaaaaaa aaaaaaa9 55555555 00000000)], [qw(00000001 ffffffff 00000000 00000002 00000002)], [qw(00000001 fffffff0 fffffff1 00000011 00000002)], ]; my $unit_test_data_16 = [ # The implementation handles data in 7-nibble chunks, so those are the # interesting boundaries. [qw(aaaaaaaa 50505050 00_000000f_afafafa 00_0000005_a5a5a5a 00_000000a_aaaaaab)], [qw(50505050 aaaaaaaa 00_000000f_afafafa ff_ffffffa_5a5a5a6 00_0000005_0505051)], [qw(ffffffff aaaaaaaa 00_000001a_aaaaaa9 00_0000005_5555555 00_0000010_0000000)], [qw(00000001 ffffffff 00_0000010_0000000 ff_ffffff0_0000002 00_0000000_0000002)], [qw(00000001 fffffff0 00_000000f_ffffff1 ff_ffffff0_0000011 00_0000000_0000002)], [qw(00_a00000a_aaaaaaa 50505050 00_a00000f_afafafa 00_a000005_a5a5a5a 00_a00000a_aaaaaab)], [qw(0f_fff0005_0505050 aaaaaaaa 0f_fff000f_afafafa 0f_ffefffa_5a5a5a6 0f_fff0005_0505051)], [qw(00_000000f_fffffff 01_800000a_aaaaaaa 01_800001a_aaaaaa9 fe_8000005_5555555 00_0000010_0000000)], [qw(00_0000000_0000001 ff_fffffff_fffffff 00_0000000_0000000 00_0000000_0000002 00_0000000_0000002)], [qw(00_0000000_0000001 ff_fffffff_ffffff0 ff_fffffff_ffffff1 00_0000000_0000011 00_0000000_0000002)], ]; $error_count += AddressAddUnitTest($unit_test_data_8, $unit_test_data_16); $error_count += AddressSubUnitTest($unit_test_data_8, $unit_test_data_16); $error_count += AddressIncUnitTest($unit_test_data_8, $unit_test_data_16); if ($error_count > 0) { print STDERR $error_count, " errors: FAILED\n"; } else { print STDERR "PASS\n"; } exit ($error_count); } ================================================ FILE: pkg/apiserver/profiling/model.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package profiling import ( "context" "database/sql/driver" "encoding/json" "fmt" "time" "github.com/joomcode/errorx" "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" "github.com/pingcap/tidb-dashboard/pkg/dbstore" ) // TaskState is used to represent the task/task group state. type TaskState int // Built-in task state. const ( TaskStateError TaskState = iota TaskStateRunning TaskStateFinish TaskStatePartialFinish // Only valid for task group TaskStateSkipped ) type TaskRawDataType string const ( RawDataTypeJeprof TaskRawDataType = "jeprof" RawDataTypeProtobuf TaskRawDataType = "protobuf" RawDataTypeText TaskRawDataType = "text" ) type ( TaskProfilingType string TaskProfilingTypeList []TaskProfilingType ) func (r *TaskProfilingTypeList) Scan(src interface{}) error { return json.Unmarshal([]byte(src.(string)), r) } func (r TaskProfilingTypeList) Value() (driver.Value, error) { val, err := json.Marshal(r) return string(val), err } const ( ProfilingTypeCPU TaskProfilingType = "cpu" ProfilingTypeHeap TaskProfilingType = "heap" ProfilingTypeGoroutine TaskProfilingType = "goroutine" ProfilingTypeMutex TaskProfilingType = "mutex" ) var profilingTypeMap = map[TaskProfilingType]struct{}{ ProfilingTypeCPU: {}, ProfilingTypeHeap: {}, ProfilingTypeGoroutine: {}, ProfilingTypeMutex: {}, } type TaskModel struct { ID uint `json:"id" gorm:"primary_key"` TaskGroupID uint `json:"task_group_id" gorm:"index"` State TaskState `json:"state" gorm:"index"` Target model.RequestTargetNode `json:"target" gorm:"embedded;embedded_prefix:target_"` FilePath string `json:"-" gorm:"type:text"` Error string `json:"error" gorm:"type:text"` StartedAt int64 `json:"started_at"` // The start running time, reset when retry. Used to estimate approximate profiling progress. RawDataType TaskRawDataType `json:"raw_data_type" gorm:"raw_data_type"` ProfilingType TaskProfilingType `json:"profiling_type"` } func (TaskModel) TableName() string { return "profiling_tasks" } type TaskGroupModel struct { ID uint `json:"id" gorm:"primary_key"` State TaskState `json:"state" gorm:"index"` ProfileDurationSecs uint `json:"profile_duration_secs"` TargetStats model.RequestTargetStatistics `json:"target_stats" gorm:"embedded;embedded_prefix:target_stats_"` StartedAt int64 `json:"started_at"` RequstedProfilingTypes TaskProfilingTypeList `json:"requsted_profiling_types"` } func (TaskGroupModel) TableName() string { return "profiling_task_groups" } func autoMigrate(db *dbstore.DB) error { return db.AutoMigrate(&TaskModel{}, &TaskGroupModel{}) } // Task is the unit to fetch profiling information. type Task struct { *TaskModel ctx context.Context cancel context.CancelFunc taskGroup *TaskGroup fetchers *fetchers } // NewTask creates a new profiling task. func NewTask(ctx context.Context, taskGroup *TaskGroup, target model.RequestTargetNode, fts *fetchers, profilingType TaskProfilingType) *Task { ctx, cancel := context.WithCancel(ctx) return &Task{ TaskModel: &TaskModel{ TaskGroupID: taskGroup.ID, State: TaskStateRunning, Target: target, StartedAt: time.Now().Unix(), ProfilingType: profilingType, }, ctx: ctx, cancel: cancel, taskGroup: taskGroup, fetchers: fts, } } func (t *Task) run() { fileNameWithoutExt := fmt.Sprintf("%s_%s", t.ProfilingType, t.Target.FileName()) protoFilePath, rawDataType, err := profileAndWritePprof(t.ctx, t.fetchers, &t.Target, fileNameWithoutExt, t.taskGroup.ProfileDurationSecs, t.ProfilingType) if err != nil { if errorx.IsOfType(err, ErrUnsupportedProfilingType) { t.State = TaskStateSkipped } else { t.Error = err.Error() t.State = TaskStateError } t.taskGroup.db.Save(t.TaskModel) return } t.FilePath = protoFilePath t.State = TaskStateFinish t.RawDataType = rawDataType t.taskGroup.db.Save(t.TaskModel) } func (t *Task) stop() { t.cancel() } // TaskGroup is the collection of tasks. type TaskGroup struct { *TaskGroupModel db *dbstore.DB } // NewTaskGroup create a new profiling task group. func NewTaskGroup(db *dbstore.DB, profileDurationSecs uint, stats model.RequestTargetStatistics, requestedProfilingTypes TaskProfilingTypeList) *TaskGroup { return &TaskGroup{ TaskGroupModel: &TaskGroupModel{ State: TaskStateRunning, ProfileDurationSecs: profileDurationSecs, TargetStats: stats, StartedAt: time.Now().Unix(), RequstedProfilingTypes: requestedProfilingTypes, }, db: db, } } ================================================ FILE: pkg/apiserver/profiling/module.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package profiling import "go.uber.org/fx" var Module = fx.Options(newFetchers, newService) ================================================ FILE: pkg/apiserver/profiling/pprof.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package profiling import ( "fmt" "os" "strconv" "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" ) type pprofOptions struct { duration uint fileNameWithoutExt string target *model.RequestTargetNode fetcher *profileFetcher profilingType TaskProfilingType } func fetchPprof(op *pprofOptions) (string, TaskRawDataType, error) { fetcher := &fetcher{profileFetcher: op.fetcher, target: op.target} tmpPath, rawDataType, err := fetcher.FetchAndWriteToFile(op.duration, op.fileNameWithoutExt, op.profilingType) if err != nil { return "", "", fmt.Errorf("failed to fetch and write to temp file: %v", err) } return tmpPath, rawDataType, nil } type fetcher struct { target *model.RequestTargetNode profileFetcher *profileFetcher } func (f *fetcher) FetchAndWriteToFile(duration uint, fileNameWithoutExt string, profilingType TaskProfilingType) (string, TaskRawDataType, error) { var profilingRawDataType TaskRawDataType var fileExtenstion string secs := strconv.Itoa(int(duration)) var url string switch profilingType { case ProfilingTypeCPU: url = "/debug/pprof/profile?seconds=" + secs profilingRawDataType = RawDataTypeProtobuf fileExtenstion = "*.proto" case ProfilingTypeHeap: url = "/debug/pprof/heap" if f.target.Kind == model.NodeKindTiKV || f.target.Kind == model.NodeKindTiFlash { profilingRawDataType = RawDataTypeJeprof fileExtenstion = "*.prof" } else { profilingRawDataType = RawDataTypeProtobuf fileExtenstion = "*.proto" } case ProfilingTypeGoroutine: // debug=2 causes STW when collecting the stacks. See https://github.com/pingcap/tidb/issues/48695. url = "/debug/pprof/goroutine?debug=1" profilingRawDataType = RawDataTypeText fileExtenstion = "*.txt" case ProfilingTypeMutex: url = "/debug/pprof/mutex?debug=1" profilingRawDataType = RawDataTypeText fileExtenstion = "*.txt" } tmpfile, err := os.CreateTemp("", fileNameWithoutExt+"_"+fileExtenstion) if err != nil { return "", "", fmt.Errorf("failed to create tmpfile to write profile: %v", err) } defer func() { _ = tmpfile.Close() }() resp, err := (*f.profileFetcher).fetch(&fetchOptions{ip: f.target.IP, port: f.target.Port, path: url}) if err != nil { return "", "", fmt.Errorf("failed to fetch profile with %v format: %v", fileExtenstion, err) } _, err = tmpfile.Write(resp) if err != nil { return "", "", fmt.Errorf("failed to write profile: %v", err) } return tmpfile.Name(), profilingRawDataType, nil } ================================================ FILE: pkg/apiserver/profiling/profile.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package profiling import ( "context" "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" ) func profileAndWritePprof(_ context.Context, fts *fetchers, target *model.RequestTargetNode, fileNameWithoutExt string, profileDurationSecs uint, profilingType TaskProfilingType) (string, TaskRawDataType, error) { switch target.Kind { case model.NodeKindTiKV: // TiKV only supports CPU/heap Profiling if profilingType != ProfilingTypeCPU && profilingType != ProfilingTypeHeap { return "", "", ErrUnsupportedProfilingType.NewWithNoMessage() } return fetchPprof(&pprofOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.tikv, profilingType: profilingType}) case model.NodeKindTiFlash: // TiFlash only supports CPU/heap Profiling if profilingType != ProfilingTypeCPU && profilingType != ProfilingTypeHeap { return "", "", ErrUnsupportedProfilingType.NewWithNoMessage() } return fetchPprof(&pprofOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.tiflash, profilingType: profilingType}) case model.NodeKindTiDB: return fetchPprof(&pprofOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.tidb, profilingType: profilingType}) case model.NodeKindPD: return fetchPprof(&pprofOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.pd, profilingType: profilingType}) case model.NodeKindTiCDC: return fetchPprof(&pprofOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.ticdc, profilingType: profilingType}) case model.NodeKindTiProxy: return fetchPprof(&pprofOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.tiproxy, profilingType: profilingType}) case model.NodeKindTSO: return fetchPprof(&pprofOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.tso, profilingType: profilingType}) case model.NodeKindScheduling: return fetchPprof(&pprofOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.scheduling, profilingType: profilingType}) default: return "", "", ErrUnsupportedProfilingTarget.New("%s", target.String()) // nolint: vet } } ================================================ FILE: pkg/apiserver/profiling/protobuf_to_svg.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package profiling import ( "flag" "fmt" "io" "strconv" "time" "github.com/goccy/go-graphviz" "github.com/google/pprof/driver" "github.com/google/pprof/profile" ) func convertProtobufToSVG(content []byte, task TaskModel) ([]byte, error) { dotContent, err := convertProtobufToDot(content, task) if err != nil { return nil, fmt.Errorf("failed to convert protobuf to dot: %v", err) } svgContent, err := convertDotToSVG(dotContent) if err != nil { return nil, fmt.Errorf("failed to convert dot to svg: %v", err) } return svgContent, nil } func convertProtobufToDot(content []byte, _ TaskModel) ([]byte, error) { args := []string{ //nolint:prealloc "-dot", // prevent printing stdout "-output", "dummy", "-seconds", strconv.Itoa(int(1)), } // the addr is required for driver. Pporf but not used here // since we have fetched proto content and just want to convert it to dot address := "" args = append(args, address) f := &flagSet{ FlagSet: flag.NewFlagSet("pprof", flag.PanicOnError), args: args, } protoToDotWriter := &protobufToDotWriter{} if err := driver.PProf(&driver.Options{ Fetch: &dotFetcher{content}, Flagset: f, UI: &blankPprofUI{}, Writer: protoToDotWriter, }); err != nil { return nil, err } return protoToDotWriter.wc.data, nil } func convertDotToSVG(dotContent []byte) ([]byte, error) { g := graphviz.New() graph, err := graphviz.ParseBytes(dotContent) if err != nil { return nil, err } svgWriteCloser := &writeCloser{} if err := g.Render(graph, graphviz.SVG, svgWriteCloser); err != nil { return nil, err } return svgWriteCloser.data, nil } // implement a writer to write content to []byte. type protobufToDotWriter struct { wc *writeCloser } func (w *protobufToDotWriter) Open(_ string) (io.WriteCloser, error) { w.wc = &writeCloser{data: make([]byte, 0)} return w.wc, nil } type writeCloser struct { data []byte } func (wc *writeCloser) Write(p []byte) (n int, err error) { wc.data = make([]byte, len(p)) copy(wc.data, p) return len(p), nil } func (wc *writeCloser) Close() error { return nil } type dotFetcher struct { data []byte } func (f *dotFetcher) Fetch(_ string, _, _ time.Duration) (*profile.Profile, string, error) { profile, err := profile.ParseData(f.data) return profile, "", err } type flagSet struct { *flag.FlagSet args []string } func (f *flagSet) StringList(o, d, c string) *[]*string { return &[]*string{f.String(o, d, c)} } func (f *flagSet) ExtraUsage() string { return "" } func (f *flagSet) Parse(usage func()) []string { f.Usage = usage _ = f.FlagSet.Parse(f.args) return f.Args() } func (f *flagSet) AddExtraUsage(_ string) {} // blankPprofUI is used to eliminate the pprof logs. type blankPprofUI struct{} func (b blankPprofUI) ReadLine(_ string) (string, error) { panic("not support") } func (b blankPprofUI) Print(_ ...interface{}) { } func (b blankPprofUI) PrintErr(_ ...interface{}) { } func (b blankPprofUI) IsTerminal() bool { return false } func (b blankPprofUI) WantBrowser() bool { return false } func (b blankPprofUI) SetAutoComplete(_ func(string) string) { } ================================================ FILE: pkg/apiserver/profiling/router.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package profiling import ( "archive/zip" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "time" "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/pkg/config" "github.com/pingcap/tidb-dashboard/util/rest" ) // Register register the handlers to the service. func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/profiling") endpoint.GET("/group/list", auth.MWAuthRequired(), s.getGroupList) endpoint.POST("/group/start", auth.MWAuthRequired(), s.handleStartGroup) endpoint.GET("/group/detail/:groupId", auth.MWAuthRequired(), s.getGroupDetail) endpoint.POST("/group/cancel/:groupId", auth.MWAuthRequired(), s.handleCancelGroup) endpoint.DELETE("/group/delete/:groupId", auth.MWAuthRequired(), s.deleteGroup) endpoint.GET("/action_token", auth.MWAuthRequired(), s.getActionToken) endpoint.GET("/group/download", s.downloadGroup) endpoint.GET("/single/download", s.downloadSingle) endpoint.GET("/single/view", s.viewSingle) endpoint.GET("/config", auth.MWAuthRequired(), s.getDynamicConfig) endpoint.PUT("/config", auth.MWAuthRequired(), auth.MWRequireWritePriv(), s.setDynamicConfig) } // @ID startProfiling // @Summary Start profiling // @Description Start a profiling task group // @Param req body StartRequest true "profiling request" // @Security JwtAuth // @Success 200 {object} TaskGroupModel "task group" // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse // @Router /profiling/group/start [post] func (s *Service) handleStartGroup(c *gin.Context) { var req StartRequest 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 } if req.DurationSecs == 0 { req.DurationSecs = config.DefaultProfilingAutoCollectionDurationSecs } if req.DurationSecs > config.MaxProfilingAutoCollectionDurationSecs { req.DurationSecs = config.MaxProfilingAutoCollectionDurationSecs } session := &StartRequestSession{ req: req, ch: make(chan struct{}, 1), } s.sessionCh <- session select { case <-session.ch: if session.err != nil { rest.Error(c, session.err) } else { c.JSON(http.StatusOK, session.taskGroup.TaskGroupModel) } case <-time.After(Timeout): rest.Error(c, ErrTimeout.NewWithNoMessage()) } } // @ID getProfilingGroups // @Summary List all profiling groups // @Description List all profiling groups // @Security JwtAuth // @Success 200 {array} TaskGroupModel // @Failure 401 {object} rest.ErrorResponse // @Router /profiling/group/list [get] func (s *Service) getGroupList(c *gin.Context) { var resp []TaskGroupModel err := s.params.LocalStore.Order("id DESC").Find(&resp).Error if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, resp) } type GroupDetailResponse struct { ServerTime int64 `json:"server_time"` TaskGroup TaskGroupModel `json:"task_group_status"` Tasks []TaskModel `json:"tasks_status"` } // @ID getProfilingGroupDetail // @Summary List all tasks with a given group ID // @Description List all profiling tasks with a given group ID // @Param groupId path string true "group ID" // @Security JwtAuth // @Success 200 {object} GroupDetailResponse // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Router /profiling/group/detail/{groupId} [get] func (s *Service) getGroupDetail(c *gin.Context) { taskGroupID, err := strconv.Atoi(c.Param("groupId")) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } var taskGroup TaskGroupModel err = s.params.LocalStore.Where("id = ?", taskGroupID).Find(&taskGroup).Error if err != nil { rest.Error(c, err) return } var tasks []TaskModel err = s.params.LocalStore.Where("task_group_id = ?", taskGroupID).Find(&tasks).Error if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, GroupDetailResponse{ ServerTime: time.Now().Unix(), // Used to estimate task progress TaskGroup: taskGroup, Tasks: tasks, }) } // @ID cancelProfilingGroup // @Summary Cancel all tasks with a given group ID // @Description Cancel all profling tasks with a given group ID // @Param groupId path string true "group ID" // @Security JwtAuth // @Success 200 {object} rest.EmptyResponse // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Router /profiling/group/cancel/{groupId} [post] func (s *Service) handleCancelGroup(c *gin.Context) { taskGroupID, err := strconv.Atoi(c.Param("groupId")) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } if err := s.cancelGroup(uint(taskGroupID)); err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, rest.EmptyResponse{}) } // @ID getActionToken // @Summary Get action token for download or view // @Description Get token with a given group ID or task ID and action type // @Produce plain // @Param id query string false "group or task ID" // @Param action query string false "action" // @Security JwtAuth // @Success 200 {string} string // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse // @Router /profiling/action_token [get] func (s *Service) getActionToken(c *gin.Context) { id := c.Query("id") action := c.Query("action") // group_download, single_download, single_view token, err := utils.NewJWTString("profiling/"+action, id) if err != nil { rest.Error(c, err) return } c.String(http.StatusOK, token) } // @ID downloadProfilingGroup // @Summary Download all results of a task group // @Description Download all finished profiling results of a task group // @Produce application/x-gzip // @Param token query string true "download token" // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse // @Router /profiling/group/download [get] func (s *Service) downloadGroup(c *gin.Context) { token := c.Query("token") str, err := utils.ParseJWTString("profiling/group_download", token) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } taskGroupID, err := strconv.Atoi(str) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } var tasks []TaskModel err = s.params.LocalStore.Where("task_group_id = ? AND state = ?", taskGroupID, TaskStateFinish).Find(&tasks).Error if err != nil { rest.Error(c, err) return } filePathes := make([]string, len(tasks)) for i, task := range tasks { filePathes[i] = task.FilePath } fileName := fmt.Sprintf("profiling_%s.zip", time.Now().Format("2006-01-02_15-04-05")) c.Writer.Header().Set("Content-type", "application/octet-stream") c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName)) zw := zip.NewWriter(c.Writer) defer func() { _ = zw.Close() }() err = writeZipFromFiles(zw, filePathes, true) if err != nil { rest.Error(c, err) return } err = zipREADME(zw) if err != nil { rest.Error(c, err) return } } // @ID downloadProfilingSingle // @Summary Download the result of a task // @Description Download the finished profiling result of a task // @Produce application/x-gzip // @Param token query string true "download token" // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse // @Router /profiling/single/download [get] func (s *Service) downloadSingle(c *gin.Context) { // FIXME: We can simply provide only a single file token := c.Query("token") str, err := utils.ParseJWTString("profiling/single_download", token) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } taskID, err := strconv.Atoi(str) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } task := TaskModel{} err = s.params.LocalStore.Where("id = ? AND state = ?", taskID, TaskStateFinish).First(&task).Error if err != nil { rest.Error(c, err) return } fileName := fmt.Sprintf("profiling_%s.zip", time.Now().Format("2006-01-02_15-04-05")) c.Writer.Header().Set("Content-type", "application/octet-stream") c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName)) zw := zip.NewWriter(c.Writer) defer func() { _ = zw.Close() }() err = writeZipFromFiles(zw, []string{task.FilePath}, true) if err != nil { rest.Error(c, err) return } err = zipREADME(zw) if err != nil { rest.Error(c, err) return } } func writeZipFromFiles(zw *zip.Writer, files []string, compress bool) error { for _, file := range files { err := writeZipFromFile(zw, file, compress) if err != nil { return err } } return nil } func writeZipFromFile(zw *zip.Writer, file string, compress bool) error { f, err := os.Open(filepath.Clean(file)) if err != nil { return err } defer func() { _ = f.Close() }() fileInfo, err := f.Stat() if err != nil { return err } zipMethod := zip.Store // no compress if compress { zipMethod = zip.Deflate // compress } zipFile, err := zw.CreateHeader(&zip.FileHeader{ Name: fileInfo.Name(), Method: zipMethod, Modified: time.Now(), }) if err != nil { return err } _, err = io.Copy(zipFile, f) if err != nil { return err } return nil } func zipREADME(zw *zip.Writer) error { const downloadREADME = ` To review the CPU profiling or go heap profiling result interactively: $ go tool pprof --http=0.0.0.0:1234 cpu_xxx.proto To review the jemalloc profile data whose file name suffix is '.prof' interactively: $ jeprof --web profile_xxx.prof ` zipFile, err := zw.CreateHeader(&zip.FileHeader{ Name: "README.md", Method: zip.Deflate, Modified: time.Now(), }) if err != nil { return err } _, err = zipFile.Write([]byte(downloadREADME)) if err != nil { return err } return nil } type ViewOutputType string const ( ViewOutputTypeProtobuf ViewOutputType = "protobuf" ViewOutputTypeGraph ViewOutputType = "graph" ViewOutputTypeText ViewOutputType = "text" ) // @ID viewProfilingSingle // @Summary View the result of a task // @Description View the finished profiling result of a task // @Produce html // @Param token query string true "download token" // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse // @Router /profiling/single/view [get] func (s *Service) viewSingle(c *gin.Context) { token := c.Query("token") outputType := c.Query("output_type") str, err := utils.ParseJWTString("profiling/single_view", token) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } taskID, err := strconv.Atoi(str) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } task := TaskModel{} err = s.params.LocalStore.Where("id = ? AND state = ?", taskID, TaskStateFinish).First(&task).Error if err != nil { rest.Error(c, err) return } content, err := os.ReadFile(task.FilePath) if err != nil { rest.Error(c, err) return } // set default content-type for legacy profiling content. contentType := "image/svg+xml" switch task.RawDataType { case RawDataTypeProtobuf: switch outputType { case string(ViewOutputTypeGraph): svgContent, err := convertProtobufToSVG(content, task) if err != nil { rest.Error(c, err) return } content = svgContent contentType = "image/svg+xml" case string(ViewOutputTypeProtobuf): contentType = "application/protobuf" default: // Will not handle converting protobuf to other formats except flamegraph and graph rest.Error(c, rest.ErrBadRequest.New("Cannot output protobuf as %s", outputType)) return } case RawDataTypeJeprof: // call jeprof to convert svg switch outputType { case string(ViewOutputTypeGraph): cmd := exec.Command("perl", "/dev/stdin", "--dot", task.FilePath) //nolint:gosec cmd.Stdin = strings.NewReader(jeprof) dotContent, err := cmd.Output() if err != nil { rest.Error(c, err) return } svgContent, err := convertDotToSVG(dotContent) if err != nil { rest.Error(c, err) return } content = svgContent contentType = "image/svg+xml" case string(ViewOutputTypeText): // Brendan Gregg's collapsed stack format cmd := exec.Command("perl", "/dev/stdin", "--collapsed", task.FilePath) //nolint:gosec cmd.Stdin = strings.NewReader(jeprof) textContent, err := cmd.Output() if err != nil { rest.Error(c, err) return } content = textContent contentType = "text/plain" default: // Will not handle converting jeprof raw data to other formats except flamegraph and graph rest.Error(c, rest.ErrBadRequest.New("Cannot output jeprof raw data as %s", outputType)) return } case RawDataTypeText: switch outputType { case string(ViewOutputTypeText): contentType = "text/plain" default: // Will not handle converting text to other formats rest.Error(c, rest.ErrBadRequest.New("Cannot output text as %s", outputType)) return } } c.Data(http.StatusOK, contentType, content) } // @ID deleteProfilingGroup // @Summary Delete all tasks with a given group ID // @Description Delete all finished profiling tasks with a given group ID // @Param groupId path string true "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 /profiling/group/delete/{groupId} [delete] func (s *Service) deleteGroup(c *gin.Context) { taskGroupID, err := strconv.Atoi(c.Param("groupId")) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } if err := s.cancelGroup(uint(taskGroupID)); err != nil { rest.Error(c, err) return } if err = s.params.LocalStore.Where("task_group_id = ?", taskGroupID).Delete(&TaskModel{}).Error; err != nil { rest.Error(c, err) return } if err = s.params.LocalStore.Where("id = ?", taskGroupID).Delete(&TaskGroupModel{}).Error; err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, rest.EmptyResponse{}) } // @Summary Get Profiling Dynamic Config // @Success 200 {object} config.ProfilingConfig // @Router /profiling/config [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) getDynamicConfig(c *gin.Context) { dc, err := s.params.ConfigManager.Get() if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, dc.Profiling) } // @Summary Set Profiling Dynamic Config // @Param request body config.ProfilingConfig true "Request body" // @Success 200 {object} config.ProfilingConfig // @Router /profiling/config [put] // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) setDynamicConfig(c *gin.Context) { var req config.ProfilingConfig if err := c.ShouldBindJSON(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } var opt config.DynamicConfigOption = func(dc *config.DynamicConfig) { dc.Profiling = req } if err := s.params.ConfigManager.Modify(opt); err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, req) } ================================================ FILE: pkg/apiserver/profiling/service.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package profiling import ( "context" "sync" "time" "github.com/joomcode/errorx" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/fx" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/dbstore" "github.com/pingcap/tidb-dashboard/pkg/pd" ) const ( Timeout = 5 * time.Second ) var ( ErrNS = errorx.NewNamespace("error.api.profiling") ErrIgnoredRequest = ErrNS.NewType("ignored_request") ErrTimeout = ErrNS.NewType("timeout") ErrUnsupportedProfilingType = ErrNS.NewType("unsupported_profiling_type") ErrUnsupportedProfilingTarget = ErrNS.NewType("unsupported_profiling_target") ) type StartRequest struct { Targets []model.RequestTargetNode `json:"targets"` DurationSecs uint `json:"duration_secs"` RequstedProfilingTypes TaskProfilingTypeList `json:"requsted_profiling_types"` } type StartRequestSession struct { req StartRequest ch chan struct{} taskGroup *TaskGroup err error } type ServiceParams struct { fx.In ConfigManager *config.DynamicConfigManager LocalStore *dbstore.DB EtcdClient *clientv3.Client PDClient *pd.Client } type Service struct { params ServiceParams lifecycleCtx context.Context wg sync.WaitGroup sessionCh chan *StartRequestSession lastTaskGroup *TaskGroup tasks sync.Map fetchers *fetchers } var newService = fx.Provide(func(lc fx.Lifecycle, p ServiceParams, fts *fetchers) (*Service, error) { if err := autoMigrate(p.LocalStore); err != nil { return nil, err } s := &Service{params: p, fetchers: fts} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { s.lifecycleCtx = ctx s.wg.Go(func() { s.serviceLoop(ctx) }) return nil }, OnStop: func(context.Context) error { s.wg.Wait() return nil }, }) return s, nil }) func (s *Service) serviceLoop(ctx context.Context) { cfgCh := s.params.ConfigManager.NewPushChannel() s.sessionCh = make(chan *StartRequestSession, 1000) defer close(s.sessionCh) var dc *config.DynamicConfig var timeCh <-chan time.Time = make(chan time.Time, 1) newAutoRequest := func() *StartRequest { if dc == nil || dc.Profiling.AutoCollectionDurationSecs == 0 { timeCh = make(chan time.Time, 1) return nil } timeCh = time.After(time.Duration(dc.Profiling.AutoCollectionIntervalSecs+dc.Profiling.AutoCollectionDurationSecs) * time.Second) return &StartRequest{ Targets: dc.Profiling.AutoCollectionTargets, DurationSecs: dc.Profiling.AutoCollectionDurationSecs, } } for { select { case <-ctx.Done(): return case newDc, ok := <-cfgCh: if !ok { return } dc = newDc if req := newAutoRequest(); req != nil { _, _ = s.exclusiveExecute(ctx, req) } case <-timeCh: if req := newAutoRequest(); req != nil { _, _ = s.exclusiveExecute(ctx, req) } case session := <-s.sessionCh: s.handleRequest(ctx, session, dc) } } } func (s *Service) handleRequest(ctx context.Context, session *StartRequestSession, dc *config.DynamicConfig) { defer close(session.ch) if dc.Profiling.AutoCollectionDurationSecs > 0 { session.err = ErrIgnoredRequest.New("automatic collection is enabled") log.Warn("request is ignored", zap.Error(session.err)) return } session.taskGroup, session.err = s.exclusiveExecute(ctx, &session.req) } func (s *Service) exclusiveExecute(ctx context.Context, req *StartRequest) (*TaskGroup, error) { if s.lastTaskGroup != nil { if err := s.cancelGroup(s.lastTaskGroup.ID); err != nil { return nil, ErrIgnoredRequest.New("failed to cancel last task group: id = %d", s.lastTaskGroup.ID) } time.Sleep(500 * time.Millisecond) } return s.startGroup(ctx, req) } func (s *Service) startGroup(ctx context.Context, req *StartRequest) (*TaskGroup, error) { taskGroup := NewTaskGroup(s.params.LocalStore, req.DurationSecs, model.NewRequestTargetStatisticsFromArray(&req.Targets), req.RequstedProfilingTypes) if err := s.params.LocalStore.Create(taskGroup.TaskGroupModel).Error; err != nil { log.Warn("failed to start task group", zap.Error(err)) return nil, err } tasks := make([]*Task, 0, len(req.Targets)) for _, target := range req.Targets { profileTypeList := req.RequstedProfilingTypes for _, profilingType := range profileTypeList { // profilingTypeMap checks the validation of requestedProfilingType. _, valid := profilingTypeMap[profilingType] if !valid { return nil, ErrUnsupportedProfilingType.NewWithNoMessage() } t := NewTask(ctx, taskGroup, target, s.fetchers, profilingType) s.params.LocalStore.Create(t.TaskModel) s.tasks.Store(t.ID, t) tasks = append(tasks, t) } } s.wg.Go(func() { var wg sync.WaitGroup for i := 0; i < len(tasks); i++ { wg.Add(1) go func(idx int) { defer wg.Done() tasks[idx].run() s.tasks.Delete(tasks[idx].ID) }(i) } wg.Wait() errorTasks := 0 finishedTasks := 0 for _, task := range tasks { switch task.State { case TaskStateError: errorTasks++ case TaskStateFinish: finishedTasks++ } } if errorTasks > 0 { taskGroup.State = TaskStateError if finishedTasks > 0 { taskGroup.State = TaskStatePartialFinish } } else { taskGroup.State = TaskStateFinish } s.params.LocalStore.Save(taskGroup.TaskGroupModel) }) return taskGroup, nil } func (s *Service) cancelGroup(taskGroupID uint) error { var tasks []TaskModel if err := s.params.LocalStore.Where("task_group_id = ? AND state = ?", taskGroupID, TaskStateRunning).Find(&tasks).Error; err != nil { log.Warn("failed to cancel task group", zap.Error(err)) return err } for _, task := range tasks { if task, ok := s.tasks.Load(task.ID); ok { t := task.(*Task) t.stop() } } // wait for tasks stop ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { var runningTasks []TaskModel if err := s.params.LocalStore.Where("task_group_id = ? AND state = ?", taskGroupID, TaskStateRunning).Find(&runningTasks).Error; err != nil { log.Warn("failed to cancel task group", zap.Error(err)) return err } if len(runningTasks) == 0 { break } <-ticker.C } return nil } ================================================ FILE: pkg/apiserver/queryeditor/service.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package queryeditor import ( "context" "database/sql" "net/http" "time" "github.com/gin-gonic/gin" "github.com/pingcap/log" "go.uber.org/fx" "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/tidb" "github.com/pingcap/tidb-dashboard/util/rest" ) type ServiceParams struct { fx.In Config *config.Config TiDBClient *tidb.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 } func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/query_editor") endpoint.Use(auth.MWAuthRequired()) endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) endpoint.Use(utils.MWForbidByExperimentalFlag(s.params.Config.EnableExperimental)) endpoint.POST("/run", auth.MWRequireWritePriv(), s.runHandler) } type RunRequest struct { Statements string `json:"statements" example:"show databases;"` MaxRows int `json:"max_rows" example:"1000"` } type RunResponse struct { ErrorMsg string `json:"error_msg"` ColumnNames []string `json:"column_names"` Rows [][]interface{} `json:"rows"` ExecutionMs int64 `json:"execution_ms"` ActualRows int `json:"actual_rows"` } func executeStatements(context context.Context, db *sql.DB, statements string) ([]string, [][]interface{}, error) { rows, err := db.QueryContext(context, statements) if err != nil { return nil, nil, err } defer rows.Close() colNames, err := rows.Columns() if err != nil { return nil, nil, err } retRows := make([][]interface{}, 0) values := make([]sql.RawBytes, len(colNames)) scanArgs := make([]interface{}, len(values)) for i := range values { scanArgs[i] = &values[i] } for rows.Next() { err = rows.Scan(scanArgs...) if err != nil { return nil, nil, err } retRow := make([]interface{}, 0, len(values)) var value interface{} for _, col := range values { if col == nil { value = nil } else { value = string(col) } retRow = append(retRow, value) } retRows = append(retRows, retRow) } if err = rows.Err(); err != nil { return nil, nil, err } return colNames, retRows, nil } // @ID queryEditorRun // @Summary Run statements // @Param request body RunRequest true "Request body" // @Success 200 {object} RunResponse // @Router /query_editor/run [post] // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 403 {object} rest.ErrorResponse func (s *Service) runHandler(c *gin.Context) { var req RunRequest if err := c.ShouldBindJSON(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } ctx, cancel := context.WithTimeout(s.lifecycleCtx, time.Minute*5) defer cancel() startTime := time.Now() sqlDB, err := utils.GetTiDBConnection(c).DB() if err != nil { panic(err) } colNames, rows, err := executeStatements(ctx, sqlDB, req.Statements) elapsedTime := time.Since(startTime) if err != nil { log.Warn("Failed to execute user input statements", zap.String("statements", req.Statements), zap.Error(err)) c.JSON(http.StatusOK, RunResponse{ ErrorMsg: err.Error(), ColumnNames: nil, Rows: nil, ExecutionMs: elapsedTime.Milliseconds(), ActualRows: 0, }) return } truncatedRows := rows if len(truncatedRows) > req.MaxRows { truncatedRows = truncatedRows[:req.MaxRows] } c.JSON(http.StatusOK, RunResponse{ ColumnNames: colNames, Rows: truncatedRows, ExecutionMs: elapsedTime.Milliseconds(), ActualRows: len(rows), }) } ================================================ FILE: pkg/apiserver/resource_manager/module.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package resourcemanager import "go.uber.org/fx" var Module = fx.Options( fx.Provide(newService), fx.Invoke(registerRouter), ) ================================================ FILE: pkg/apiserver/resource_manager/service.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package resourcemanager import ( "errors" "fmt" "net/http" "regexp" "sort" "strings" "time" "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" "github.com/pingcap/tidb-dashboard/util/featureflag" "github.com/pingcap/tidb-dashboard/util/rest" ) var workloadInjectChecker = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) type ServiceParams struct { fx.In TiDBClient *tidb.Client } type Service struct { FeatureResourceManager *featureflag.FeatureFlag params ServiceParams } func newService(p ServiceParams, ff *featureflag.Registry) *Service { return &Service{params: p, FeatureResourceManager: ff.Register("resource_manager", ">= 7.1.0")} } func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/resource_manager") endpoint.Use( auth.MWAuthRequired(), s.FeatureResourceManager.VersionGuard(), utils.MWConnectTiDB(s.params.TiDBClient), ) { endpoint.GET("/config", s.GetConfig) endpoint.GET("/information", s.GetInformation) endpoint.GET("/information/group_names", s.resourceGroupNamesHandler) endpoint.GET("/calibrate/hardware", s.GetCalibrateByHardware) endpoint.GET("/calibrate/actual", s.GetCalibrateByActual) } } type GetConfigResponse struct { Enable bool `json:"enable" gorm:"column:tidb_enable_resource_control"` } // @Summary Get Resource Control enable config // @Router /resource_manager/config [get] // @Security JwtAuth // @Success 200 {object} GetConfigResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) GetConfig(c *gin.Context) { db := utils.GetTiDBConnection(c) resp := &GetConfigResponse{} err := db.Raw("SELECT @@GLOBAL.tidb_enable_resource_control as tidb_enable_resource_control").Find(resp).Error if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, resp) } type ResourceInfoRowDef struct { Name string `json:"name" gorm:"column:NAME"` RuPerSec string `json:"ru_per_sec" gorm:"column:RU_PER_SEC"` Priority string `json:"priority" gorm:"column:PRIORITY"` Burstable string `json:"burstable" gorm:"column:BURSTABLE"` } // @Summary Get Information of Resource Groups // @Router /resource_manager/information [get] // @Security JwtAuth // @Success 200 {object} []ResourceInfoRowDef // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) GetInformation(c *gin.Context) { db := utils.GetTiDBConnection(c) var cfg []ResourceInfoRowDef err := db.Table("INFORMATION_SCHEMA.RESOURCE_GROUPS").Scan(&cfg).Error if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, cfg) } // @Summary List all resource groups // @Router /resource_manager/information/group_names [get] // @Security JwtAuth // @Success 200 {object} []string // @Failure 401 {object} rest.ErrorResponse func (s *Service) resourceGroupNamesHandler(c *gin.Context) { type groupSchemas struct { Groups string `gorm:"column:NAME"` } var result []groupSchemas db := utils.GetTiDBConnection(c) err := db.Raw("SELECT NAME FROM INFORMATION_SCHEMA.RESOURCE_GROUPS").Scan(&result).Error if err != nil { rest.Error(c, err) return } strs := []string{} for _, v := range result { strs = append(strs, strings.ToLower(v.Groups)) } sort.Strings(strs) c.JSON(http.StatusOK, strs) } type CalibrateResponse struct { EstimatedCapacity int `json:"estimated_capacity" gorm:"column:QUOTA"` } // @Summary Get calibrate of Resource Groups by hardware deployment // @Router /resource_manager/calibrate/hardware [get] // @Param workload query string true "workload" default("tpcc") // @Security JwtAuth // @Success 200 {object} CalibrateResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) GetCalibrateByHardware(c *gin.Context) { w := c.Query("workload") if w == "" { rest.Error(c, rest.ErrBadRequest.New("workload cannot be empty")) return } if !workloadInjectChecker.MatchString(w) { rest.Error(c, errors.New("invalid workload")) return } db := utils.GetTiDBConnection(c) resp := &CalibrateResponse{} err := db.Raw(fmt.Sprintf("calibrate resource workload %s", w)).Scan(resp).Error if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, resp) } type GetCalibrateByActualRequest struct { StartTime int64 `json:"start_time" form:"start_time"` EndTime int64 `json:"end_time" form:"end_time"` } // @Summary Get calibrate of Resource Groups by actual workload // @Router /resource_manager/calibrate/actual [get] // @Param q query GetCalibrateByActualRequest true "Query" // @Security JwtAuth // @Success 200 {object} CalibrateResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) GetCalibrateByActual(c *gin.Context) { var req GetCalibrateByActualRequest if err := c.ShouldBindQuery(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } startTime := time.Unix(req.StartTime, 0).Format("2006-01-02 15:04:05") endTime := time.Unix(req.EndTime, 0).Format("2006-01-02 15:04:05") db := utils.GetTiDBConnection(c) resp := &CalibrateResponse{} err := db.Raw(fmt.Sprintf("calibrate resource start_time '%s' end_time '%s'", startTime, endTime)).Scan(resp).Error if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, resp) } ================================================ FILE: pkg/apiserver/slowquery/model.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package slowquery import ( "strings" "gorm.io/datatypes" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap/tidb-dashboard/util/reflectutil" ) type Model struct { Digest string `gorm:"column:Digest" json:"digest"` Query string `gorm:"column:Query" json:"query"` Instance string `gorm:"column:INSTANCE" json:"instance"` DB string `gorm:"column:DB" json:"db"` // TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. ConnectionID string `gorm:"column:Conn_ID" json:"connection_id"` Success int `gorm:"column:Succ" json:"success"` Timestamp float64 `gorm:"column:timestamp" proj:"(UNIX_TIMESTAMP(Time) + 0E0)" json:"timestamp" related:"time"` // finish time QueryTime float64 `gorm:"column:Query_time" json:"query_time"` // latency ParseTime float64 `gorm:"column:Parse_time" json:"parse_time"` CompileTime float64 `gorm:"column:Compile_time" json:"compile_time"` RewriteTime float64 `gorm:"column:Rewrite_time" json:"rewrite_time"` PreprocSubqueriesTime float64 `gorm:"column:Preproc_subqueries_time" json:"preproc_subqueries_time"` OptimizeTime float64 `gorm:"column:Optimize_time" json:"optimize_time"` WaitTSTime float64 `gorm:"column:Wait_TS" json:"wait_ts"` CopTime float64 `gorm:"column:Cop_time" json:"cop_time"` LockKeysTime float64 `gorm:"column:LockKeys_time" json:"lock_keys_time"` WriteRespTime float64 `gorm:"column:Write_sql_response_total" json:"write_sql_response_total"` ExecRetryTime float64 `gorm:"column:Exec_retry_time" json:"exec_retry_time"` MemoryMax int `gorm:"column:Mem_max" json:"memory_max"` DiskMax int `gorm:"column:Disk_max" json:"disk_max"` MemArbitration float64 `gorm:"column:Mem_arbitration" json:"mem_arbitration"` // TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. TxnStartTS string `gorm:"column:Txn_start_ts" json:"txn_start_ts"` // Detail PrevStmt string `gorm:"column:Prev_stmt" json:"prev_stmt"` Plan string `gorm:"column:Plan" json:"plan"` // deprecated, replaced by BinaryPlanText BinaryPlan string `gorm:"column:Binary_plan" json:"binary_plan"` Warnings datatypes.JSON `gorm:"column:Warnings" json:"warnings"` // Basic IsInternal int `gorm:"column:Is_internal" json:"is_internal"` IndexNames string `gorm:"column:Index_names" json:"index_names"` Stats string `gorm:"column:Stats" json:"stats"` BackoffTypes string `gorm:"column:Backoff_types" json:"backoff_types"` Prepared int `gorm:"column:Prepared" json:"prepared"` PlanFromCache int `gorm:"column:Plan_from_cache" json:"plan_from_cache"` PlanFromBinding int `gorm:"column:Plan_from_binding" json:"plan_from_binding"` // Connection User string `gorm:"column:User" json:"user"` Host string `gorm:"column:Host" json:"host"` // Time ProcessTime float64 `gorm:"column:Process_time" json:"process_time"` WaitTime float64 `gorm:"column:Wait_time" json:"wait_time"` BackoffTime float64 `gorm:"column:Backoff_time" json:"backoff_time"` GetCommitTSTime float64 `gorm:"column:Get_commit_ts_time" json:"get_commit_ts_time"` LocalLatchWaitTime float64 `gorm:"column:Local_latch_wait_time" json:"local_latch_wait_time"` ResolveLockTime float64 `gorm:"column:Resolve_lock_time" json:"resolve_lock_time"` PrewriteTime float64 `gorm:"column:Prewrite_time" json:"prewrite_time"` WaitPreWriteBinlogTime float64 `gorm:"column:Wait_prewrite_binlog_time" json:"wait_prewrite_binlog_time"` CommitTime float64 `gorm:"column:Commit_time" json:"commit_time"` CommitBackoffTime float64 `gorm:"column:Commit_backoff_time" json:"commit_backoff_time"` CopProcAvg float64 `gorm:"column:Cop_proc_avg" json:"cop_proc_avg"` CopProcP90 float64 `gorm:"column:Cop_proc_p90" json:"cop_proc_p90"` CopProcMax float64 `gorm:"column:Cop_proc_max" json:"cop_proc_max"` CopWaitAvg float64 `gorm:"column:Cop_wait_avg" json:"cop_wait_avg"` CopWaitP90 float64 `gorm:"column:Cop_wait_p90" json:"cop_wait_p90"` CopWaitMax float64 `gorm:"column:Cop_wait_max" json:"cop_wait_max"` // Transaction WriteKeys int `gorm:"column:Write_keys" json:"write_keys"` WriteSize int `gorm:"column:Write_size" json:"write_size"` PrewriteRegion int `gorm:"column:Prewrite_region" json:"prewrite_region"` TxnRetry int `gorm:"column:Txn_retry" json:"txn_retry"` // Coprocessor RequestCount uint `gorm:"column:Request_count" json:"request_count"` ProcessKeys uint `gorm:"column:Process_keys" json:"process_keys"` TotalKeys uint `gorm:"column:Total_keys" json:"total_keys"` CopProcAddr string `gorm:"column:Cop_proc_addr" json:"cop_proc_addr"` CopWaitAddr string `gorm:"column:Cop_wait_addr" json:"cop_wait_addr"` // RocksDB RocksdbDeleteSkippedCount uint `gorm:"column:Rocksdb_delete_skipped_count" json:"rocksdb_delete_skipped_count"` RocksdbKeySkippedCount uint `gorm:"column:Rocksdb_key_skipped_count" json:"rocksdb_key_skipped_count"` RocksdbBlockCacheHitCount uint `gorm:"column:Rocksdb_block_cache_hit_count" json:"rocksdb_block_cache_hit_count"` RocksdbBlockReadCount uint `gorm:"column:Rocksdb_block_read_count" json:"rocksdb_block_read_count"` RocksdbBlockReadByte uint `gorm:"column:Rocksdb_block_read_byte" json:"rocksdb_block_read_byte"` // Computed fields BinaryPlanJSON string `json:"binary_plan_json"` // binary plan json format BinaryPlanText string `json:"binary_plan_text"` // binary plan plain text // Resource Control RU float64 `gorm:"column:RU" json:"ru" proj:"(Request_unit_write + Request_unit_read)" related:"Request_unit_write,Request_unit_read"` QueuedTime float64 `gorm:"column:Time_queued_by_rc" json:"time_queued_by_rc"` ResourceGroup string `gorm:"column:Resource_group" json:"resource_group"` // Network fields UnpackedBytesSentTiKVTotal uint `gorm:"column:Unpacked_bytes_sent_tikv_total" json:"unpacked_bytes_sent_tikv_total"` UnpackedBytesReceivedTiKVTotal uint `gorm:"column:Unpacked_bytes_received_tikv_total" json:"unpacked_bytes_received_tikv_total"` UnpackedBytesSentTiKVCrossZone uint `gorm:"column:Unpacked_bytes_sent_tikv_cross_zone" json:"unpacked_bytes_sent_tikv_cross_zone"` UnpackedBytesReceivedTiKVCrossZone uint `gorm:"column:Unpacked_bytes_received_tikv_cross_zone" json:"unpacked_bytes_received_tikv_cross_zone"` UnpackedBytesSentTiFlashTotal uint `gorm:"column:Unpacked_bytes_sent_tiflash_total" json:"unpacked_bytes_sent_tiflash_total"` UnpackedBytesReceivedTiFlashTotal uint `gorm:"column:Unpacked_bytes_received_tiflash_total" json:"unpacked_bytes_received_tiflash_total"` UnpackedBytesSentTiFlashCrossZone uint `gorm:"column:Unpacked_bytes_sent_tiflash_cross_zone" json:"unpacked_bytes_sent_tiflash_cross_zone"` UnpackedBytesReceivedTiFlashCrossZone uint `gorm:"column:Unpacked_bytes_received_tiflash_cross_zone" json:"unpacked_bytes_received_tiflash_cross_zone"` // IA remote read IARemoteReadSegmentSize uint64 `gorm:"column:IA_remote_read_segment_size" json:"ia_remote_read_segment_size"` IARemoteReadSegmentWaitTime float64 `gorm:"column:IA_remote_read_segment_wait_time" json:"ia_remote_read_segment_wait_time"` } type Field struct { ColumnName string JSONName string Projection string // `related` tag is used to verify a non-existent column, which is aggregated/projection from the columns represented by related. Related []string } func getFieldsAndTags() (slowQueryFields []Field) { fields := reflectutil.GetFieldsAndTags(Model{}, []string{"gorm", "proj", "json", "related"}) for _, f := range fields { sqf := Field{ ColumnName: utils.GetGormColumnName(f.Tags["gorm"]), JSONName: f.Tags["json"], Projection: f.Tags["proj"], } if f.Tags["related"] != "" { sqf.Related = strings.Split(f.Tags["related"], ",") } slowQueryFields = append(slowQueryFields, sqf) } return } func filterFieldsByColumns(fields []Field, columns []string) []Field { colMap := map[string]struct{}{} for _, c := range columns { colMap[strings.ToLower(c)] = struct{}{} } filteredFields := []Field{} for _, f := range fields { _, ok := colMap[strings.ToLower(f.ColumnName)] if ok || (f.Projection != "") { filteredFields = append(filteredFields, f) } } return filteredFields } ================================================ FILE: pkg/apiserver/slowquery/module.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package slowquery import "go.uber.org/fx" var Module = fx.Options( fx.Provide(newService), fx.Invoke(registerRouter), ) ================================================ FILE: pkg/apiserver/slowquery/queries.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package slowquery import ( "strings" "gorm.io/gorm" "github.com/pingcap/tidb-dashboard/pkg/utils" ) const ( SlowQueryTable = "INFORMATION_SCHEMA.CLUSTER_SLOW_QUERY" ) type GetListRequest struct { BeginTime int `json:"begin_time" form:"begin_time"` EndTime int `json:"end_time" form:"end_time"` DB []string `json:"db" form:"db"` ResourceGroup []string `json:"resource_group" form:"resource_group"` Limit int `json:"limit" form:"limit"` Text string `json:"text" form:"text"` OrderBy string `json:"orderBy" form:"orderBy"` IsDesc bool `json:"desc" form:"desc"` // for showing slow queries in the statement detail page Plans []string `json:"plans" form:"plans"` Digest string `json:"digest" form:"digest"` Fields string `json:"fields" form:"fields"` // example: "Query,Digest" } type GetDetailRequest struct { Digest string `json:"digest" form:"digest"` Timestamp float64 `json:"timestamp" form:"timestamp"` // TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. ConnectID string `json:"connect_id" form:"connect_id"` } func QuerySlowLogList(req *GetListRequest, sysSchema *utils.SysSchema, db *gorm.DB) ([]Model, error) { slowQueryColumns, err := sysSchema.GetTableColumnNames(db, SlowQueryTable) if err != nil { return nil, err } reqFields := strings.Split(req.Fields, ",") selectStmt, err := genSelectStmt(slowQueryColumns, reqFields) if err != nil { return nil, err } tx := db. Select(selectStmt) if req.BeginTime != 0 && req.EndTime != 0 { tx = tx.Where("Time BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)", req.BeginTime, req.EndTime) } if req.Limit <= 0 { req.Limit = 100 } tx = tx.Limit(req.Limit) if req.Text != "" { lowerStr := strings.ToLower(req.Text) arr := strings.FieldsSeq(lowerStr) for v := range arr { tx = tx.Where( `Txn_start_ts REGEXP ? OR LOWER(Digest) REGEXP ? OR LOWER(CONVERT(Prev_stmt USING utf8)) REGEXP ? OR LOWER(CONVERT(Query USING utf8)) REGEXP ?`, v, v, v, v, ) } } if len(req.DB) > 0 { tx = tx.Where("DB IN (?)", req.DB) } if len(req.ResourceGroup) > 0 { tx = tx.Where("RESOURCE_GROUP IN (?)", req.ResourceGroup) } // more robust if req.OrderBy == "" { req.OrderBy = "timestamp" } orderStmt, err := genOrderStmt(slowQueryColumns, req.OrderBy, req.IsDesc) if err != nil { return nil, err } tx = tx.Order(orderStmt) // in TiDB Dashboard SQL Statements detail page, we can get multiple plan digests for a certain SQL statement. // we can get related slow queries for these plan digests. if len(req.Plans) > 0 { tx = tx.Where("Plan_digest IN (?)", req.Plans) } // in TiDB Dashboard SQL Statements detail page, we can get related slow queries by its SQL statement digest. if req.Digest != "" { tx = tx.Where("Digest = ?", req.Digest) } var results []Model err = tx.Find(&results).Error if err != nil { return nil, err } // truncate each row's query, keep the start 1000 characters to avoid too long text // if user want to see the full query, they can access the detail api for i := range results { if len(results[i].Query) > 1000 { results[i].Query = results[i].Query[:1000] + "..." } } return results, nil } func QuerySlowLogDetail(req *GetDetailRequest, sysSchema *utils.SysSchema, db *gorm.DB) (*Model, error) { var result Model slowQueryColumns, err := sysSchema.GetTableColumnNames(db, SlowQueryTable) if err != nil { return nil, err } selectStmt, err := genSelectStmt(slowQueryColumns, []string{"*"}) if err != nil { return nil, err } err = db. Select(selectStmt). Where("Digest = ?", req.Digest). Where("Time = FROM_UNIXTIME(?)", req.Timestamp). Where("Conn_id = ?", req.ConnectID). First(&result).Error if err != nil { return nil, err } return &result, nil } func GetAvailableFields(sysSchema *utils.SysSchema, db *gorm.DB) ([]string, error) { cs, err := sysSchema.GetTableColumnNames(db, SlowQueryTable) if err != nil { return nil, err } fields := filterFieldsByColumns(getFieldsAndTags(), cs) jsonNames := make([]string, 0, len(fields)) for _, f := range fields { jsonNames = append(jsonNames, f.JSONName) } return jsonNames, nil } ================================================ FILE: pkg/apiserver/slowquery/service.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package slowquery import ( "fmt" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "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" ) var ( ErrNS = errorx.NewNamespace("error.api.slow_query") ErrNoData = ErrNS.NewType("export_no_data") ) 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("/slow_query") { endpoint.GET("/download", s.downloadHandler) endpoint.Use(auth.MWAuthRequired()) endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) { endpoint.GET("/list", s.getList) endpoint.GET("/detail", s.getDetails) endpoint.POST("/download/token", s.downloadTokenHandler) endpoint.GET("/available_fields", s.getAvailableFields) } } } // @Summary List all slow queries // @Param q query GetListRequest true "Query" // @Success 200 {array} Model // @Router /slow_query/list [get] // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse func (s *Service) getList(c *gin.Context) { var req GetListRequest if err := c.ShouldBindQuery(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } db := utils.GetTiDBConnection(c) results, err := QuerySlowLogList(&req, s.params.SysSchema, db.Table(SlowQueryTable)) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } c.JSON(http.StatusOK, results) } // @Summary Get details of a slow query // @Param q query GetDetailRequest true "Query" // @Success 200 {object} Model // @Router /slow_query/detail [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) getDetails(c *gin.Context) { var req GetDetailRequest if err := c.ShouldBindQuery(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } db := utils.GetTiDBConnection(c) result, err := QuerySlowLogDetail(&req, s.params.SysSchema, db.Table(SlowQueryTable)) if err != nil { rest.Error(c, err) return } // generate binary plan json // // Due to a kernel bug, the binary plan may fail to parse due to // encoding issues. Additionally, since the binary plan field is // not a required field, we can mask this error. // // See: https://github.com/pingcap/tidb-dashboard/issues/1515 if result.BinaryPlan != "" { // may failed but it's ok result.BinaryPlanText, err = utils.GenerateBinaryPlanText(db, result.BinaryPlan) // may failed but it's ok result.BinaryPlanJSON, _ = utils.GenerateBinaryPlanJSON(result.BinaryPlan) if err == nil { // reduce response size result.BinaryPlan = "" result.Plan = "" } } c.JSON(http.StatusOK, *result) } // @Router /slow_query/download/token [post] // @Summary Generate a download token for exported slow query statements // @Produce plain // @Param request body GetListRequest true "Request body" // @Success 200 {string} string "xxx" // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse func (s *Service) downloadTokenHandler(c *gin.Context) { var req GetListRequest if err := c.ShouldBindJSON(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } fields := []string{} if strings.TrimSpace(req.Fields) != "" { fields = strings.Split(req.Fields, ",") } db := utils.GetTiDBConnection(c) list, err := QuerySlowLogList(&req, s.params.SysSchema, db.Table(SlowQueryTable)) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } if len(list) == 0 { rest.Error(c, ErrNoData.NewWithNoMessage()) return } // interface{} tricky rawData := make([]interface{}, len(list)) for i, v := range list { rawData[i] = v } // convert data csvData := utils.GenerateCSVFromRaw(rawData, fields, []string{}) // generate temp file that persist encrypted data timeLayout := "0102150405" beginTime := time.Unix(int64(req.BeginTime), 0).Format(timeLayout) endTime := time.Unix(int64(req.EndTime), 0).Format(timeLayout) token, err := utils.ExportCSV(csvData, fmt.Sprintf("slowquery_%s_%s_*.csv", beginTime, endTime), "slowquery/download") if err != nil { rest.Error(c, err) return } c.String(http.StatusOK, token) } // @Router /slow_query/download [get] // @Summary Download slow query statements // @Produce text/csv // @Param token query string true "download token" // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse func (s *Service) downloadHandler(c *gin.Context) { token := c.Query("token") utils.DownloadByToken(token, "slowquery/download", c) } // @Summary Get available field names // @Description Get available field names by slowquery table columns // @Success 200 {array} string // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Security JwtAuth // @Router /slow_query/available_fields [get] func (s *Service) getAvailableFields(c *gin.Context) { db := utils.GetTiDBConnection(c) jsonNames, err := GetAvailableFields(s.params.SysSchema, db) if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, jsonNames) } ================================================ FILE: pkg/apiserver/slowquery/statement_gen.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package slowquery import ( "fmt" "strings" "github.com/samber/lo" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" ) var ErrUnknownColumn = ErrNS.NewType("unknown_column") func genSelectStmt(tableColumns []string, reqJSONColumns []string) (string, error) { fields := getFieldsAndTags() // use required fields filter when not all fields are requested if reqJSONColumns[0] != "*" { // These three fields are the most basic information of a slow query record and should contain them requiredFields := lo.Uniq(append(reqJSONColumns, "digest", "connection_id", "timestamp")) fields = lo.Filter(fields, func(f Field, _ int) bool { return lo.Contains(requiredFields, f.JSONName) }) } // We have both TiDB 4.x and TiDB 5.x columns listed in the model. Filter out columns that do not exist in current version TiDB schema. fields = lo.Filter(fields, func(f Field, _ int) bool { var representedColumns []string if len(f.Related) != 0 { representedColumns = f.Related } else { representedColumns = []string{f.ColumnName} } // For compatibility with old TiDB, we need to check if the column exists in the table. // Dependent columns of the requested field must exist in the db schema. Otherwise, the requested field will be ignored. return utils.IsSubsetICaseInsensitive(tableColumns, representedColumns) }) if len(fields) == 0 { return "", ErrUnknownColumn.New("all columns are not included in the current version TiDB schema, columns: %q", reqJSONColumns) } stmt := lo.Map(fields, func(f Field, _ int) string { if f.Projection == "" { return f.ColumnName } return fmt.Sprintf("%s AS %s", f.Projection, f.ColumnName) }) return strings.Join(stmt, ", "), nil } func genOrderStmt(tableColumns []string, orderBy string, isDesc bool) (string, error) { var order string // to handle the special case: timestamp // Order by column instead of expression, see related optimization in TiDB: https://github.com/pingcap/tidb/pull/20750 if orderBy == "timestamp" { order = "Time" } else { // We have both TiDB 4.x and TiDB 5.x columns listed in the model. Filter out columns that do not exist in current version TiDB schema. fields := lo.Filter(getFieldsAndTags(), func(f Field, _ int) bool { var representedColumns []string if len(f.Related) != 0 { representedColumns = f.Related } else { representedColumns = []string{f.ColumnName} } // For compatibility with old TiDB, we need to check if the column exists in the table. // Dependent columns of the requested field must exist in the db schema. Otherwise, the requested field will be ignored. return utils.IsSubsetICaseInsensitive(tableColumns, representedColumns) }) orderField, ok := lo.Find(fields, func(f Field) bool { return f.JSONName == orderBy }) if !ok { return "", ErrUnknownColumn.New("unknown order by %s", orderBy) } order = orderField.ColumnName } if isDesc { order = fmt.Sprintf("%s DESC", order) } else { order = fmt.Sprintf("%s ASC", order) } return order, nil } ================================================ FILE: pkg/apiserver/statement/config.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package statement import ( "fmt" "reflect" "strings" "github.com/samber/lo" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" ) // incoming configuration field should have the gorm tag `column` used to specify global variables // sql will be built like this, // struct { FieldName `gorm:"column:some_global_var"` } -> @@GLOBAL.some_global_var AS some_global_var. func buildGlobalConfigProjectionSelectSQL(config interface{}) string { str := buildStringByStructField(config, func(f reflect.StructField) (string, bool) { gormTag, ok := f.Tag.Lookup("gorm") if !ok { return "", false } column := utils.GetGormColumnName(gormTag) return fmt.Sprintf("@@GLOBAL.%s AS %s", column, column), true }, ", ") return "SELECT " + str // #nosec } // sql will be built like this, // struct { FieldName `gorm:"column:some_global_var"` } -> @@GLOBAL.some_global_var = @FieldName // `allowedFields` means only allowed fields can be kept in built SQL. func buildGlobalConfigNamedArgsUpdateSQL(config interface{}, allowedFields ...string) string { str := buildStringByStructField(config, func(f reflect.StructField) (string, bool) { // extract fields on demand if len(allowedFields) != 0 && !lo.Contains(allowedFields, f.Name) { return "", false } gormTag, ok := f.Tag.Lookup("gorm") if !ok { return "", false } column := utils.GetGormColumnName(gormTag) return fmt.Sprintf("@@GLOBAL.%s = @%s", column, f.Name), true }, ", ") return "SET " + str // #nosec } func buildStringByStructField(i interface{}, buildFunc func(f reflect.StructField) (string, bool), sep string) string { var t reflect.Type if reflect.ValueOf(i).Kind() == reflect.Pointer { t = reflect.TypeOf(i).Elem() } else { t = reflect.TypeOf(i) } strs := []string{} fNum := t.NumField() for i := range fNum { str, ok := buildFunc(t.Field(i)) if !ok { continue } strs = append(strs, str) } return strings.Join(strs, sep) } ================================================ FILE: pkg/apiserver/statement/config_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package statement import ( "testing" "github.com/pingcap/check" ) func TestT(t *testing.T) { check.CustomVerboseFlag = true check.TestingT(t) } var _ = check.Suite(&testConfigSuite{}) type testConfigSuite struct{} type testConfig struct { Enable bool `json:"enable" gorm:"column:tidb_enable_stmt_summary"` RefreshInterval int `json:"refresh_interval" gorm:"column:tidb_stmt_summary_refresh_interval"` } func (t *testConfigSuite) Test_buildGlobalConfigProjectionSelectSQL_struct_success(c *check.C) { testConfigStmt := "SELECT @@GLOBAL.tidb_enable_stmt_summary AS tidb_enable_stmt_summary, @@GLOBAL.tidb_stmt_summary_refresh_interval AS tidb_stmt_summary_refresh_interval" c.Assert(buildGlobalConfigProjectionSelectSQL(testConfig{}), check.Equals, testConfigStmt) } func (t *testConfigSuite) Test_buildGlobalConfigProjectionSelectSQL_ptr_success(c *check.C) { testConfigStmt := "SELECT @@GLOBAL.tidb_enable_stmt_summary AS tidb_enable_stmt_summary, @@GLOBAL.tidb_stmt_summary_refresh_interval AS tidb_stmt_summary_refresh_interval" c.Assert(buildGlobalConfigProjectionSelectSQL(&testConfig{}), check.Equals, testConfigStmt) } type testConfig2 struct { Enable bool `json:"enable" gorm:"column:tidb_enable_stmt_summary"` RefreshInterval int `json:"refresh_interval"` } func (t *testConfigSuite) Test_buildGlobalConfigProjectionSelectSQL_without_gorm_tag(c *check.C) { testConfigStmt := "SELECT @@GLOBAL.tidb_enable_stmt_summary AS tidb_enable_stmt_summary" c.Assert(buildGlobalConfigProjectionSelectSQL(&testConfig2{}), check.Equals, testConfigStmt) } func (t *testConfigSuite) Test_buildGlobalConfigNamedArgsUpdateSQL_struct_success(c *check.C) { testConfigStmt := "SET @@GLOBAL.tidb_enable_stmt_summary = @Enable, @@GLOBAL.tidb_stmt_summary_refresh_interval = @RefreshInterval" c.Assert(buildGlobalConfigNamedArgsUpdateSQL(testConfig{Enable: true, RefreshInterval: 1800}), check.Equals, testConfigStmt) } func (t *testConfigSuite) Test_buildGlobalConfigNamedArgsUpdateSQL_ptr_success(c *check.C) { testConfigStmt := "SET @@GLOBAL.tidb_enable_stmt_summary = @Enable, @@GLOBAL.tidb_stmt_summary_refresh_interval = @RefreshInterval" c.Assert(buildGlobalConfigNamedArgsUpdateSQL(&testConfig{Enable: true, RefreshInterval: 1800}), check.Equals, testConfigStmt) } func (t *testConfigSuite) Test_buildGlobalConfigNamedArgsUpdateSQL_without_gorm_tag(c *check.C) { testConfigStmt := "SET @@GLOBAL.tidb_enable_stmt_summary = @Enable" c.Assert(buildGlobalConfigNamedArgsUpdateSQL(&testConfig2{Enable: true, RefreshInterval: 1800}), check.Equals, testConfigStmt) } func (t *testConfigSuite) Test_buildGlobalConfigNamedArgsUpdateSQL_extract_fields(c *check.C) { testConfigStmt := "SET @@GLOBAL.tidb_enable_stmt_summary = @Enable" c.Assert(buildGlobalConfigNamedArgsUpdateSQL(&testConfig{Enable: true, RefreshInterval: 1800}, "Enable"), check.Equals, testConfigStmt) } ================================================ FILE: pkg/apiserver/statement/models.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package statement import ( "strings" "github.com/samber/lo" "gorm.io/gorm" "gorm.io/gorm/schema" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap/tidb-dashboard/util/reflectutil" ) type Model struct { AggBeginTime int `json:"summary_begin_time" agg:"FLOOR(UNIX_TIMESTAMP(MIN(summary_begin_time)))"` AggEndTime int `json:"summary_end_time" agg:"FLOOR(UNIX_TIMESTAMP(MAX(summary_end_time)))"` AggDigestText string `json:"digest_text" agg:"ANY_VALUE(digest_text)"` AggDigest string `json:"digest" agg:"ANY_VALUE(digest)"` AggExecCount int `json:"exec_count" agg:"SUM(exec_count)"` AggStmtType string `json:"stmt_type" agg:"ANY_VALUE(stmt_type)"` AggSumErrors int `json:"sum_errors" agg:"SUM(sum_errors)"` AggSumWarnings int `json:"sum_warnings" agg:"SUM(sum_warnings)"` AggSumLatency int `json:"sum_latency" agg:"SUM(sum_latency)"` AggMaxLatency int `json:"max_latency" agg:"MAX(max_latency)"` AggMinLatency int `json:"min_latency" agg:"MIN(min_latency)"` AggAvgLatency int `json:"avg_latency" agg:"CAST(SUM(exec_count * avg_latency) / SUM(exec_count) AS SIGNED)"` AggAvgParseLatency int `json:"avg_parse_latency" agg:"CAST(SUM(exec_count * avg_parse_latency) / SUM(exec_count) AS SIGNED)"` AggMaxParseLatency int `json:"max_parse_latency" agg:"MAX(max_parse_latency)"` AggAvgCompileLatency int `json:"avg_compile_latency" agg:"CAST(SUM(exec_count * avg_compile_latency) / SUM(exec_count) AS SIGNED)"` AggMaxCompileLatency int `json:"max_compile_latency" agg:"MAX(max_compile_latency)"` AggSumCopTaskNum int `json:"sum_cop_task_num" agg:"SUM(sum_cop_task_num)"` AggAvgCopProcessTime int `json:"avg_cop_process_time" agg:"CAST(SUM(exec_count * avg_process_time) / SUM(sum_cop_task_num) AS SIGNED)"` // avg process time per copr task AggMaxCopProcessTime int `json:"max_cop_process_time" agg:"MAX(max_cop_process_time)"` // max process time per copr task AggAvgCopWaitTime int `json:"avg_cop_wait_time" agg:"CAST(SUM(exec_count * avg_wait_time) / SUM(sum_cop_task_num) AS SIGNED)"` // avg wait time per copr task AggMaxCopWaitTime int `json:"max_cop_wait_time" agg:"MAX(max_cop_wait_time)"` // max wait time per copr task AggAvgProcessTime int `json:"avg_process_time" agg:"CAST(SUM(exec_count * avg_process_time) / SUM(exec_count) AS SIGNED)"` // avg total process time per sql AggMaxProcessTime int `json:"max_process_time" agg:"MAX(max_process_time)"` // max process time per sql AggAvgWaitTime int `json:"avg_wait_time" agg:"CAST(SUM(exec_count * avg_wait_time) / SUM(exec_count) AS SIGNED)"` // avg total wait time per sql AggMaxWaitTime int `json:"max_wait_time" agg:"MAX(max_wait_time)"` // max wait time per sql AggAvgBackoffTime int `json:"avg_backoff_time" agg:"CAST(SUM(exec_count * avg_backoff_time) / SUM(exec_count) AS SIGNED)"` // avg total back off time per sql AggMaxBackoffTime int `json:"max_backoff_time" agg:"MAX(max_backoff_time)"` // max back off time per sql AggAvgTotalKeys int `json:"avg_total_keys" agg:"CAST(SUM(exec_count * avg_total_keys) / SUM(exec_count) AS SIGNED)"` AggMaxTotalKeys int `json:"max_total_keys" agg:"MAX(max_total_keys)"` AggAvgProcessedKeys int `json:"avg_processed_keys" agg:"CAST(SUM(exec_count * avg_processed_keys) / SUM(exec_count) AS SIGNED)"` AggMaxProcessedKeys int `json:"max_processed_keys" agg:"MAX(max_processed_keys)"` AggAvgPrewriteTime int `json:"avg_prewrite_time" agg:"CAST(SUM(exec_count * avg_prewrite_time) / SUM(exec_count) AS SIGNED)"` AggMaxPrewriteTime int `json:"max_prewrite_time" agg:"MAX(max_prewrite_time)"` AggAvgCommitTime int `json:"avg_commit_time" agg:"CAST(SUM(exec_count * avg_commit_time) / SUM(exec_count) AS SIGNED)"` AggMaxCommitTime int `json:"max_commit_time" agg:"MAX(max_commit_time)"` AggAvgGetCommitTsTime int `json:"avg_get_commit_ts_time" agg:"CAST(SUM(exec_count * avg_get_commit_ts_time) / SUM(exec_count) AS SIGNED)"` AggMaxGetCommitTsTime int `json:"max_get_commit_ts_time" agg:"MAX(max_get_commit_ts_time)"` AggAvgCommitBackoffTime int `json:"avg_commit_backoff_time" agg:"CAST(SUM(exec_count * avg_commit_backoff_time) / SUM(exec_count) AS SIGNED)"` AggMaxCommitBackoffTime int `json:"max_commit_backoff_time" agg:"MAX(max_commit_backoff_time)"` AggAvgResolveLockTime int `json:"avg_resolve_lock_time" agg:"CAST(SUM(exec_count * avg_resolve_lock_time) / SUM(exec_count) AS SIGNED)"` AggMaxResolveLockTime int `json:"max_resolve_lock_time" agg:"MAX(max_resolve_lock_time)"` AggAvgLocalLatchWaitTime int `json:"avg_local_latch_wait_time" agg:"CAST(SUM(exec_count * avg_local_latch_wait_time) / SUM(exec_count) AS SIGNED)"` AggMaxLocalLatchWaitTime int `json:"max_local_latch_wait_time" agg:"MAX(max_local_latch_wait_time)"` AggAvgWriteKeys int `json:"avg_write_keys" agg:"CAST(SUM(exec_count * avg_write_keys) / SUM(exec_count) AS SIGNED)"` AggMaxWriteKeys int `json:"max_write_keys" agg:"MAX(max_write_keys)"` AggAvgWriteSize int `json:"avg_write_size" agg:"CAST(SUM(exec_count * avg_write_size) / SUM(exec_count) AS SIGNED)"` AggMaxWriteSize int `json:"max_write_size" agg:"MAX(max_write_size)"` AggAvgPrewriteRegions int `json:"avg_prewrite_regions" agg:"CAST(SUM(exec_count * avg_prewrite_regions) / SUM(exec_count) AS SIGNED)"` AggMaxPrewriteRegions int `json:"max_prewrite_regions" agg:"MAX(max_prewrite_regions)"` AggAvgTxnRetry int `json:"avg_txn_retry" agg:"CAST(SUM(exec_count * avg_txn_retry) / SUM(exec_count) AS SIGNED)"` AggMaxTxnRetry int `json:"max_txn_retry" agg:"MAX(max_txn_retry)"` AggSumBackoffTimes int `json:"sum_backoff_times" agg:"SUM(sum_backoff_times)"` AggAvgMem int `json:"avg_mem" agg:"CAST(SUM(exec_count * avg_mem) / SUM(exec_count) AS SIGNED)"` AggMaxMem int `json:"max_mem" agg:"MAX(max_mem)"` AggAvgMemArbitration float64 `json:"avg_mem_arbitration" agg:"SUM(exec_count * avg_mem_arbitration) / SUM(exec_count)"` AggMaxMemArbitration float64 `json:"max_mem_arbitration" agg:"MAX(max_mem_arbitration)"` AggAvgDisk int `json:"avg_disk" agg:"CAST(SUM(exec_count * avg_disk) / SUM(exec_count) AS SIGNED)"` AggMaxDisk int `json:"max_disk" agg:"MAX(max_disk)"` AggAvgAffectedRows int `json:"avg_affected_rows" agg:"CAST(SUM(exec_count * avg_affected_rows) / SUM(exec_count) AS SIGNED)"` AggFirstSeen int `json:"first_seen" agg:"UNIX_TIMESTAMP(MIN(first_seen))"` AggLastSeen int `json:"last_seen" agg:"UNIX_TIMESTAMP(MAX(last_seen))"` AggSampleUser string `json:"sample_user" agg:"ANY_VALUE(sample_user)"` AggQuerySampleText string `json:"query_sample_text" agg:"ANY_VALUE(query_sample_text)"` AggPrevSampleText string `json:"prev_sample_text" agg:"ANY_VALUE(prev_sample_text)"` AggSchemaName string `json:"schema_name" agg:"ANY_VALUE(schema_name)"` AggTableNames string `json:"table_names" agg:"ANY_VALUE(table_names)"` AggIndexNames string `json:"index_names" agg:"ANY_VALUE(index_names)"` AggPlanCount int `json:"plan_count" agg:"COUNT(DISTINCT plan_digest)" related:"plan_digest"` AggPlan string `json:"plan" agg:"ANY_VALUE(plan)"` // deprecated, replaced by BinaryPlanText AggBinaryPlan string `json:"binary_plan" agg:"ANY_VALUE(binary_plan)"` AggPlanDigest string `json:"plan_digest" agg:"ANY_VALUE(plan_digest)"` AggPlanHint *string `json:"plan_hint" agg:"ANY_VALUE(plan_hint)"` AggPlanCacheHits int `json:"plan_cache_hits" agg:"SUM(plan_cache_hits)"` // RocksDB AggMaxRocksdbDeleteSkippedCount uint `json:"max_rocksdb_delete_skipped_count" agg:"MAX(max_rocksdb_delete_skipped_count)"` AggAvgRocksdbDeleteSkippedCount uint `json:"avg_rocksdb_delete_skipped_count" agg:"CAST(SUM(exec_count * avg_rocksdb_delete_skipped_count) / SUM(exec_count) as SIGNED)"` AggMaxRocksdbKeySkippedCount uint `json:"max_rocksdb_key_skipped_count" agg:"MAX(max_rocksdb_key_skipped_count)"` AggAvgRocksdbKeySkippedCount uint `json:"avg_rocksdb_key_skipped_count" agg:"CAST(SUM(exec_count * avg_rocksdb_key_skipped_count) / SUM(exec_count) as SIGNED)"` AggMaxRocksdbBlockCacheHitCount uint `json:"max_rocksdb_block_cache_hit_count" agg:"MAX(max_rocksdb_block_cache_hit_count)"` AggAvgRocksdbBlockCacheHitCount uint `json:"avg_rocksdb_block_cache_hit_count" agg:"CAST(SUM(exec_count * avg_rocksdb_block_cache_hit_count) / SUM(exec_count) as SIGNED)"` AggMaxRocksdbBlockReadCount uint `json:"max_rocksdb_block_read_count" agg:"MAX(max_rocksdb_block_read_count)"` AggAvgRocksdbBlockReadCount uint `json:"avg_rocksdb_block_read_count" agg:"CAST(SUM(exec_count * avg_rocksdb_block_read_count) / SUM(exec_count) as SIGNED)"` AggMaxRocksdbBlockReadByte uint `json:"max_rocksdb_block_read_byte" agg:"MAX(max_rocksdb_block_read_byte)"` AggAvgRocksdbBlockReadByte uint `json:"avg_rocksdb_block_read_byte" agg:"CAST(SUM(exec_count * avg_rocksdb_block_read_byte) / SUM(exec_count) as SIGNED)"` // Computed fields RelatedSchemas string `json:"related_schemas"` PlanCanBeBound bool `json:"plan_can_be_bound"` BinaryPlanJSON string `json:"binary_plan_json"` BinaryPlanText string `json:"binary_plan_text"` // Resource Control AggResourceGroup string `json:"resource_group" agg:"ANY_VALUE(resource_group)"` AggAvgRU float64 `json:"avg_ru" agg:"CAST(AVG(avg_request_unit_write + avg_request_unit_read) AS DECIMAL(64, 2))" related:"avg_request_unit_write,avg_request_unit_read"` AggMaxRU float64 `json:"max_ru" agg:"MAX(max_request_unit_write + max_request_unit_read)" related:"max_request_unit_write,max_request_unit_read"` AggSumRU float64 `json:"sum_ru" agg:"CAST(SUM(exec_count * (avg_request_unit_write + avg_request_unit_read)) AS DECIMAL(64, 2))" related:"avg_request_unit_write,avg_request_unit_read"` AvgQueuedTime float64 `json:"avg_time_queued_by_rc" agg:"CAST(AVG(AVG_QUEUED_RC_TIME) AS DECIMAL(64, 2))" related:"AVG_QUEUED_RC_TIME"` MaxQueuedTime float64 `json:"max_time_queued_by_rc" agg:"Max(MAX_QUEUED_RC_TIME)" related:"MAX_QUEUED_RC_TIME"` // Network Fields SumUnpackedBytesSentTiKVTotal uint `json:"sum_unpacked_bytes_sent_tikv_total" agg:"SUM(sum_unpacked_bytes_sent_tikv_total)"` SumUnpackedBytesReceivedTiKVTotal uint `json:"sum_unpacked_bytes_received_tikv_total" agg:"SUM(sum_unpacked_bytes_received_tikv_total)"` SumUnpackedBytesSentTiKVCrossZone uint `json:"sum_unpacked_bytes_sent_tikv_cross_zone" agg:"SUM(sum_unpacked_bytes_sent_tikv_cross_zone)"` SumUnpackedBytesReceivedTiKVCrossZone uint `json:"sum_unpacked_bytes_received_tikv_cross_zone" agg:"SUM(sum_unpacked_bytes_received_tikv_cross_zone)"` SumUnpackedBytesSentTiFlashTotal uint `json:"sum_unpacked_bytes_sent_tiflash_total" agg:"SUM(sum_unpacked_bytes_sent_tiflash_total)"` SumUnpackedBytesReceivedTiFlashTotal uint `json:"sum_unpacked_bytes_received_tiflash_total" agg:"SUM(sum_unpacked_bytes_received_tiflash_total)"` SumUnpackedBytesSentTiFlashCrossZone uint `json:"sum_unpacked_bytes_sent_tiflash_cross_zone" agg:"SUM(sum_unpacked_bytes_sent_tiflash_cross_zone)"` SumUnpackedBytesReceivedTiFlashCrossZone uint `json:"sum_unpacked_bytes_received_tiflash_cross_zone" agg:"SUM(sum_unpacked_bytes_received_tiflash_cross_zone)"` // IA remote read segment metrics SumIaReadSegmentCount int64 `json:"sum_ia_read_segment_count" agg:"SUM(sum_ia_read_segment_count)"` SumIaRemoteReadSegmentSize int64 `json:"sum_ia_remote_read_segment_size" agg:"SUM(sum_ia_remote_read_segment_size)"` SumIaRemoteReadSegmentWaitTime float64 `json:"sum_ia_remote_read_segment_wait_time" agg:"SUM(sum_ia_remote_read_segment_wait_time)"` AvgIaReadSegmentCount float64 `json:"avg_ia_read_segment_count" agg:"SUM(exec_count * avg_ia_read_segment_count) / SUM(exec_count)"` AvgIaRemoteReadSegmentSize float64 `json:"avg_ia_remote_read_segment_size" agg:"SUM(exec_count * avg_ia_remote_read_segment_size) / SUM(exec_count)"` AvgIaRemoteReadSegmentWaitTime float64 `json:"avg_ia_remote_read_segment_wait_time" agg:"SUM(exec_count * avg_ia_remote_read_segment_wait_time) / SUM(exec_count)"` MaxIaReadSegmentCount int64 `json:"max_ia_read_segment_count" agg:"MAX(max_ia_read_segment_count)"` MaxIaRemoteReadSegmentSize int64 `json:"max_ia_remote_read_segment_size" agg:"MAX(max_ia_remote_read_segment_size)"` MaxIaRemoteReadSegmentWaitTime float64 `json:"max_ia_remote_read_segment_wait_time" agg:"MAX(max_ia_remote_read_segment_wait_time)"` } // tableNames example: "d1.a1,d2.a2,d1.a1,d3.a3" // return "d1, d2, d3". func extractSchemasFromTableNames(tableNames string) string { schemas := make(map[string]bool) tables := strings.SplitSeq(tableNames, ",") for v := range tables { schema := strings.Trim(strings.Split(v, ".")[0], " ") if len(schema) > 0 { schemas[schema] = true } } keys := make([]string, 0, len(schemas)) for k := range schemas { keys = append(keys, k) } return strings.Join(keys, ", ") } // checkSupportPlanBinding checks if whether the plan can be bound manually with sql `CREATE GLOBAL BINDING FROM HISTORY USING PLAN DIGEST '%s'`. func (m *Model) checkSupportPlanBinding() bool { if !lo.Contains([]string{"SELECT", "DELETE", "UPDATE", "INSERT", "REPLACE"}, strings.ToUpper(m.AggStmtType)) { return false } if m.AggPlanHint != nil && *m.AggPlanHint == "" { return false } return true } func (m *Model) AfterFind(_ *gorm.DB) error { if len(m.AggTableNames) > 0 { m.RelatedSchemas = extractSchemasFromTableNames(m.AggTableNames) } m.PlanCanBeBound = m.checkSupportPlanBinding() return nil } type Field struct { ColumnName string JSONName string // `related` tag is used to verify a non-existent column, which is aggregated from the columns represented by related. Related []string Aggregation string } var gormDefaultNamingStrategy = schema.NamingStrategy{} func getFieldsAndTags() (stmtFields []Field) { fields := reflectutil.GetFieldsAndTags(Model{}, []string{"related", "agg", "json"}) for _, f := range fields { sf := Field{ ColumnName: gormDefaultNamingStrategy.ColumnName("", f.Name), JSONName: f.Tags["json"], Related: []string{}, Aggregation: f.Tags["agg"], } if f.Tags["related"] != "" { sf.Related = strings.Split(f.Tags["related"], ",") } stmtFields = append(stmtFields, sf) } return } func filterFieldsByColumns(fields []Field, columns []string) []Field { colMap := map[string]struct{}{} for _, c := range columns { colMap[strings.ToLower(c)] = struct{}{} } filteredFields := []Field{} for _, f := range fields { // The json name of Statement is currently exactly the same as the table column name // TODO: use util.VirtualView instead of the convention in the comment _, ok := colMap[strings.ToLower(f.JSONName)] if ok || (len(f.Related) != 0 && utils.IsSubsetICaseInsensitive(columns, f.Related)) { filteredFields = append(filteredFields, f) } } return filteredFields } // Binding struct maps to the response of `SHOW BINDINGS` query. type Binding struct { Status string `json:"status" example:"enabled" enums:"enabled,using,disabled,deleted,invalid,rejected,pending verify"` Source string `json:"source" example:"manual" enums:"manual,history,capture,evolve"` SQLDigest string `json:"-" gorm:"column:Sql_digest"` PlanDigest string `json:"plan_digest" gorm:"column:Plan_digest"` } ================================================ FILE: pkg/apiserver/statement/module.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package statement import "go.uber.org/fx" var Module = fx.Options( fx.Provide(newService), fx.Invoke(registerRouter), ) ================================================ FILE: pkg/apiserver/statement/queries.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package statement import ( "context" "fmt" "regexp" "strings" "time" "github.com/pingcap/errors" "gorm.io/gorm" ) const ( statementsTable = "INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY" ) var digestInjectChecker = regexp.MustCompile(`^[a-zA-Z0-9]+$`) func queryStmtTypes(db *gorm.DB) (result []string, err error) { // why should put DISTINCT inside the `Pluck()` method, see here: // https://github.com/jinzhu/gorm/issues/496 err = db. Table(statementsTable). Order("stmt_type ASC"). Pluck("DISTINCT stmt_type", &result). Error return } // sample params: // beginTime: 1586844000 // endTime: 1586845800 // schemas: ["tpcc", "test"] // stmtTypes: ["select", "update"] // fields: ["digest_text", "sum_latency"] func (s *Service) queryStatements( db *gorm.DB, beginTime, endTime int, schemas, resourceGroups, stmtTypes []string, text string, reqFields []string, ) (result []Model, err error) { tableColumns, err := s.params.SysSchema.GetTableColumnNames(db, statementsTable) if err != nil { return nil, err } selectStmt, err := s.genSelectStmt(tableColumns, reqFields) if err != nil { return nil, err } query := db. Select(selectStmt). Table(statementsTable). // https://stackoverflow.com/questions/3269434/whats-the-most-efficient-way-to-test-if-two-ranges-overlap Where("summary_begin_time <= FROM_UNIXTIME(?) AND summary_end_time >= FROM_UNIXTIME(?)", endTime, beginTime). Group("schema_name, digest"). Order("agg_sum_latency DESC") if len(schemas) > 0 { regex := make([]string, 0, len(schemas)) for _, schema := range schemas { regex = append(regex, fmt.Sprintf("\\b%s\\.", regexp.QuoteMeta(schema))) } regexAll := strings.Join(regex, "|") query = query.Where("table_names REGEXP ?", regexAll) } if len(resourceGroups) > 0 { query = query.Where("resource_group in (?)", resourceGroups) } if len(stmtTypes) > 0 { query = query.Where("stmt_type in (?)", stmtTypes) } if len(text) > 0 { lowerText := strings.ToLower(text) arr := strings.FieldsSeq(lowerText) for v := range arr { query = query.Where( `LOWER(digest_text) REGEXP ? OR LOWER(digest) REGEXP ? OR LOWER(schema_name) REGEXP ? OR LOWER(table_names) REGEXP ?`, v, v, v, v, ) } } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() err = query.WithContext(ctx).Find(&result).Error if err == nil { // truncate each row's digest_text, keep the start 1000 characters to avoid too long text // if user want to see the full digest_text, they can access the detail api for i := range result { if len(result[i].AggDigestText) > 1000 { result[i].AggDigestText = result[i].AggDigestText[:1000] + "..." } } } return } func (s *Service) queryPlans( db *gorm.DB, beginTime, endTime int, schemaName, digest string, ) (result []Model, err error) { tableColumns, err := s.params.SysSchema.GetTableColumnNames(db, statementsTable) if err != nil { return nil, err } selectStmt, err := s.genSelectStmt(tableColumns, []string{ "plan_digest", "schema_name", "digest_text", "digest", "sum_latency", "max_latency", "min_latency", "avg_latency", "exec_count", "avg_mem", "max_mem", "stmt_type", // required by quick plan binding "plan_hint", // required by quick plan binding, only available in TiDB 6.6.0+, could be filter out by `tableColumns` }) if err != nil { return nil, err } query := db. Select(selectStmt). Table(statementsTable). Where("summary_begin_time <= FROM_UNIXTIME(?) AND summary_end_time >= FROM_UNIXTIME(?)", endTime, beginTime). Group("plan_digest") if digest == "" { // the evicted record's digest will be NULL query.Where("digest IS NULL") } else { if schemaName != "" { query.Where("schema_name = ?", schemaName) } query.Where("digest = ?", digest) } err = query.Find(&result).Error return } func (s *Service) queryPlanDetail( db *gorm.DB, beginTime, endTime int, schemaName, digest string, plans []string, ) (result Model, err error) { tableColumns, err := s.params.SysSchema.GetTableColumnNames(db, statementsTable) if err != nil { return } selectStmt, err := s.genSelectStmt(tableColumns, []string{"*"}) if err != nil { return } query := db. Select(selectStmt). Table(statementsTable). Where("summary_begin_time <= FROM_UNIXTIME(?) AND summary_end_time >= FROM_UNIXTIME(?)", endTime, beginTime) if digest == "" { // the evicted record's digest will be NULL query.Where("digest IS NULL") } else { if schemaName != "" { query.Where("schema_name = ?", schemaName) } if len(plans) > 0 { query = query.Where("plan_digest in (?)", plans) } query.Where("digest = ?", digest) } err = query.Scan(&result).Error return } func (s *Service) queryPlanBinding(db *gorm.DB, sqlDigest string, beginTime, endTime int) (bindings []Binding, err error) { // The binding sql digest is newly generated and different from the original sql digest, // we have to do one more query here. // First, get plan digests by sql digest. q1 := db. Table(statementsTable). Select("plan_digest"). Where("digest = ? AND summary_begin_time <= FROM_UNIXTIME(?) AND summary_end_time > FROM_UNIXTIME(?)", sqlDigest, endTime, beginTime) q1Res := make([]map[string]any, 0) if err := q1.Find(&q1Res).Error; err != nil { return nil, err } planDigests := make([]string, 0, len(q1Res)) for _, row := range q1Res { s, ok := row["plan_digest"].(string) if !ok { return nil, errors.New("invalid plan digest value") } planDigests = append(planDigests, s) } // Second, get bindings. query := db.Raw("SHOW GLOBAL BINDINGS WHERE plan_digest IN (?) AND source = ? AND status IN (?)", planDigests, "history", []string{"enabled", "using"}) return bindings, query.Scan(&bindings).Error } func (s *Service) createPlanBinding(db *gorm.DB, planDigest string) (err error) { // Caution! SQL injection vulnerability! // We have to interpolate sql string here, since plan binding stmt does not support session level prepare. // go-sql-driver can enable interpolation globally. Refer to https://github.com/go-sql-driver/mysql#interpolateparams. if !digestInjectChecker.MatchString(planDigest) { return errors.New("invalid planDigest") } query := db.Exec(fmt.Sprintf("CREATE GLOBAL BINDING FROM HISTORY USING PLAN DIGEST '%s'", planDigest)) return query.Error } func (s *Service) dropPlanBinding(db *gorm.DB, sqlDigest string) (err error) { // The binding sql digest is newly generated and different from the original sql digest, // we have to do one more query here. bindings, err := s.queryPlanBinding(db, sqlDigest, 0, int(time.Now().Unix())) if err != nil { return err } if len(bindings) <= 0 { return errors.New("no binding found") } for _, binding := range bindings { // No SQL injection vulnerability here. query := db.Exec(fmt.Sprintf("DROP GLOBAL BINDING FOR SQL DIGEST '%s'", binding.SQLDigest)) if query.Error != nil { return query.Error } } return nil } ================================================ FILE: pkg/apiserver/statement/service.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package statement import ( "fmt" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/pingcap/errors" "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/featureflag" "github.com/pingcap/tidb-dashboard/util/rest" ) var ( ErrNS = errorx.NewNamespace("error.api.statement") ErrNoData = ErrNS.NewType("export_no_data") ) type ServiceParams struct { fx.In TiDBClient *tidb.Client SysSchema *commonUtils.SysSchema } type Service struct { params ServiceParams planBindingFeatureFlag *featureflag.FeatureFlag } func newService(p ServiceParams, ff *featureflag.Registry) *Service { return &Service{params: p, planBindingFeatureFlag: ff.Register("plan_binding", ">= 6.5.0")} } func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/statements") { endpoint.GET("/download", s.downloadHandler) endpoint.Use(auth.MWAuthRequired()) endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) { endpoint.POST("/download/token", s.downloadTokenHandler) endpoint.GET("/config", s.configHandler) endpoint.POST("/config", auth.MWRequireWritePriv(), s.modifyConfigHandler) endpoint.GET("/stmt_types", s.stmtTypesHandler) endpoint.GET("/list", s.listHandler) endpoint.GET("/plans", s.plansHandler) endpoint.GET("/plan/detail", s.planDetailHandler) endpoint.GET("/available_fields", s.getAvailableFields) binding := endpoint.Group("/plan/binding") binding.Use(s.planBindingFeatureFlag.VersionGuard()) { binding.GET("", s.getPlanBindingHandler) binding.POST("", s.createPlanBindingHandler) binding.DELETE("", s.dropPlanBindingHandler) } } } } type EditableConfig struct { Enable bool `json:"enable" gorm:"column:tidb_enable_stmt_summary"` RefreshInterval int `json:"refresh_interval" gorm:"column:tidb_stmt_summary_refresh_interval"` HistorySize int `json:"history_size" gorm:"column:tidb_stmt_summary_history_size"` MaxSize int `json:"max_size" gorm:"column:tidb_stmt_summary_max_stmt_count"` InternalQuery bool `json:"internal_query" gorm:"column:tidb_stmt_summary_internal_query"` } // @Summary Get statement configurations // @Success 200 {object} statement.EditableConfig // @Router /statements/config [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) configHandler(c *gin.Context) { db := utils.GetTiDBConnection(c) cfg := &EditableConfig{} err := db.Raw(buildGlobalConfigProjectionSelectSQL(cfg)).Find(cfg).Error if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, cfg) } // @Summary Update statement configurations // @Param request body statement.EditableConfig true "Request body" // @Success 204 {object} string // @Router /statements/config [post] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) modifyConfigHandler(c *gin.Context) { var config EditableConfig if err := c.ShouldBindJSON(&config); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } db := utils.GetTiDBConnection(c) var sqlWithNamedArgument string if !config.Enable { sqlWithNamedArgument = buildGlobalConfigNamedArgsUpdateSQL(&config, "Enable") } else { sqlWithNamedArgument = buildGlobalConfigNamedArgsUpdateSQL(&config) } err := db.Exec(sqlWithNamedArgument, &config).Error if err != nil { rest.Error(c, err) return } c.Status(http.StatusNoContent) } // @Summary Get all statement types // @Success 200 {array} string // @Router /statements/stmt_types [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) stmtTypesHandler(c *gin.Context) { db := utils.GetTiDBConnection(c) stmtTypes, err := queryStmtTypes(db) if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, stmtTypes) } type GetStatementsRequest struct { Schemas []string `json:"schemas" form:"schemas"` ResourceGroups []string `json:"resource_groups" form:"resource_groups"` StmtTypes []string `json:"stmt_types" form:"stmt_types"` BeginTime int `json:"begin_time" form:"begin_time"` EndTime int `json:"end_time" form:"end_time"` Text string `json:"text" form:"text"` Fields string `json:"fields" form:"fields"` } // @Summary Get a list of statements // @Param q query GetStatementsRequest true "Query" // @Success 200 {array} Model // @Router /statements/list [get] // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse func (s *Service) listHandler(c *gin.Context) { var req GetStatementsRequest if err := c.ShouldBindQuery(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } db := utils.GetTiDBConnection(c) fields := []string{} if strings.TrimSpace(req.Fields) != "" { fields = strings.Split(req.Fields, ",") } overviews, err := s.queryStatements( db, req.BeginTime, req.EndTime, req.Schemas, req.ResourceGroups, req.StmtTypes, req.Text, fields) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } c.JSON(http.StatusOK, overviews) } type GetPlansRequest struct { SchemaName string `json:"schema_name" form:"schema_name"` Digest string `json:"digest" form:"digest"` BeginTime int `json:"begin_time" form:"begin_time"` EndTime int `json:"end_time" form:"end_time"` } // @Summary Get execution plans of a statement // @Param q query GetPlansRequest true "Query" // @Success 200 {array} Model // @Router /statements/plans [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) plansHandler(c *gin.Context) { var req GetPlansRequest if err := c.ShouldBindQuery(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } db := utils.GetTiDBConnection(c) plans, err := s.queryPlans(db, req.BeginTime, req.EndTime, req.SchemaName, req.Digest) if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, plans) } type GetPlanDetailRequest struct { GetPlansRequest Plans []string `json:"plans" form:"plans"` } // @Summary Get details of a statement in an execution plan // @Param q query GetPlanDetailRequest true "Query" // @Success 200 {object} Model // @Router /statements/plan/detail [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) planDetailHandler(c *gin.Context) { var req GetPlanDetailRequest if err := c.ShouldBindQuery(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } db := utils.GetTiDBConnection(c) result, err := s.queryPlanDetail(db, req.BeginTime, req.EndTime, req.SchemaName, req.Digest, req.Plans) if err != nil { rest.Error(c, err) return } if result.AggBinaryPlan != "" { // may failed but it's ok result.BinaryPlanText, err = utils.GenerateBinaryPlanText(db, result.AggBinaryPlan) // may failed but it's ok result.BinaryPlanJSON, _ = utils.GenerateBinaryPlanJSON(result.AggBinaryPlan) if err == nil { // reduce response size result.AggBinaryPlan = "" result.AggPlan = "" } } c.JSON(http.StatusOK, result) } // @Summary Get the bound plan digest (if exists) of a statement // @Param sql_digest query string true "query template id" // @Param begin_time query int true "begin time" // @Param end_time query int true "end time" // @Success 200 {object} Binding // @Router /statements/plan/binding [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) getPlanBindingHandler(c *gin.Context) { digest := c.Query("sql_digest") if digest == "" { rest.Error(c, rest.ErrBadRequest.New("sql_digest cannot be empty")) return } bTimeS := c.Query("begin_time") bTime, err := strconv.Atoi(bTimeS) if err != nil { rest.Error(c, rest.ErrBadRequest.New("begin_time is not a valid timestamp second int")) return } eTimeS := c.Query("end_time") eTime, err := strconv.Atoi(eTimeS) if err != nil { rest.Error(c, rest.ErrBadRequest.New("end_time is not a valid timestamp second int")) return } db := utils.GetTiDBConnection(c) results, err := s.queryPlanBinding(db, digest, bTime, eTime) if err != nil { rest.Error(c, err) return } // Creating binding with the same plan digest will override the previous one. // Therefore, we only need to return the first result. var result *Binding if len(results) >= 1 { result = &results[0] } c.JSON(http.StatusOK, result) } // @Summary Create a binding for a statement and a plan // @Param plan_digest query string true "plan digest id" // @Success 200 {string} string "success" // @Router /statements/plan/binding [post] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) createPlanBindingHandler(c *gin.Context) { digest := c.Query("plan_digest") if digest == "" { rest.Error(c, rest.ErrBadRequest.New("plan_digest cannot be empty")) return } db := utils.GetTiDBConnection(c) err := s.createPlanBinding(db, digest) if err != nil { rest.Error(c, errors.Annotate(err, "create plan binding failed due to internal failure, please refer to https://docs.pingcap.com/tidb/stable/sql-plan-management")) return } c.String(http.StatusOK, "success") } // @Summary Drop all manually created bindings for a statement // @Param sql_digest query string true "query template ID (a.k.a. sql digest)" // @Success 200 {string} string "success" // @Router /statements/plan/binding [delete] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) dropPlanBindingHandler(c *gin.Context) { digest := c.Query("sql_digest") if digest == "" { rest.Error(c, rest.ErrBadRequest.New("sql_digest cannot be empty")) return } db := utils.GetTiDBConnection(c) err := s.dropPlanBinding(db, digest) if err != nil { rest.Error(c, err) return } c.String(http.StatusOK, "success") } // @Router /statements/download/token [post] // @Summary Generate a download token for exported statements // @Produce plain // @Param request body GetStatementsRequest true "Request body" // @Success 200 {string} string "xxx" // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse func (s *Service) downloadTokenHandler(c *gin.Context) { var req GetStatementsRequest if err := c.ShouldBindJSON(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } db := utils.GetTiDBConnection(c) fields := []string{} if strings.TrimSpace(req.Fields) != "" { fields = strings.Split(req.Fields, ",") } overviews, err := s.queryStatements( db, req.BeginTime, req.EndTime, req.Schemas, req.ResourceGroups, req.StmtTypes, req.Text, fields) if err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } if len(overviews) == 0 { rest.Error(c, ErrNoData.NewWithNoMessage()) return } // interface{} tricky rawData := make([]interface{}, len(overviews)) for i, v := range overviews { rawData[i] = v } // convert data csvData := utils.GenerateCSVFromRaw(rawData, fields, []string{"first_seen", "last_seen"}) // generate temp file that persist encrypted data timeLayout := "01021504" beginTime := time.Unix(int64(req.BeginTime), 0).Format(timeLayout) endTime := time.Unix(int64(req.EndTime), 0).Format(timeLayout) token, err := utils.ExportCSV(csvData, fmt.Sprintf("statements_%s_%s_*.csv", beginTime, endTime), "statements/download") if err != nil { rest.Error(c, err) return } c.String(http.StatusOK, token) } // @Router /statements/download [get] // @Summary Download statements // @Produce text/csv // @Param token query string true "download token" // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse func (s *Service) downloadHandler(c *gin.Context) { token := c.Query("token") utils.DownloadByToken(token, "statements/download", c) } // @Summary Get available field names // @Description Get available field names by statements table columns // @Success 200 {array} string // @Failure 401 {object} rest.ErrorResponse // @Security JwtAuth // @Router /statements/available_fields [get] func (s *Service) getAvailableFields(c *gin.Context) { db := utils.GetTiDBConnection(c) cs, err := s.params.SysSchema.GetTableColumnNames(db, statementsTable) if err != nil { rest.Error(c, err) return } fields := filterFieldsByColumns(getFieldsAndTags(), cs) jsonNames := make([]string, 0, len(fields)) for _, f := range fields { jsonNames = append(jsonNames, f.JSONName) } c.JSON(http.StatusOK, jsonNames) } ================================================ FILE: pkg/apiserver/statement/statement_gen.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package statement import ( "fmt" "strings" "github.com/samber/lo" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap/tidb-dashboard/util/distro" ) var ErrUnknownColumn = ErrNS.NewType("unknown_column") func (s *Service) genSelectStmt(tableColumns []string, reqJSONColumns []string) (string, error) { fields := getFieldsAndTags() // use required fields filter when not all fields are requested if reqJSONColumns[0] != "*" { requiredFields := lo.Uniq(append(reqJSONColumns, "schema_name", "digest", // required by group by "sum_latency", // required by order "summary_begin_time", "summary_end_time", )) fields = lo.Filter(fields, func(f Field, _ int) bool { return lo.Contains(requiredFields, f.JSONName) }) } // Filter out columns that do not exist in current version TiDB schema. // Current version TiDB schema columns are passed in by `tableColumns`. fields = lo.Filter(fields, func(f Field, _ int) bool { var representedColumns []string if len(f.Related) != 0 { representedColumns = f.Related } else { representedColumns = []string{f.JSONName} } // Dependent columns of the requested field must exist in the db schema. Otherwise, the requested field will be ignored. return utils.IsSubsetICaseInsensitive(tableColumns, representedColumns) }) if len(fields) == 0 { return "", ErrUnknownColumn.New("all columns are not included in the current version %s schema, columns: %q", distro.R().TiDB, reqJSONColumns) } stmt := lo.Map(fields, func(f Field, _ int) string { if f.Aggregation == "" { return f.JSONName } return fmt.Sprintf("%s AS %s", f.Aggregation, f.ColumnName) }) return strings.Join(stmt, ", "), nil } ================================================ FILE: pkg/apiserver/topsql/module.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topsql import "go.uber.org/fx" var Module = fx.Options( fx.Provide(newService), fx.Invoke(registerRouter), ) ================================================ FILE: pkg/apiserver/topsql/service.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topsql import ( "bytes" "encoding/json" "net" "net/http" "sort" "strconv" "strings" "sync" "time" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "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/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/featureflag" "github.com/pingcap/tidb-dashboard/util/rest" ) var ErrNS = errorx.NewNamespace("error.api.topsql") type ServiceParams struct { fx.In TiDBClient *tidb.Client NgmProxy *utils.NgmProxy PDClient *pd.Client TiKVClient *tikv.Client } type Service struct { FeatureTopSQL *featureflag.FeatureFlag params ServiceParams } func newService(p ServiceParams, ff *featureflag.Registry) *Service { return &Service{params: p, FeatureTopSQL: ff.Register("topsql", ">= 5.4.0")} } func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/topsql") endpoint.Use( auth.MWAuthRequired(), s.FeatureTopSQL.VersionGuard(), utils.MWConnectTiDB(s.params.TiDBClient), ) { endpoint.GET("/config", s.GetConfig) endpoint.POST("/config", auth.MWRequireWritePriv(), s.UpdateConfig) endpoint.GET("/tikv_network_io_collection", s.GetTiKVNetworkIOCollection) endpoint.POST( "/tikv_network_io_collection", auth.MWRequireWritePriv(), s.UpdateTiKVNetworkIOCollection, ) endpoint.GET("/instances", s.params.NgmProxy.Route("/topsql/v1/instances")) endpoint.GET("/summary", s.params.NgmProxy.Route("/topsql/v1/summary")) } } type GetInstancesRequest struct { Start string `json:"start"` End string `json:"end"` DataSource string `json:"data_source"` } type InstanceResponse struct { Data []InstanceItem `json:"data"` } type InstanceItem struct { Instance string `json:"instance"` InstanceType string `json:"instance_type"` } // @Summary Get available instances // @Router /topsql/instances [get] // @Security JwtAuth // @Param q query GetInstancesRequest true "Query" // @Success 200 {object} InstanceResponse "ok" // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) GetInstance(_ *gin.Context) { // dummy, for generate open api } type GetSummaryRequest struct { Instance string `json:"instance"` InstanceType string `json:"instance_type"` Start string `json:"start"` End string `json:"end"` Top string `json:"top"` GroupBy string `json:"group_by"` OrderBy string `json:"order_by"` Window string `json:"window"` DataSource string `json:"data_source"` } type SummaryResponse struct { Data []SummaryItem `json:"data"` DataBy []SummaryByItem `json:"data_by"` } type SummaryItem struct { SQLDigest string `json:"sql_digest"` SQLText string `json:"sql_text"` IsOther bool `json:"is_other"` CPUTimeMs uint64 `json:"cpu_time_ms"` ExecCountPerSec float64 `json:"exec_count_per_sec"` DurationPerExecMs float64 `json:"duration_per_exec_ms"` ScanRecordsPerSec float64 `json:"scan_records_per_sec"` ScanIndexesPerSec float64 `json:"scan_indexes_per_sec"` NetworkBytes uint64 `json:"network_bytes"` LogicalIoBytes uint64 `json:"logical_io_bytes"` Plans []SummaryPlanItem `json:"plans"` } type SummaryByItem struct { Text string `json:"text"` TimestampSec []uint64 `json:"timestamp_sec"` CPUTimeMs []uint64 `json:"cpu_time_ms,omitempty"` CPUTimeMsSum uint64 `json:"cpu_time_ms_sum"` NetworkBytes []uint64 `json:"network_bytes,omitempty"` NetworkBytesSum uint64 `json:"network_bytes_sum"` LogicalIoBytes []uint64 `json:"logical_io_bytes,omitempty"` LogicalIoBytesSum uint64 `json:"logical_io_bytes_sum"` } type SummaryPlanItem struct { PlanDigest string `json:"plan_digest"` PlanText string `json:"plan_text"` TimestampSec []uint64 `json:"timestamp_sec"` CPUTimeMs []uint64 `json:"cpu_time_ms,omitempty"` ExecCountPerSec float64 `json:"exec_count_per_sec"` DurationPerExecMs float64 `json:"duration_per_exec_ms"` ScanRecordsPerSec float64 `json:"scan_records_per_sec"` ScanIndexesPerSec float64 `json:"scan_indexes_per_sec"` NetworkBytes []uint64 `json:"network_bytes"` LogicalIoBytes []uint64 `json:"logical_io_bytes"` } // @Summary Get summaries // @Router /topsql/summary [get] // @Security JwtAuth // @Param q query GetSummaryRequest true "Query" // @Success 200 {object} SummaryResponse "ok" // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) GetSummary(_ *gin.Context) { // dummy, for generate open api } type EditableConfig struct { Enable bool `json:"enable" gorm:"column:tidb_enable_top_sql"` } // @Summary Get Top SQL config // @Router /topsql/config [get] // @Security JwtAuth // @Success 200 {object} EditableConfig "ok" // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) GetConfig(c *gin.Context) { db := utils.GetTiDBConnection(c) cfg := &EditableConfig{} err := db.Raw("SELECT @@GLOBAL.tidb_enable_top_sql as tidb_enable_top_sql").Find(cfg).Error if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, cfg) } // @Summary Update Top SQL config // @Router /topsql/config [post] // @Param request body EditableConfig true "Request body" // @Security JwtAuth // @Success 204 {object} string // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) UpdateConfig(c *gin.Context) { var cfg EditableConfig if err := c.ShouldBindJSON(&cfg); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } db := utils.GetTiDBConnection(c) err := db.Exec("SET @@GLOBAL.tidb_enable_top_sql = @Enable", &cfg).Error if err != nil { rest.Error(c, err) return } c.Status(http.StatusNoContent) } const ( tikvNetworkIoCollectionKey = "resource-metering.enable-network-io-collection" tikvNetworkIoCollectionNodeTimeout = 3 * time.Second tikvNetworkIoCollectionMaxConcurrency = 10 ) type TikvNetworkIoCollectionConfig struct { Enable bool `json:"enable"` IsMultiValue bool `json:"is_multi_value,omitempty"` } type UpdateTikvNetworkIoCollectionResponse struct { Warnings []rest.ErrorResponse `json:"warnings"` } // @ID topsqlGetTiKVNetworkIOCollection // @Summary Get TiKV network IO collection config // @Router /topsql/tikv_network_io_collection [get] // @Security JwtAuth // @Success 200 {object} TikvNetworkIoCollectionConfig "ok" // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) GetTiKVNetworkIOCollection(c *gin.Context) { tikvInfo, _, err := topology.FetchStoreTopology(s.params.PDClient) if err != nil { rest.Error(c, err) return } if len(tikvInfo) == 0 { c.JSON(http.StatusOK, &TikvNetworkIoCollectionConfig{Enable: false}) return } type getResult struct { value bool found bool err error } concurrency := getTiKVNetworkIoCollectionConcurrency(len(tikvInfo)) taskChan := make(chan topology.StoreInfo, len(tikvInfo)) resultChan := make(chan getResult, len(tikvInfo)) var wg sync.WaitGroup for range concurrency { wg.Go(func() { for kvStore := range taskChan { data, err := s.params.TiKVClient. WithTimeout(tikvNetworkIoCollectionNodeTimeout). SendGetRequest(kvStore.IP, int(kvStore.StatusPort), "/config") if err != nil { resultChan <- getResult{err: err} continue } v, found, err := parseNestedBoolByDotPath(data, tikvNetworkIoCollectionKey) if err != nil { resultChan <- getResult{err: err} continue } resultChan <- getResult{value: v, found: found} } }) } for _, kvStore := range tikvInfo { taskChan <- kvStore } close(taskChan) wg.Wait() close(resultChan) successes := 0 failures := 0 trueCount := 0 falseCount := 0 hasMissing := false for result := range resultChan { if result.err != nil { failures++ continue } successes++ // Keep existing semantics: missing key is treated as "false". if !result.found { hasMissing = true falseCount++ continue } if result.value { trueCount++ } else { falseCount++ } } if successes == 0 { rest.Error(c, errorx.IllegalState.New("Failed to fetch config from any TiKV node")) return } // Keep existing semantics: // 1. Any failed request means "not enabled on all nodes". // 2. Missing config key is treated as false and marks multi-value. allTrue := failures == 0 && !hasMissing && falseCount == 0 isMulti := failures > 0 || hasMissing || (trueCount > 0 && falseCount > 0) c.JSON(http.StatusOK, &TikvNetworkIoCollectionConfig{ Enable: allTrue, IsMultiValue: isMulti, }) } // @ID topsqlUpdateTiKVNetworkIOCollection // @Summary Update TiKV network IO collection config // @Param request body TikvNetworkIoCollectionConfig true "Request body" // @Router /topsql/tikv_network_io_collection [post] // @Security JwtAuth // @Success 200 {object} UpdateTikvNetworkIoCollectionResponse "ok" // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) UpdateTiKVNetworkIOCollection(c *gin.Context) { var cfg TikvNetworkIoCollectionConfig if err := c.ShouldBindJSON(&cfg); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } tikvInfo, _, err := topology.FetchStoreTopology(s.params.PDClient) if err != nil { rest.Error(c, err) return } body := map[string]interface{}{ tikvNetworkIoCollectionKey: cfg.Enable, } bodyJSON, err := json.Marshal(&body) if err != nil { rest.Error(c, err) return } type postResult struct { target string err error } concurrency := getTiKVNetworkIoCollectionConcurrency(len(tikvInfo)) taskChan := make(chan topology.StoreInfo, len(tikvInfo)) resultChan := make(chan postResult, len(tikvInfo)) var wg sync.WaitGroup for range concurrency { wg.Go(func() { for kvStore := range taskChan { target := net.JoinHostPort(kvStore.IP, strconv.Itoa(int(kvStore.Port))) _, err := s.params.TiKVClient. WithTimeout(tikvNetworkIoCollectionNodeTimeout). SendPostRequest( kvStore.IP, int(kvStore.StatusPort), "/config", bytes.NewBuffer(bodyJSON), ) resultChan <- postResult{target: target, err: err} } }) } for _, kvStore := range tikvInfo { taskChan <- kvStore } close(taskChan) wg.Wait() close(resultChan) failures := make([]error, 0) failedStores := make([]string, 0) for result := range resultChan { if result.err == nil { continue } failedStores = append(failedStores, result.target) failures = append( failures, errorx.Decorate(result.err, "Failed to edit config for TiKV instance `%s`", result.target), ) } if len(failures) == len(tikvInfo) && len(failures) > 0 { sort.Strings(failedStores) rest.Error(c, errorx.Decorate( failures[0], "Failed to edit config for all TiKV instances: %s", strings.Join(failedStores, ", "), )) return } sort.Slice(failures, func(i, j int) bool { return failures[i].Error() < failures[j].Error() }) warnings := make([]rest.ErrorResponse, 0) for _, err := range failures { warnings = append(warnings, rest.NewErrorResponse(err)) } c.JSON(http.StatusOK, &UpdateTikvNetworkIoCollectionResponse{Warnings: warnings}) } func getTiKVNetworkIoCollectionConcurrency(tikvCount int) int { concurrency := max(tikvCount/10, 1) if concurrency > tikvNetworkIoCollectionMaxConcurrency { concurrency = tikvNetworkIoCollectionMaxConcurrency } return concurrency } func parseNestedBoolByDotPath(data []byte, dotPath string) (value bool, found bool, err error) { var m map[string]interface{} if err := json.Unmarshal(data, &m); err != nil { return false, false, err } cur := interface{}(m) for _, p := range splitDotPath(dotPath) { obj, ok := cur.(map[string]interface{}) if !ok { return false, false, nil } next, ok := obj[p] if !ok { return false, false, nil } cur = next } switch v := cur.(type) { case bool: return v, true, nil case string: // Be tolerant if TiKV returns "true"/"false" if v == "true" { return true, true, nil } if v == "false" { return false, true, nil } return false, true, nil default: return false, true, nil } } func splitDotPath(dotPath string) []string { // Avoid importing strings for a single split; keep consistent with other packages. parts := make([]string, 0, 4) last := 0 for i := 0; i < len(dotPath); i++ { if dotPath[i] == '.' { parts = append(parts, dotPath[last:i]) last = i + 1 } } parts = append(parts, dotPath[last:]) return parts } ================================================ FILE: pkg/apiserver/user/auth.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package user import ( "crypto/rsa" "encoding/base64" "encoding/json" "errors" "net/http" "os" "sort" "time" jwt "github.com/appleboy/gin-jwt/v2" "github.com/gin-gonic/gin" "github.com/gtank/cryptopasta" "github.com/joomcode/errorx" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap/tidb-dashboard/util/featureflag" "github.com/pingcap/tidb-dashboard/util/rest" ) var ( ErrNS = errorx.NewNamespace("error.api.user") ErrUnsupportedAuthType = ErrNS.NewType("unsupported_auth_type") ErrNSSignIn = ErrNS.NewSubNamespace("signin") ErrSignInOther = ErrNSSignIn.NewType("other") ) type AuthService struct { FeatureFlagNonRootLogin *featureflag.FeatureFlag middleware *jwt.GinJWTMiddleware authenticators map[utils.AuthType]Authenticator RsaPublicKey *rsa.PublicKey RsaPrivateKey *rsa.PrivateKey } type AuthenticateForm struct { Type utils.AuthType `json:"type" example:"0"` Username string `json:"username" example:"root"` // Does not present for AuthTypeSharingCode Password string `json:"password"` Extra string `json:"extra"` // FIXME: Use strong type } type TokenResponse struct { Token string `json:"token"` Expire time.Time `json:"expire"` } type SignOutInfo struct { EndSessionURL string `json:"end_session_url"` } type Authenticator interface { IsEnabled() (bool, error) Authenticate(form AuthenticateForm) (*utils.SessionUser, error) ProcessSession(u *utils.SessionUser) bool SignOutInfo(u *utils.SessionUser, redirectURL string) (*SignOutInfo, error) } type BaseAuthenticator struct{} func (a BaseAuthenticator) IsEnabled() (bool, error) { return true, nil } func (a BaseAuthenticator) ProcessSession(_ *utils.SessionUser) bool { return true } func (a BaseAuthenticator) SignOutInfo(_ *utils.SessionUser, _ string) (*SignOutInfo, error) { return &SignOutInfo{}, nil } func NewAuthService(featureFlags *featureflag.Registry) *AuthService { var secret *[32]byte secretStr := os.Getenv("DASHBOARD_SESSION_SECRET") switch len(secretStr) { case 0: secret = cryptopasta.NewEncryptionKey() case 32: log.Info("DASHBOARD_SESSION_SECRET is overridden from env var") secret = &[32]byte{} copy(secret[:], secretStr) default: log.Warn("DASHBOARD_SESSION_SECRET does not meet the 32 byte size requirement, ignored") secret = cryptopasta.NewEncryptionKey() } privateKey, publicKey, err := GenerateKey() if err != nil { log.Fatal("Failed to generate rsa key pairs", zap.Error(err)) } service := &AuthService{ FeatureFlagNonRootLogin: featureFlags.Register("nonRootLogin", ">= 5.3.0"), middleware: nil, authenticators: map[utils.AuthType]Authenticator{}, RsaPrivateKey: privateKey, RsaPublicKey: publicKey, } middleware, err := jwt.New(&jwt.GinJWTMiddleware{ IdentityKey: utils.SessionUserKey, Realm: "dashboard", Key: secret[:], Timeout: time.Hour * 24, MaxRefresh: time.Hour * 24, Authenticator: func(c *gin.Context) (interface{}, error) { var form AuthenticateForm if err := c.ShouldBindJSON(&form); err != nil { return nil, rest.ErrBadRequest.WrapWithNoMessage(err) } u, err := service.authForm(form) if err != nil { return nil, errorx.Decorate(err, "authenticate failed") } // TODO: uncomment it after thinking clearly // if form.Type == 0 { // // generate new rsa key pair for each sql auth login // privateKey, publicKey, err := GenerateKey() // // if generate successfully, replace the old key pair // if err == nil { // service.RsaPrivateKey = privateKey // service.RsaPublicKey = publicKey // } // } return u, nil }, PayloadFunc: func(data interface{}) jwt.MapClaims { user, ok := data.(*utils.SessionUser) if !ok { return jwt.MapClaims{} } // `user` contains sensitive information, thus it is encrypted in the token. // In order to be simple, we keep using JWS instead of JWE for thus scenario. plain, err := json.Marshal(user) if err != nil { return jwt.MapClaims{} } encrypted, err := cryptopasta.Encrypt(plain, secret) if err != nil { return jwt.MapClaims{} } return jwt.MapClaims{ "p": base64.StdEncoding.EncodeToString(encrypted), } }, IdentityHandler: func(c *gin.Context) interface{} { claims := jwt.ExtractClaims(c) encoded, ok := claims["p"].(string) if !ok { return nil } decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return nil } decrypted, err := cryptopasta.Decrypt(decoded, secret) if err != nil { return nil } var user utils.SessionUser if err := json.Unmarshal(decrypted, &user); err != nil { return nil } // Force expire schema outdated sessions. if user.Version != utils.SessionVersion { return nil } a, ok := service.authenticators[user.AuthFrom] if !ok { return nil } if !a.ProcessSession(&user) { return nil } return &user }, Authorizator: func(data interface{}, _ *gin.Context) bool { // Ensure identity is valid if data == nil { return false } user := data.(*utils.SessionUser) return user != nil }, HTTPStatusMessageFunc: func(e error, c *gin.Context) string { var err error if errorxErr := errorx.Cast(e); errorxErr != nil { // If the error is an errorx, use it directly. err = e } else if errors.Is(e, jwt.ErrFailedTokenCreation) { // Try to catch other sign in failure errors. err = ErrSignInOther.WrapWithNoMessage(e) } else { // The remaining error comes from checking tokens for protected endpoints. err = rest.ErrUnauthenticated.NewWithNoMessage() } rest.Error(c, err) return err.Error() }, Unauthorized: func(c *gin.Context, code int, _ string) { c.Status(code) }, LoginResponse: func(c *gin.Context, _ int, token string, expire time.Time) { c.JSON(http.StatusOK, TokenResponse{ Token: token, Expire: expire, }) }, }) if err != nil { // Error only comes from configuration errors. Fatal is fine. log.Fatal("Failed to configure auth service", zap.Error(err)) } service.middleware = middleware return service } func (s *AuthService) authForm(f AuthenticateForm) (*utils.SessionUser, error) { a, ok := s.authenticators[f.Type] if !ok { return nil, ErrUnsupportedAuthType.NewWithNoMessage() } u, err := a.Authenticate(f) if err != nil { return nil, err } u.AuthFrom = f.Type return u, nil } func registerRouter(r *gin.RouterGroup, s *AuthService) { endpoint := r.Group("/user") endpoint.GET("/login_info", s.GetLoginInfoHandler) endpoint.POST("/login", s.LoginHandler) endpoint.GET("/sign_out_info", s.MWAuthRequired(), s.getSignOutInfoHandler) } // MWAuthRequired creates a middleware that verifies the authentication token (JWT) in the request. If the token // is valid, identity information will be attached in the context. If there is no authentication token, or the // token is invalid, subsequent handlers will be skipped and errors will be generated. func (s *AuthService) MWAuthRequired() gin.HandlerFunc { return s.middleware.MiddlewareFunc() } // TODO: Make these MWRequireXxxPriv more general to use. func (s *AuthService) MWRequireSharePriv() gin.HandlerFunc { return func(c *gin.Context) { u := utils.GetSession(c) if u == nil { rest.Error(c, rest.ErrUnauthenticated.NewWithNoMessage()) c.Abort() return } if !u.IsShareable { rest.Error(c, rest.ErrForbidden.NewWithNoMessage()) c.Abort() return } c.Next() } } func (s *AuthService) MWRequireWritePriv() gin.HandlerFunc { return func(c *gin.Context) { u := utils.GetSession(c) if u == nil { rest.Error(c, rest.ErrUnauthenticated.NewWithNoMessage()) c.Abort() return } if !u.IsWriteable { rest.Error(c, rest.ErrForbidden.NewWithNoMessage()) c.Abort() return } c.Next() } } // RegisterAuthenticator registers an authenticator in the authenticate pipeline. func (s *AuthService) RegisterAuthenticator(typeID utils.AuthType, a Authenticator) { s.authenticators[typeID] = a } type GetLoginInfoResponse struct { SupportedAuthTypes []int `json:"supported_auth_types"` SQLAuthPublicKey string `json:"sql_auth_public_key"` } // @ID userGetLoginInfo // @Summary Get log in information, like supported authenticate types // @Success 200 {object} GetLoginInfoResponse // @Router /user/login_info [get] func (s *AuthService) GetLoginInfoHandler(c *gin.Context) { supportedAuth := make([]int, 0) for typeID, a := range s.authenticators { enabled, err := a.IsEnabled() if err != nil { rest.Error(c, err) return } if enabled { supportedAuth = append(supportedAuth, int(typeID)) } } sort.Ints(supportedAuth) // both work // publicKeyStr, err := ExportPublicKeyAsString(s.rsaPublicKey) publicKeyStr, err := DumpPublicKeyBase64(s.RsaPublicKey) if err != nil { rest.Error(c, err) return } resp := GetLoginInfoResponse{ SupportedAuthTypes: supportedAuth, SQLAuthPublicKey: publicKeyStr, } c.JSON(http.StatusOK, resp) } // @ID userLogin // @Summary Log in // @Param message body AuthenticateForm true "Credentials" // @Success 200 {object} TokenResponse // @Failure 401 {object} rest.ErrorResponse // @Router /user/login [post] func (s *AuthService) LoginHandler(c *gin.Context) { s.middleware.LoginHandler(c) } type GetSignOutInfoRequest struct { RedirectURL string `json:"redirect_url" form:"redirect_url"` } // @ID userGetSignOutInfo // @Summary Get sign out info // @Success 200 {object} SignOutInfo // @Param q query GetSignOutInfoRequest true "Query" // @Router /user/sign_out_info [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *AuthService) getSignOutInfoHandler(c *gin.Context) { var req GetSignOutInfoRequest if err := c.ShouldBindQuery(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } u := utils.GetSession(c) a, ok := s.authenticators[u.AuthFrom] if !ok { rest.Error(c, ErrUnsupportedAuthType.NewWithNoMessage()) return } si, err := a.SignOutInfo(u, req.RedirectURL) if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, si) } ================================================ FILE: pkg/apiserver/user/code/codeauth/auth.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package codeauth import ( "time" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user/code" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" ) const typeID utils.AuthType = 1 var ErrSignInInvalidCode = user.ErrNSSignIn.NewType("invalid_code") // Invalid or expired type Authenticator struct { user.BaseAuthenticator sharingCodeService *code.Service } func newAuthenticator(sharingCodeService *code.Service) *Authenticator { return &Authenticator{ sharingCodeService: sharingCodeService, } } func registerAuthenticator(a *Authenticator, authService *user.AuthService) { authService.RegisterAuthenticator(typeID, a) } var Module = fx.Options( fx.Provide(newAuthenticator), fx.Invoke(registerAuthenticator), ) func (a *Authenticator) Authenticate(f user.AuthenticateForm) (*utils.SessionUser, error) { session := a.sharingCodeService.NewSessionFromSharingCode(f.Password) if session == nil { return nil, ErrSignInInvalidCode.NewWithNoMessage() } return session, nil } func (a *Authenticator) ProcessSession(user *utils.SessionUser) bool { return !time.Now().After(user.SharedSessionExpireAt) } ================================================ FILE: pkg/apiserver/user/code/router.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package code import ( "net/http" "time" "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("/user/share") endpoint.Use(auth.MWAuthRequired()) endpoint.POST("/code", auth.MWRequireSharePriv(), s.ShareHandler) endpoint.POST("/revoke", auth.MWRequireSharePriv(), s.RevokeHandler) } type ShareRequest struct { ExpireInSeconds int64 `json:"expire_in_sec"` RevokeWritePriv bool `json:"revoke_write_priv"` } type ShareResponse struct { Code string `json:"code"` } // @ID userShareSession // @Summary Share current session and generate a sharing code // @Param request body ShareRequest true "Request body" // @Security JwtAuth // @Success 200 {object} ShareResponse // @Router /user/share/code [post] func (s *Service) ShareHandler(c *gin.Context) { var req ShareRequest if err := c.ShouldBindJSON(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } expiry := time.Second * time.Duration(req.ExpireInSeconds) // after allow user customize the expiration // we should remove the following check // // if expiry > MaxSessionShareExpiry || expiry < 0 { // rest.Error(c, rest.ErrBadRequest.New("Invalid share expiry")) // return // } if expiry < 0 { rest.Error(c, rest.ErrBadRequest.New("Invalid share expiry")) return } sessionUser := utils.GetSession(c) code := s.SharingCodeFromSession(sessionUser, expiry, req.RevokeWritePriv) if code == nil { rest.Error(c, ErrShareFailed.New("Share session failed")) return } c.JSON(http.StatusOK, ShareResponse{Code: *code}) } // @ID userRevokeSession // @Summary Reset encryption key to revoke all authorized codes // @Security JwtAuth // @Success 200 // @Router /user/share/revoke [post] func (s *Service) RevokeHandler(c *gin.Context) { s.ResetEncryptionKey() c.JSON(http.StatusOK, nil) } ================================================ FILE: pkg/apiserver/user/code/service.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package code import ( "encoding/hex" "fmt" "sync/atomic" "time" "unsafe" "github.com/gtank/cryptopasta" "github.com/joomcode/errorx" "github.com/vmihailenco/msgpack/v5" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" ) var ( ErrNS = errorx.NewNamespace("error.api.user.code") ErrShareFailed = ErrNS.NewType("share_failed") ) // after allow user customize the expiration and support no expiration // we should remove the following check // // const ( // // Max permitted lifetime of a shared session. // MaxSessionShareExpiry = time.Hour * 24 * 30 // ) type Service struct { sharingSecret *[32]byte } type sharedSession struct { Session *utils.SessionUser ExpireAt time.Time RevokeWritePriv bool } func NewService() *Service { return &Service{ sharingSecret: cryptopasta.NewEncryptionKey(), } } var Module = fx.Options( fx.Provide(NewService), fx.Invoke(registerRouter), ) func (s *Service) NewSessionFromSharingCode(codeInHex string) *utils.SessionUser { encrypted, err := hex.DecodeString(codeInHex) if err != nil { return nil } b, err := cryptopasta.Decrypt(encrypted, s.loadShareingSecret()) if err != nil { return nil } var shared sharedSession if err := msgpack.Unmarshal(b, &shared); err != nil { return nil } if time.Now().After(shared.ExpireAt) { return nil } shared.Session.SharedSessionExpireAt = shared.ExpireAt shared.Session.DisplayName = fmt.Sprintf("Shared from %s", shared.Session.DisplayName) shared.Session.IsShareable = false if shared.RevokeWritePriv { shared.Session.IsWriteable = false } return shared.Session } func (s *Service) SharingCodeFromSession(session *utils.SessionUser, expireIn time.Duration, revokeWritePriv bool) *string { if !session.IsShareable { return nil } // after allow user customize the expiration and support no expiration // we should remove the following check // // if expireIn > MaxSessionShareExpiry { // return nil // } if expireIn < 0 { return nil } shared := sharedSession{ Session: session, ExpireAt: time.Now().Add(expireIn), RevokeWritePriv: revokeWritePriv, } b, err := msgpack.Marshal(&shared) if err != nil { // Do not output anything about how serialization is failed to avoid potential leaks. return nil } encrypted, err := cryptopasta.Encrypt(b, s.loadShareingSecret()) if err != nil { return nil } codeInHex := hex.EncodeToString(encrypted) return &codeInHex } func (s *Service) ResetEncryptionKey() { //nolint:gosec // Using unsafe is necessary because atomic pointer operations are required. atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&s.sharingSecret)), unsafe.Pointer(cryptopasta.NewEncryptionKey())) } func (s *Service) loadShareingSecret() *[32]byte { //nolint:gosec // Using unsafe is necessary because atomic pointer operations are required. return (*[32]byte)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&s.sharingSecret)))) } ================================================ FILE: pkg/apiserver/user/module.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package user import ( "go.uber.org/fx" ) var Module = fx.Options( fx.Provide(NewAuthService), fx.Invoke(registerRouter), ) ================================================ FILE: pkg/apiserver/user/rsa_utils.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package user import ( "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/pem" ) // Generate RSA private/public key. func GenerateKey() (*rsa.PrivateKey, *rsa.PublicKey, error) { privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, nil, err } publicKey := &privateKey.PublicKey return privateKey, publicKey, nil } // Export public key to string // Output format: // -----BEGIN PUBLIC KEY----- // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA67F1RPMUO4SjARRe4UfX // J7ZOCbcysna0jx2Av14KteGo6AWFHhuIxZwgp83GDqFv0Dhc/be7n+9V5vfq0Ob4 // fUtdjBio5ciF4pcqzVGbddfJ0R2e52DF6TI2pDgUFdN+1bmGDwZOCyrwBvVh0wW2 // jAI+QfQyRimZOMqFeX97XjW32vGk7cxNYMys9ExyJcfzfLanbzOwp6kdNbPXnYtU // Y2nmp+evlPKrRzBPnmO0bpZhYHklrRxLo/u/mThysMEttLkgzCare+JPQyb3z3Si // Q2E7WG4yz6+6L/wB4etHDfRljMOtqEwv9z4inUfh5716Mg23Div/AbwqGPiKPZf7 // cQIDAQAB // -----END PUBLIC KEY-----. func ExportPublicKeyAsString(publicKey *rsa.PublicKey) (string, error) { publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) if err != nil { return "", err } publicKeyPEM := &pem.Block{ Type: "PUBLIC KEY", Bytes: publicKeyBytes, } publicKeyString := string(pem.EncodeToMemory(publicKeyPEM)) return publicKeyString, nil } // Dump public key to base64 string // 1. Have no header/tailer line // 2. Key content is merged into one-line format // // The output is: // // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2y8mEdCRE8siiI7udpge......2QIDAQAB func DumpPublicKeyBase64(publicKey *rsa.PublicKey) (string, error) { keyBytes, err := x509.MarshalPKIXPublicKey(publicKey) if err != nil { return "", err } keyBase64 := base64.StdEncoding.EncodeToString(keyBytes) return keyBase64, nil } // Dump private key to base64 string // 1. Have no header/tailer line // 2. Key content is merged into one-line format // // The output is: // // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2y8mEdCRE8siiI7udpge......2QIDAQAB func DumpPrivateKeyBase64(privatekey *rsa.PrivateKey) (string, error) { keyBytes := x509.MarshalPKCS1PrivateKey(privatekey) keyBase64 := base64.StdEncoding.EncodeToString(keyBytes) return keyBase64, nil } // Encrypt by public key. func Encrypt(plainText string, publicKey *rsa.PublicKey) (string, error) { encryptedText, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(plainText)) if err != nil { return "", err } // the encryptedText is encoded by base64 in the frontend by jsEncrypt encodedText := base64.StdEncoding.EncodeToString(encryptedText) return encodedText, nil } // Decrypt by private key. func Decrypt(cipherText string, privateKey *rsa.PrivateKey) (string, error) { // the cipherText is encoded by base64 in the frontend by jsEncrypt decodedText, err := base64.StdEncoding.DecodeString(cipherText) if err != nil { return "", err } decryptedText, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, decodedText) if err != nil { return "", err } return string(decryptedText), nil } ================================================ FILE: pkg/apiserver/user/sqlauth/sqlauth.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package sqlauth import ( "github.com/joomcode/errorx" "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" ) const typeID utils.AuthType = 0 type Authenticator struct { user.BaseAuthenticator tidbClient *tidb.Client authService *user.AuthService } func NewAuthenticator(tidbClient *tidb.Client) *Authenticator { return &Authenticator{ tidbClient: tidbClient, } } func registerAuthenticator(a *Authenticator, authService *user.AuthService) { authService.RegisterAuthenticator(typeID, a) a.authService = authService } var Module = fx.Options( fx.Provide(NewAuthenticator), fx.Invoke(registerAuthenticator), ) func (a *Authenticator) Authenticate(f user.AuthenticateForm) (*utils.SessionUser, error) { plainPwd, err := user.Decrypt(f.Password, a.authService.RsaPrivateKey) if err != nil { return nil, user.ErrSignInOther.WrapWithNoMessage(err) } writeable, err := user.VerifySQLUser(a.tidbClient, f.Username, plainPwd) if err != nil { if errorx.Cast(err) == nil { return nil, user.ErrSignInOther.WrapWithNoMessage(err) } // Possible errors could be: // tidb.ErrNoAliveTiDB // tidb.ErrPDAccessFailed // tidb.ErrTiDBConnFailed // tidb.ErrTiDBAuthFailed // user.ErrInsufficientPrivs return nil, err } return &utils.SessionUser{ Version: utils.SessionVersion, HasTiDBAuth: true, TiDBUsername: f.Username, TiDBPassword: plainPwd, DisplayName: f.Username, IsShareable: true, IsWriteable: writeable, }, nil } ================================================ FILE: pkg/apiserver/user/sso/models.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package sso import ( "github.com/pingcap/tidb-dashboard/pkg/dbstore" ) type ImpersonateStatus string const ( ImpersonateStatusSuccess ImpersonateStatus = "success" ImpersonateStatusAuthFail ImpersonateStatus = "auth_fail" ImpersonateStatusInsufficientPrivs ImpersonateStatus = "insufficient_privileges" ) type SSOImpersonationModel struct { // nolint SQLUser string `gorm:"primary_key;size:128" json:"sql_user"` // The encryption key is placed somewhere else in the FS, to avoid being collected by diagnostics collecting tools. EncryptedPass string `gorm:"type:text" json:"-"` LastImpersonateStatus *ImpersonateStatus `gorm:"size:32" json:"last_impersonate_status"` } func (SSOImpersonationModel) TableName() string { return "sso_impersonation" } func autoMigrate(db *dbstore.DB) error { return db.AutoMigrate(&SSOImpersonationModel{}) } ================================================ FILE: pkg/apiserver/user/sso/router.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package sso import ( "net/http" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/util/rest" ) func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/user/sso") endpoint.GET("/auth_url", s.getAuthURLHandler) endpoint.Use(auth.MWAuthRequired()) // TODO: Forbid modifying config when signed in as SSO. endpoint.GET("/impersonations/list", s.listImpersonationHandler) endpoint.POST("/impersonation", auth.MWRequireWritePriv(), s.createImpersonationHandler) endpoint.GET("/config", s.getConfig) endpoint.PUT("/config", auth.MWRequireWritePriv(), s.setConfig) } type GetAuthURLRequest struct { RedirectURL string `json:"redirect_url" form:"redirect_url"` CodeVerifier string `json:"code_verifier" form:"code_verifier"` State string `json:"state" form:"state"` } // @ID userSSOGetAuthURL // @Summary Get SSO Auth URL // @Param q query GetAuthURLRequest true "Query" // @Success 200 {string} string // @Router /user/sso/auth_url [get] func (s *Service) getAuthURLHandler(c *gin.Context) { var req GetAuthURLRequest if err := c.ShouldBindQuery(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } authURL, err := s.buildOAuthURL(req.RedirectURL, req.State, req.CodeVerifier) if err != nil { rest.Error(c, err) return } c.String(http.StatusOK, authURL) } // @ID userSSOListImpersonations // @Summary List all impersonations // @Success 200 {array} SSOImpersonationModel // @Router /user/sso/impersonations/list [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) listImpersonationHandler(c *gin.Context) { var resp []SSOImpersonationModel err := s.params.LocalStore.Find(&resp).Error if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, resp) } type CreateImpersonationRequest struct { SQLUser string `json:"sql_user"` Password string `json:"password"` } // @ID userSSOCreateImpersonation // @Summary Create an impersonation // @Param request body CreateImpersonationRequest true "Request body" // @Success 200 {object} SSOImpersonationModel // @Router /user/sso/impersonation [post] // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) createImpersonationHandler(c *gin.Context) { var req CreateImpersonationRequest if err := c.ShouldBindJSON(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } rec, err := s.createImpersonation(req.SQLUser, req.Password) if err != nil { rest.Error(c, err) if errorx.IsOfType(err, ErrUnsupportedUser) || errorx.IsOfType(err, ErrInvalidImpersonateCredential) { c.Status(http.StatusBadRequest) } return } c.JSON(http.StatusOK, rec) } // @ID userSSOGetConfig // @Summary Get SSO config // @Success 200 {object} config.SSOCoreConfig // @Router /user/sso/config [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) getConfig(c *gin.Context) { dc, err := s.params.ConfigManager.Get() if err != nil { rest.Error(c, err) return } // Hide client secret for security dc.SSO.CoreConfig.ClientSecret = "" c.JSON(http.StatusOK, dc.SSO.CoreConfig) } type SetConfigRequest struct { Config config.SSOCoreConfig `json:"config"` } // @ID userSSOSetConfig // @Summary Set SSO config // @Param request body SetConfigRequest true "Request body" // @Success 200 {object} config.SSOCoreConfig // @Router /user/sso/config [put] // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) setConfig(c *gin.Context) { var req SetConfigRequest if err := c.ShouldBindJSON(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } dConfig := config.SSOConfig{CoreConfig: req.Config} if req.Config.Enabled { wellKnownConfig, err := s.discoverOIDC(req.Config.DiscoveryURL) if err != nil { rest.Error(c, rest.ErrBadRequest.WrapWithNoMessage(err)) return } dConfig.AuthURL = wellKnownConfig.AuthURL dConfig.TokenURL = wellKnownConfig.TokenURL dConfig.UserInfoURL = wellKnownConfig.UserInfoURL dConfig.SignOutURL = wellKnownConfig.EndSessionURL // This is optional } else { err := s.revokeAllImpersonations() if err != nil { rest.Error(c, err) return } } var opt config.DynamicConfigOption = func(dc *config.DynamicConfig) { dc.SSO = dConfig } if err := s.params.ConfigManager.Modify(opt); err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, req.Config) } ================================================ FILE: pkg/apiserver/user/sso/service.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package sso import ( "context" "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "net/url" "os" "path" "strings" "sync" "time" "github.com/go-resty/resty/v2" "github.com/gtank/cryptopasta" "github.com/joomcode/errorx" "github.com/pingcap/log" "go.uber.org/fx" "go.uber.org/zap" "golang.org/x/oauth2" "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" ) var ( ErrNS = errorx.NewNamespace("error.api.user.sso") ErrUnsupportedUser = ErrNS.NewType("unsupported_user") ErrInvalidImpersonateCredential = ErrNS.NewType("invalid_impersonate_credential") ErrDiscoverFailed = ErrNS.NewType("discover_failed") ErrBadConfig = ErrNS.NewType("bad_config") ErrOIDCInternalErr = ErrNS.NewType("oidc_internal_err") ) const ( discoveryTimeout = time.Second * 30 exchangeTimeout = time.Second * 30 userInfoTimeout = time.Second * 30 ) type ServiceParams struct { fx.In LocalStore *dbstore.DB TiDBClient *tidb.Client ConfigManager *config.DynamicConfigManager } type Service struct { params ServiceParams lifecycleCtx context.Context oauthStateSecret []byte encKeyPath string encKeyLock sync.Mutex createImpersonationLock sync.Mutex } func NewService(p ServiceParams, lc fx.Lifecycle, config *config.Config) (*Service, error) { if err := autoMigrate(p.LocalStore); err != nil { return nil, err } s := &Service{ params: p, oauthStateSecret: cryptopasta.NewHMACKey()[:], encKeyPath: path.Join(config.DataDir, "dbek.bin"), encKeyLock: sync.Mutex{}, createImpersonationLock: sync.Mutex{}, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { s.lifecycleCtx = ctx return nil }, }) return s, nil } var Module = fx.Options( fx.Provide(NewService), fx.Invoke(registerRouter), ) func (s *Service) getMasterEncKey() (*[32]byte, error) { b, err := os.ReadFile(s.encKeyPath) if err != nil { // Key does not exist if os.IsNotExist(err) { return nil, nil } return nil, err } if len(b) != 32 { return nil, fmt.Errorf("encryption key is broken") } var fixedLenKey [32]byte copy(fixedLenKey[:], b) return &fixedLenKey, nil } // This function is thread-safe. func (s *Service) getOrCreateMasterEncKey() (*[32]byte, error) { s.encKeyLock.Lock() defer s.encKeyLock.Unlock() key, _ := s.getMasterEncKey() if key != nil { return key, nil } // Try to create a key otherwise key = cryptopasta.NewEncryptionKey() err := os.WriteFile(s.encKeyPath, key[:], 0o400) // read only for owner if err != nil { return nil, fmt.Errorf("persist key failed: %v", err) } return key, nil } // getAndDecryptImpersonation reads the impersonation record from local Sqlite and decrypt the record to get the // plain SQL password. Currently this function only reads `root` user impersonation. func (s *Service) getAndDecryptImpersonation() (string, string, error) { var imp SSOImpersonationModel err := s.params.LocalStore. First(&imp).Error if err != nil { return "", "", fmt.Errorf("bad record: %v", err) } key, err := s.getMasterEncKey() if err != nil { return "", "", fmt.Errorf("bad encryption key: %v", err) } if key == nil { return "", "", fmt.Errorf("encryption key is missing") } encrypted, err := hex.DecodeString(imp.EncryptedPass) if err != nil { return "", "", fmt.Errorf("bad record: %v", err) } decryptedPass, err := cryptopasta.Decrypt(encrypted, key) if err != nil { return "", "", fmt.Errorf("bad record: %v", err) } return imp.SQLUser, string(decryptedPass), nil } func (s *Service) updateImpersonationStatus(user string, status ImpersonateStatus) error { return s.params.LocalStore. Model(&SSOImpersonationModel{}). Where("sql_user = ?", user). Update("last_impersonate_status", status). Error } // newSessionFromImpersonation creates a new session from the impersonation records. func (s *Service) newSessionFromImpersonation(userInfo *oAuthUserInfo, idToken string) (*utils.SessionUser, error) { dc, err := s.params.ConfigManager.Get() if err != nil { return nil, err } userName, password, err := s.getAndDecryptImpersonation() if err != nil { return nil, err } // Check whether this user can access dashboard writeable, err := user.VerifySQLUser(s.params.TiDBClient, userName, password) if err != nil { if errorx.IsOfType(err, tidb.ErrTiDBAuthFailed) { _ = s.updateImpersonationStatus(userName, ImpersonateStatusAuthFail) return nil, ErrInvalidImpersonateCredential.Wrap(err, "Invalid SQL credential") } if errorx.IsOfType(err, user.ErrInsufficientPrivs) { _ = s.updateImpersonationStatus(userName, ImpersonateStatusInsufficientPrivs) return nil, ErrInvalidImpersonateCredential.Wrap(err, "Insufficient privileges") } return nil, err } _ = s.updateImpersonationStatus(userName, ImpersonateStatusSuccess) return &utils.SessionUser{ Version: utils.SessionVersion, HasTiDBAuth: true, TiDBUsername: userName, TiDBPassword: password, DisplayName: userInfo.Email, IsShareable: true, IsWriteable: writeable && !dc.SSO.CoreConfig.IsReadOnly, OIDCIDToken: idToken, }, nil } func (s *Service) createImpersonation(userName string, password string) (*SSOImpersonationModel, error) { { // Check whether this user can access dashboard _, err := user.VerifySQLUser(s.params.TiDBClient, userName, password) if err != nil { if errorx.IsOfType(err, tidb.ErrTiDBAuthFailed) { return nil, ErrInvalidImpersonateCredential.Wrap(err, "Invalid SQL credential") } if errorx.IsOfType(err, user.ErrInsufficientPrivs) { return nil, ErrInvalidImpersonateCredential.Wrap(err, "Insufficient privileges") } return nil, err } } key, err := s.getOrCreateMasterEncKey() if err != nil { return nil, err } encrypted, err := cryptopasta.Encrypt([]byte(password), key) if err != nil { return nil, err } encryptedInHex := hex.EncodeToString(encrypted) record := &SSOImpersonationModel{ SQLUser: userName, EncryptedPass: encryptedInHex, LastImpersonateStatus: nil, } // currently, we only support to authorize one sql user s.createImpersonationLock.Lock() defer s.createImpersonationLock.Unlock() err = s.revokeAllImpersonations() if err != nil { return nil, err } err = s.params.LocalStore.Create(&record).Error if err != nil { return nil, err } return record, nil } func (s *Service) revokeAllImpersonations() error { sqlStr := fmt.Sprintf("DELETE FROM `%s`", SSOImpersonationModel{}.TableName()) // #nosec return s.params.LocalStore. Exec(sqlStr). Error } type oidcWellKnownConfig struct { Issuer string `json:"issuer"` AuthURL string `json:"authorization_endpoint"` TokenURL string `json:"token_endpoint"` UserInfoURL string `json:"userinfo_endpoint"` EndSessionURL string `json:"end_session_endpoint"` JWKSURI string `json:"jwks_uri"` ResponseTypesSupported []string `json:"response_types_supported"` SubjectTypesSupported []string `json:"subject_types_supported"` IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` } func (s *Service) discoverOIDC(issuer string) (*oidcWellKnownConfig, error) { issuer = strings.TrimSuffix(issuer, "/") if !strings.HasPrefix(issuer, "http://") && !strings.HasPrefix(issuer, "https://") { issuer = "https://" + issuer } _, err := url.Parse(issuer) if err != nil { return nil, ErrDiscoverFailed.Wrap(err, "Invalid URL format") } ctx, cancel := context.WithTimeout(s.lifecycleCtx, discoveryTimeout) defer cancel() wellKnownURL := issuer + "/.well-known/openid-configuration" resp, err := resty.New().R().SetContext(ctx).SetResult(&oidcWellKnownConfig{}).Get(wellKnownURL) if err != nil { return nil, ErrDiscoverFailed.Wrap(err, "Failed to discover OIDC endpoints") } wellKnownConfig := resp.Result().(*oidcWellKnownConfig) if strings.TrimSuffix(wellKnownConfig.Issuer, "/") != issuer { return nil, ErrDiscoverFailed.New("Issuer did not match in the OIDC provider, expect %s, got %s", issuer, wellKnownConfig.Issuer) } if len(wellKnownConfig.TokenURL) == 0 { return nil, ErrDiscoverFailed.New("token_endpoint is not provided in the OIDC provider") } if len(wellKnownConfig.AuthURL) == 0 { return nil, ErrDiscoverFailed.New("authorization_endpoint is not provided in the OIDC provider") } if len(wellKnownConfig.UserInfoURL) == 0 { return nil, ErrDiscoverFailed.New("userinfo_endpoint is not provided in the OIDC provider") } if len(wellKnownConfig.JWKSURI) == 0 { return nil, ErrDiscoverFailed.New("jwks_uri is not provided in the OIDC provider") } if len(wellKnownConfig.ResponseTypesSupported) == 0 { return nil, ErrDiscoverFailed.New("response_types_supported is not provided in the OIDC provider") } if len(wellKnownConfig.SubjectTypesSupported) == 0 { return nil, ErrDiscoverFailed.New("subject_types_supported is not provided in the OIDC provider") } if len(wellKnownConfig.IDTokenSigningAlgValuesSupported) == 0 { return nil, ErrDiscoverFailed.New("id_token_signing_alg_values_supported is not provided in the OIDC provider") } return wellKnownConfig, nil } func (s *Service) IsEnabled() (bool, error) { dc, err := s.params.ConfigManager.Get() if err != nil { return false, err } return dc.SSO.CoreConfig.Enabled, nil } func (s *Service) buildOAuth2Config(redirectURL string) (*oauth2.Config, error) { dc, err := s.params.ConfigManager.Get() if err != nil { return nil, err } if !dc.SSO.CoreConfig.Enabled { return nil, ErrBadConfig.New("SSO is not enabled") } scopes := []string{"openid", "profile", "email"} if len(dc.SSO.CoreConfig.Scopes) > 0 { userSupplied := strings.Split(dc.SSO.CoreConfig.Scopes, " ") scopes = append(scopes, userSupplied...) } return &oauth2.Config{ ClientID: dc.SSO.CoreConfig.ClientID, ClientSecret: dc.SSO.CoreConfig.ClientSecret, RedirectURL: redirectURL, Endpoint: oauth2.Endpoint{ AuthURL: dc.SSO.AuthURL, TokenURL: dc.SSO.TokenURL, }, Scopes: scopes, }, nil } // buildOAuthURL builds an OAuth URL (to be redirected by the browser) if OIDC SSO is enabled. // Returns nil if OIDC SSO is not enabled. // // `state` is generated by the browser, persisted in local storage and to be verified later before exchange. // // Browser uses this to ensure that the auth callback is not replayed (by an CSRF attacker that use another state). // // `codeVerifier` is also generated by the browser, persisted in local storage and will be presented to the RP at exchange. // // RP uses this to ensure that the exchange request is indeed issued by the same client (browser instance). func (s *Service) buildOAuthURL(redirectURL string, state string, codeVerifier string) (string, error) { oauthConfig, err := s.buildOAuth2Config(redirectURL) if err != nil { return "", err } // generate PKCE code challenge, which is base64(sha256(codeVerifier)). h := sha256.New() _, _ = h.Write([]byte(codeVerifier)) codeChallenge := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) authURL := oauthConfig.AuthCodeURL(state, oauth2.SetAuthURLParam("code_challenge", codeChallenge), oauth2.SetAuthURLParam("code_challenge_method", "S256")) return authURL, nil } func (s *Service) exchangeOAuthCode(redirectURL string, code string, codeVerifier string) (string, string, error) { oauthConfig, err := s.buildOAuth2Config(redirectURL) if err != nil { return "", "", err } ctx, cancel := context.WithTimeout(s.lifecycleCtx, exchangeTimeout) defer cancel() token, err := oauthConfig.Exchange(ctx, code, oauth2.SetAuthURLParam("code_verifier", codeVerifier)) if err != nil { return "", "", ErrOIDCInternalErr.Wrap(err, "oidc: exchange failed") } idToken, ok := token.Extra("id_token").(string) if !ok { return "", "", ErrOIDCInternalErr.Wrap(err, "oidc: id_token not exist") } return token.AccessToken, idToken, nil } type oAuthUserInfo struct { Name string `json:"name"` Email string `json:"email"` } func (s *Service) oAuthGetUserInfo(accessToken string) (*oAuthUserInfo, error) { dc, err := s.params.ConfigManager.Get() if err != nil { return nil, err } if !dc.SSO.CoreConfig.Enabled { return nil, ErrBadConfig.New("SSO is not enabled") } ctx, cancel := context.WithTimeout(s.lifecycleCtx, userInfoTimeout) defer cancel() resp, err := resty.New().R().SetContext(ctx). SetResult(&oAuthUserInfo{}). SetAuthToken(accessToken). Get(dc.SSO.UserInfoURL) if err != nil { return nil, ErrOIDCInternalErr.Wrap(err, "Failed to read user info") } info := resp.Result().(*oAuthUserInfo) return info, nil } func (s *Service) NewSessionFromOAuthExchange(redirectURL string, code string, codeVerifier string) (*utils.SessionUser, error) { ak, idToken, err := s.exchangeOAuthCode(redirectURL, code, codeVerifier) if err != nil { return nil, ErrBadConfig.Wrap(err, "SSO is not configured correctly") } info, err := s.oAuthGetUserInfo(ak) if err != nil { // This is likely not a configuration error return nil, err } log.Info("New session via SSO", zap.Any("userinfo", info)) u, err := s.newSessionFromImpersonation(info, idToken) if err != nil { return nil, ErrBadConfig.Wrap(err, "SSO is not configured correctly") } return u, nil } func (s *Service) BuildEndSessionURL(user *utils.SessionUser, redirectURL string) (string, error) { dc, err := s.params.ConfigManager.Get() if err != nil { return "", err } if !dc.SSO.CoreConfig.Enabled { return "", ErrBadConfig.New("SSO is not enabled") } u, err := url.Parse(dc.SSO.SignOutURL) if err != nil { return "", ErrBadConfig.Wrap(err, "Bad end session URL") } q := u.Query() q.Add("client_id", dc.SSO.CoreConfig.ClientID) q.Add("id_token_hint", user.OIDCIDToken) if len(redirectURL) > 0 { q.Add("post_logout_redirect_uri", redirectURL) } u.RawQuery = q.Encode() return u.String(), nil } ================================================ FILE: pkg/apiserver/user/sso/ssoauth/auth.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package ssoauth import ( "encoding/json" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user/sso" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap/tidb-dashboard/util/rest" ) const typeID utils.AuthType = 2 type Authenticator struct { user.BaseAuthenticator ssoService *sso.Service } func newAuthenticator(ssoService *sso.Service) *Authenticator { return &Authenticator{ ssoService: ssoService, } } func registerAuthenticator(a *Authenticator, authService *user.AuthService) { authService.RegisterAuthenticator(typeID, a) } var Module = fx.Options( fx.Provide(newAuthenticator), fx.Invoke(registerAuthenticator), ) type SSOExtra struct { Code string `json:"code"` CodeVerifier string `json:"code_verifier"` RedirectURL string `json:"redirect_url"` } func (a *Authenticator) Authenticate(f user.AuthenticateForm) (*utils.SessionUser, error) { var extra SSOExtra err := json.Unmarshal([]byte(f.Extra), &extra) if err != nil { return nil, rest.ErrBadRequest.Wrap(err, "Invalid extra payload") } u, err := a.ssoService.NewSessionFromOAuthExchange(extra.RedirectURL, extra.Code, extra.CodeVerifier) if err != nil { return nil, err } return u, nil } func (a *Authenticator) IsEnabled() (bool, error) { return a.ssoService.IsEnabled() } func (a *Authenticator) SignOutInfo(u *utils.SessionUser, redirectURL string) (*user.SignOutInfo, error) { esURL, err := a.ssoService.BuildEndSessionURL(u, redirectURL) if err != nil { return nil, err } return &user.SignOutInfo{ EndSessionURL: esURL, }, nil } ================================================ FILE: pkg/apiserver/user/verify_sql_user.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package user import ( "encoding/json" "regexp" "strings" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap/tidb-dashboard/pkg/tidb" ) var ErrInsufficientPrivs = ErrNSSignIn.NewType("insufficient_priv") // TiDB config response // // "security": { // ... // "enable-sem": true/false, // ... // },. type tidbSecurityConfig struct { Security tidbSEMConfig `json:"security"` } type tidbSEMConfig struct { EnableSEM bool `json:"enable-sem"` SkipGrantTable bool `json:"skip-grant-table"` } func VerifySQLUser(tidbClient *tidb.Client, userName, password string) (writeable bool, err error) { db, err := tidbClient.OpenSQLConn(userName, password) if err != nil { return false, err } defer utils.CloseTiDBConnection(db) //nolint:errcheck // Check dashboard privileges // 1. Get TiDB config resData, err := tidbClient.SendGetRequest("/config") if err != nil { return false, err } var config tidbSecurityConfig err = json.Unmarshal(resData, &config) if err != nil { return false, err } // 2. Check SkipGrantTable // Note: Currently, if TiDB enable the skip-grant-table, running `show grants` will get error. // So this is a workaround before the above bug is fixed. if config.Security.SkipGrantTable { return true, nil } // 3. Get grants var grantRows []string err = db.Raw("show grants for current_user()").Find(&grantRows).Error if err != nil { return false, err } grants := parseUserGrants(grantRows) // 4. Check grants if !checkDashboardPriv(grants, config.Security.EnableSEM) { return false, ErrInsufficientPrivs.NewWithNoMessage() } return checkWriteablePriv(grants), nil } var grantRegex = regexp.MustCompile(`GRANT (.+) ON`) // Currently, There are 2 kinds of grant output format in TiDB: // - GRANT [grants] ON [db.table] TO [user] // - GRANT [roles] TO [user] // Examples: // - GRANT PROCESS,SHOW DATABASES,CONFIG ON *.* TO 'dashboardAdmin'@'%' // - GRANT SYSTEM_VARIABLES_ADMIN,RESTRICTED_VARIABLES_ADMIN,RESTRICTED_STATUS_ADMIN,RESTRICTED_TABLES_ADMIN ON *.* TO 'dashboardAdmin'@'%' // - GRANT ALL PRIVILEGES ON *.* TO 'dashboardAdmin'@'%' // - GRANT `app_read`@`%` TO `test`@`%`. func parseUserGrants(grantRows []string) map[string]struct{} { grants := map[string]struct{}{} for _, row := range grantRows { m := grantRegex.FindStringSubmatch(row) if len(m) == 2 { curRowGrants := strings.SplitSeq(m[1], ",") for grant := range curRowGrants { grants[grant] = struct{}{} } } } return grants } // To access TiDB Dashboard, following base privileges are required // - ALL PRIVILEGES // - or // - PROCESS // - SHOW DATABASES // - CONFIG // - DASHBOARD_CLIENT or SUPER (SUPER includes DASHBOARD_CLIENT) // When TiDB SEM is enabled, following extra privileges are required // - RESTRICTED_VARIABLES_ADMIN // - RESTRICTED_TABLES_ADMIN // - RESTRICTED_STATUS_ADMIN. func checkDashboardPriv(privs map[string]struct{}, enableSEM bool) bool { if enableSEM { // Note: When SEM is enabled, these additional privileges need to be checked even if "ALL PRIVILEGES" is granted. if !hasPriv("RESTRICTED_VARIABLES_ADMIN", privs) { return false } if !hasPriv("RESTRICTED_TABLES_ADMIN", privs) { return false } if !hasPriv("RESTRICTED_STATUS_ADMIN", privs) { return false } } if hasPriv("ALL PRIVILEGES", privs) { // ALL PRIVILEGES contains privileges below. If it is set, privilege requirement is met. return true } if !hasPriv("PROCESS", privs) { return false } if !hasPriv("SHOW DATABASES", privs) { return false } if !hasPriv("CONFIG", privs) { return false } if hasPriv("SUPER", privs) { // SUPER contains privileges below. If it is set, privilege requirement is met. return true } if !hasPriv("DASHBOARD_CLIENT", privs) { return false } return true } func checkWriteablePriv(privs map[string]struct{}) bool { if hasPriv("ALL PRIVILEGES", privs) { return true } if hasPriv("SUPER", privs) { return true } if hasPriv("SYSTEM_VARIABLES_ADMIN", privs) { return true } return false } func hasPriv(priv string, privs map[string]struct{}) bool { _, ok := privs[priv] return ok } ================================================ FILE: pkg/apiserver/user/verify_sql_user_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package user import ( "testing" "github.com/pingcap/check" ) func TestT(t *testing.T) { check.CustomVerboseFlag = true check.TestingT(t) } var _ = check.Suite(&testVerifySQLUserSuite{}) type testVerifySQLUserSuite struct{} func (t *testVerifySQLUserSuite) Test_parseUserGrants(c *check.C) { cases := []struct { desc string input []string expected map[string]struct{} }{ // 0 { desc: "all privileges", input: []string{ "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION", }, expected: map[string]struct{}{ "ALL PRIVILEGES": {}, }, }, // 1 { desc: "table privileges", input: []string{ "GRANT SELECT,INSERT ON mysql.* TO 'dashboardAdmin'@'%'", }, expected: map[string]struct{}{ "SELECT": {}, "INSERT": {}, }, }, // 2 { desc: "global privileges", input: []string{ "GRANT PROCESS,SHOW DATABASES,CONFIG ON *.* TO 'dashboardAdmin'@'%'", "GRANT SYSTEM_VARIABLES_ADMIN ON *.* TO 'dashboardAdmin'@'%'", }, expected: map[string]struct{}{ "PROCESS": {}, "SHOW DATABASES": {}, "CONFIG": {}, "SYSTEM_VARIABLES_ADMIN": {}, }, }, // 3 { desc: "role privileges", input: []string{ "GRANT `app_read`@`%` TO `test`@`%`", }, expected: map[string]struct{}{}, }, } for i, v := range cases { actual := parseUserGrants(v.input) c.Assert(actual, check.DeepEquals, v.expected, check.Commentf("parse %s (index: %d) failed", v.desc, i)) } } func (t *testVerifySQLUserSuite) Test_checkDashboardPriv(c *check.C) { cases := []struct { desc string grants []string enableSEM bool expected bool }{ // 0 { desc: "all privileges with enableSEM false", grants: []string{"ALL PRIVILEGES"}, enableSEM: false, expected: true, }, // 1 { desc: "all privileges with enableSEM true", grants: []string{"ALL PRIVILEGES"}, enableSEM: true, expected: false, }, // 2 { desc: "super privileges with enableSEM false", grants: []string{"PROCESS", "SHOW DATABASES", "CONFIG", "SUPER"}, enableSEM: false, expected: true, }, // 3 { desc: "super privileges with enableSEM true", grants: []string{"PROCESS", "SHOW DATABASES", "CONFIG", "SUPER"}, enableSEM: true, expected: false, }, // 4 { desc: "base privileges with enableSEM false", grants: []string{"PROCESS", "SHOW DATABASES", "CONFIG", "DASHBOARD_CLIENT"}, enableSEM: false, expected: true, }, // 5 { desc: "base privileges with enableSEM true", grants: []string{"PROCESS", "SHOW DATABASES", "CONFIG", "DASHBOARD_CLIENT"}, enableSEM: true, expected: false, }, // 6 { desc: "lack PROCESS privilege", grants: []string{"SHOW DATABASES", "CONFIG", "DASHBOARD_CLIENT"}, enableSEM: false, expected: false, }, // 7 { desc: "lack DASHBOARD_CLIENT privilege", grants: []string{"PROCESS", "SHOW DATABASES", "CONFIG"}, enableSEM: false, expected: false, }, // 8 { desc: "extra privileges", grants: []string{"PROCESS", "SHOW DATABASES", "CONFIG", "DASHBOARD_CLIENT", "RESTRICTED_VARIABLES_ADMIN", "RESTRICTED_TABLES_ADMIN", "RESTRICTED_STATUS_ADMIN"}, enableSEM: true, expected: true, }, // 9 { desc: "lack RESTRICTED_VARIABLES_ADMIN extra privileges", grants: []string{"PROCESS", "SHOW DATABASES", "CONFIG", "DASHBOARD_CLIENT", "RESTRICTED_TABLES_ADMIN", "RESTRICTED_STATUS_ADMIN"}, enableSEM: true, expected: false, }, } for i, v := range cases { grants := map[string]struct{}{} for _, grant := range v.grants { grants[grant] = struct{}{} } actual := checkDashboardPriv(grants, v.enableSEM) c.Assert(actual, check.DeepEquals, v.expected, check.Commentf("check %s (index: %d) failed", v.desc, i)) } } func (t *testVerifySQLUserSuite) Test_checkWriteablePriv(c *check.C) { cases := []struct { desc string grants []string expected bool }{ // 0 { desc: "ALL privileges", grants: []string{ "ALL PRIVILEGES", }, expected: true, }, // 1 { desc: "SUPER privileges", grants: []string{ "SUPER", }, expected: true, }, // 2 { desc: "SYSTEM_VARIABLES_ADMIN privileges", grants: []string{ "SYSTEM_VARIABLES_ADMIN", }, expected: true, }, // 3 { desc: "all privileges", grants: []string{ "ALL PRIVILEGES", "SUPER", "SYSTEM_VARIABLES_ADMIN", }, expected: true, }, // 4 { desc: "other privileges", grants: []string{ "PROCESS", "CONFIG", }, expected: false, }, } for i, v := range cases { grants := map[string]struct{}{} for _, grant := range v.grants { grants[grant] = struct{}{} } actual := checkWriteablePriv(grants) c.Assert(actual, check.DeepEquals, v.expected, check.Commentf("check %s (index: %d) failed", v.desc, i)) } } ================================================ FILE: pkg/apiserver/utils/auth.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "time" "github.com/gin-gonic/gin" ) type AuthType int const SessionVersion = 2 // The content of this structure will be encrypted and stored as both Session Token and Sharing Token. // For fields that don't need to be cloned during session sharing, mark fields as `msgpack:"-"`. type SessionUser struct { // Must be 2. This field is used to invalidate outdated sessions after schema change. Version int DisplayName string HasTiDBAuth bool TiDBUsername string TiDBPassword string // This field only exists for CodeAuth. SharedSessionExpireAt time.Time `msgpack:"-"` // This field only exists for SSOAuth OIDCIDToken string `json:",omitempty"` // These fields should not be updated by individual authenticators. AuthFrom AuthType `msgpack:"-" json:",omitempty"` // TODO: Make them table fields IsShareable bool IsWriteable bool } const ( // The key that attached the SessionUser in the gin Context. SessionUserKey = "user" ) func GetSession(c *gin.Context) *SessionUser { i, ok := c.Get(SessionUserKey) if !ok { return nil } return i.(*SessionUser) } ================================================ FILE: pkg/apiserver/utils/binary_plan.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "encoding/base64" "fmt" "sort" "strconv" "strings" "time" simplejson "github.com/bitly/go-simplejson" "github.com/golang/snappy" "github.com/pingcap/tipb/go-tipb" json "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/runtime/protoimpl" "gorm.io/gorm" ) const ( MainTree = "main" CteTrees = "ctes" Children = "children" Duration = "duration" Time = "time" Diagnosis = "diagnosis" RootGroupExecInfo = "rootGroupExecInfo" RootBasicExecInfo = "rootBasicExecInfo" OperatorInfo = "operatorInfo" OperatorName = "name" CopExecInfo = "copExecInfo" CacheHitRatio = "cacheHitRatio" TaskType = "taskType" StoreType = "storeType" DiskBytes = "diskBytes" MemoryBytes = "memoryBytes" ActRows = "actRows" EstRows = "estRows" AccessObjects = "accessObjects" ScanObject = "scanObject" DynamicpartitionObjects = "dynamicpartitionObjects" OtherObject = "otherObject" DiscardedDueToTooLong = "discardedDueToTooLong" BuildSide = "buildSide" ProbeSide = "probeSide" JoinTaskThreshold = 10000 returnTableThreshold = 0.7 // role tag. HighEstError = "high_est_error" DiskSpill = "disk_spill" PseudoEst = "pseudo_est" GoodFilterOnTableFullScan = "good_filter_on_table_fullscan" BadIndexForIndexLookUp = "bad_index_for_index_lookup" IndexJoinBuildSideTooLarge = "index_join_build_side_too_large" TiKVHugeTableScan = "tikv_huge_table_scan" ) // operator. type operator int const ( Default operator = iota IndexJoin IndexMergeJoin IndexHashJoin Apply Shuffle ShuffleReceiver IndexLookUpReader IndexMergeReader IndexFullScan IndexRangeScan TableFullScan TableRangeScan TableRowIDScan Selection ) type concurrency struct { joinConcurrency int copConcurrency int tableConcurrency int applyConcurrency int shuffleConcurrency int } type diagnosticOperation struct { needUdateStatistics bool } var ( needJSONFormat = []string{ RootGroupExecInfo, RootBasicExecInfo, CopExecInfo, } needSetNA = []string{ MemoryBytes, DiskBytes, } needCheckOperator = []string{ "eq", "ge", "gt", "le", "lt", "isnull", "in", } ) func newConcurrency() concurrency { return concurrency{ joinConcurrency: 1, copConcurrency: 1, tableConcurrency: 1, applyConcurrency: 1, shuffleConcurrency: 1, } } func newDiagnosticOperation() diagnosticOperation { return diagnosticOperation{} } // GenerateBinaryPlan generate visual plan from raw data. func GenerateBinaryPlan(v string) (*tipb.ExplainData, error) { if v == "" { return nil, nil } // base64 decode compressVPBytes, err := base64.StdEncoding.DecodeString(v) if err != nil { return nil, err } // snappy uncompress bpBytes, err := snappy.Decode(nil, compressVPBytes) if err != nil { return nil, err } // proto unmarshal bp := &tipb.ExplainData{} err = bp.Unmarshal(bpBytes) if err != nil { return nil, err } return bp, nil } func GenerateBinaryPlanJSON(b string) (string, error) { // generate bp bp, err := GenerateBinaryPlan(b) if err != nil { return "", err } if bp == nil { return "", nil } // json marshal bpJSON, err := json.Marshal(protoimpl.X.ProtoMessageV2Of(bp)) if err != nil { return "", err } bpJSON, err = formatBinaryPlanJSON(bpJSON) if err != nil { return "", err } bpJSON, err = analyzeDuration(bpJSON) if err != nil { return "", err } bpJSON, err = diagnosticOperator(bpJSON) if err != nil { return "", err } return string(bpJSON), nil } func diagnosticOperator(bp []byte) ([]byte, error) { // new simple json vp, err := simplejson.NewJson(bp) if err != nil { return nil, err } if vp.Get(DiscardedDueToTooLong).MustBool() { return vp.MarshalJSON() } // main _, err = diagnosticOperatorNode(vp.Get(MainTree), newDiagnosticOperation()) if err != nil { return nil, err } // ctes _, err = diagnosticOperatorNodes((vp.Get(CteTrees)), newDiagnosticOperation()) if err != nil { return nil, err } return vp.MarshalJSON() } // diagnosticOperatorNode set node.diagnosis. func diagnosticOperatorNode(node *simplejson.Json, diagOp diagnosticOperation) (diagnosticOperation, error) { operator := getOperatorType(node) operatorInfo := node.Get(OperatorInfo).MustString() diagnosis := []string{} // filter system table switch strings.ToLower(getScanDatabase(node)) { case "information_schema", "metrics_schema", "performance_schema", "mysql": diagOp, err := diagnosticOperatorNodes(node.Get(Children), diagOp) if err != nil { return diagOp, nil } // set diagnosis node.Set(Diagnosis, diagnosis) return diagOp, nil } // pseudo stats if strings.Contains(operatorInfo, "stats:pseudo") { diagnosis = append(diagnosis, PseudoEst) } // use disk diskBytes := node.Get(DiskBytes).MustString() if diskBytes != "N/A" { diagnosis = append(diagnosis, DiskSpill) } diagOp, err := diagnosticOperatorNodes(node.Get(Children), diagOp) if err != nil { return diagOp, nil } // marked rows estimation error too high if diagOp.needUdateStatistics { switch operator { case IndexFullScan, IndexRangeScan, TableFullScan, TableRangeScan, TableRowIDScan, Selection: actRows := node.Get(ActRows).MustFloat64() estRows := node.Get(EstRows).MustFloat64() if actRows == 0 || estRows == 0 { actRows = actRows + 1 estRows = estRows + 1 } if actRows/estRows > 100 || estRows/actRows > 100 { diagnosis = append(diagnosis, HighEstError) } default: diagOp.needUdateStatistics = false } } switch operator { // index join case IndexJoin, IndexMergeJoin, IndexHashJoin: // only use in build rootGroupInfo := node.Get(RootGroupExecInfo) rootGroupInfoCount := len(rootGroupInfo.MustArray()) if rootGroupInfoCount > 0 { for i := range rootGroupInfoCount { joinTaskCountStr := rootGroupInfo.GetIndex(i).GetPath("inner", "task").MustString() joinTaskCount, _ := strconv.Atoi(joinTaskCountStr) if joinTaskCount > JoinTaskThreshold { diagnosis = append(diagnosis, IndexJoinBuildSideTooLarge) break } } } // unreasonable index return table plan case IndexLookUpReader: cNode := getBuildChildrenWithDriverSide(node) if getOperatorType(cNode) != Selection { break } if len(cNode.Get(Children).MustArray()) != 1 { break } gNode := cNode.Get(Children).GetIndex(0) if getOperatorType(gNode) != IndexFullScan { break } if !strings.Contains(gNode.Get(OperatorInfo).MustString(), "keep order:false") { break } cNodeActRows := cNode.Get(ActRows).MustFloat64() gNodeActRows := gNode.Get(ActRows).MustFloat64() if cNodeActRows/gNodeActRows > returnTableThreshold { diagnosis = append(diagnosis, BadIndexForIndexLookUp) } case Selection: if len(node.Get(Children).MustArray()) != 1 { break } cNode := node.Get(Children).GetIndex(0) if getOperatorType(cNode) != TableFullScan { break } if node.Get(StoreType).MustString() != "tikv" { break } if !useComparisonOperator(operatorInfo) { break } if node.Get(ActRows).MustFloat64() < 10000 && cNode.Get(ActRows).MustFloat64() > 5000000 { diagnosis = append(diagnosis, GoodFilterOnTableFullScan) } case TableFullScan: if node.Get(StoreType).MustString() == "tikv" && node.Get(ActRows).MustFloat64() > 1000000000 { diagnosis = append(diagnosis, TiKVHugeTableScan) } } // set diagnosis node.Set(Diagnosis, diagnosis) return diagOp, nil } func diagnosticOperatorNodes(nodes *simplejson.Json, diagOp diagnosticOperation) (diagnosticOperation, error) { length := len(nodes.MustArray()) // no children nodes if length == 0 { diagOp.needUdateStatistics = true return diagOp, nil } var needUdateStatistics bool for i := range length { c := nodes.GetIndex(i) n, err := diagnosticOperatorNode(c, diagOp) if err != nil { return diagOp, err } if n.needUdateStatistics { needUdateStatistics = n.needUdateStatistics } } diagOp.needUdateStatistics = needUdateStatistics return diagOp, nil } // cut go 1.18 strings.Cut. func cut(s, sep string) (before, after string, found bool) { if before0, after0, ok := strings.Cut(s, sep); ok { return before0, after0, true } return s, "", false } func getScanDatabase(node *simplejson.Json) string { accessObjects := node.Get(AccessObjects) length := len(accessObjects.MustArray()) if length == 0 { return "" } for i := range length { database := accessObjects.GetIndex(i).GetPath(ScanObject, "database").MustString() if database != "" { return database } } return "" } // useComparisonOperator // matching rules: only match the eq/ge/gt/le/lt/isnull/in functions on a single column // for example: // eq(test.t.a, 1) ture // eq(minus(test.t1.b, 1), 1) false // eq(test.t.a, 1), eq(test.t.a, 2) ture // eq(test.t.a, 1), eq(test.t.b, 1) false // in(test.t.a, 1, 2, 3, 4) ture // in(test.t.a, 1, 2, 3, 4), in(test.t.b, 1, 2, 3, 4) false. func useComparisonOperator(operatorInfo string) bool { useComparisonOperator := false columnSet := make(map[string]bool) for _, op := range needCheckOperator { if strings.Contains(operatorInfo, op) { useComparisonOperator = true n := strings.Count(operatorInfo, op+"(") for range n { column := "" s1, s, _ := cut(operatorInfo, op+"(") s, s2, _ := cut(s, ")") if strings.Contains(s, "(") { return false } switch op { case "isnull": // not(isnull(test2.t1.a)) true if strings.HasSuffix(s1, "not(") && strings.HasPrefix(s2, ")") { s1 = strings.TrimRight(s1, "not(") s2 = strings.TrimLeft(s2, ")") } // isnull(test.t.a) column = strings.TrimSpace(s) // if strings. case "in": // in(test.t.a, 1, 2, 3, 4) true slist := strings.Split(s, ",") column = strings.TrimSpace(slist[0]) // in(test.t.a, 1, 2, test.t.b, 4) false for _, c := range slist[1:] { c = strings.TrimSpace(c) if strings.Count(c, ".") == 2 && !strings.Contains(c, `"`) { return false } } default: // eq(test.t.a, 1) true slist := strings.Split(s, ",") if len(slist) != 2 { return false } c := "" c1, c2 := strings.TrimSpace(slist[0]), strings.TrimSpace(slist[1]) if strings.Count(c1, ".") == 2 && !strings.Contains(c1, `"`) { column = c1 c = c2 } else if strings.Count(c2, ".") == 2 && !strings.Contains(c2, `"`) { column = c2 c = c1 } // eq(test2.t1.a, test2.t2.a) false database := strings.Split(column, ".")[0] if len(slist) > 1 && strings.HasPrefix(c, database) { return false } } if column != "" { columnSet[column] = true } operatorInfo = s1 + s2 } } } if useComparisonOperator { if strings.Count(operatorInfo, "(") == strings.Count(operatorInfo, ")") && strings.Count(operatorInfo, "(") > 0 { return false } // single column if len(columnSet) != 1 { return false } } return useComparisonOperator } func analyzeDuration(bp []byte) ([]byte, error) { // new simple json vp, err := simplejson.NewJson(bp) if err != nil { return nil, err } if vp.Get(DiscardedDueToTooLong).MustBool() { return vp.MarshalJSON() } rootTs := vp.Get(MainTree).GetPath(RootBasicExecInfo, Time).MustString() // main mainConcurrency := newConcurrency() _, err = analyzeDurationNode(vp.Get(MainTree), mainConcurrency) if err != nil { return nil, err } vp.Get(MainTree).Set(Duration, rootTs) // ctes ctesConcurrency := newConcurrency() _, err = analyzeDurationNodes(vp.Get(CteTrees), Default, ctesConcurrency) if err != nil { return nil, err } return vp.MarshalJSON() } // analyzeDurationNode set node.duration. func analyzeDurationNode(node *simplejson.Json, concurrency concurrency) (time.Duration, error) { // get duration time ts := node.GetPath(RootBasicExecInfo, Time).MustString() // cop task if ts == "" { ts = getCopTaskDuration(node, concurrency) } else { ts = getOperatorDuration(ts, concurrency) } operator := getOperatorType(node) duration, err := time.ParseDuration(ts) if err != nil { duration = 0 } // get current_node concurrency concurrency = getConcurrency(node, operator, concurrency) subDuration, err := analyzeDurationNodes(node.Get(Children), operator, concurrency) if err != nil { return 0, err } if duration < subDuration { duration = subDuration } // set node.Set(Duration, duration.String()) return duration, nil } // analyzeDurationNodes return max(node.duration). func analyzeDurationNodes(nodes *simplejson.Json, operator operator, concurrency concurrency) (time.Duration, error) { length := len(nodes.MustArray()) // no children nodes if length == 0 { return 0, nil } var durations []time.Duration if operator == Apply { for i := range length { n := nodes.GetIndex(i) if isBuildSide(n) { newConcurrency := concurrency newConcurrency.applyConcurrency = 1 d, err := analyzeDurationNode(n, newConcurrency) if err != nil { return 0, err } durations = append(durations, d) // get probe concurrency var cacheHitRatio, actRows float64 rootGroupInfo := n.Get(RootGroupExecInfo) for i := 0; i < len(rootGroupInfo.MustArray()); i++ { cacheHitRatioStr := strings.TrimRight(rootGroupInfo.GetIndex(i).Get(CacheHitRatio).MustString(), "%") if cacheHitRatioStr == "" { continue } cacheHitRatio, err = strconv.ParseFloat(cacheHitRatioStr, 64) if err != nil { return 0, err } } actRowsStr := n.Get(ActRows).MustString() if actRowsStr == "" { continue } actRows, err = strconv.ParseFloat(n.Get(ActRows).MustString(), 64) if err != nil { return 0, err } taskCount := int(actRows * (1 - cacheHitRatio/100)) taskCountAvg := taskCount / concurrency.joinConcurrency * concurrency.shuffleConcurrency * concurrency.tableConcurrency if taskCountAvg < concurrency.applyConcurrency { concurrency.applyConcurrency = taskCountAvg } break } } for i := range length { n := nodes.GetIndex(i) if isProbeSide(n) { d, err := analyzeDurationNode(n, concurrency) if err != nil { return 0, err } durations = append(durations, d) break } } } else { for i := range length { var d time.Duration var err error n := nodes.GetIndex(i) switch operator { case IndexJoin, IndexMergeJoin, IndexHashJoin: if isProbeSide(n) { d, err = analyzeDurationNode(n, concurrency) } else { // build: set joinConcurrency == 1 newConcurrency := concurrency newConcurrency.joinConcurrency = 1 d, err = analyzeDurationNode(n, newConcurrency) } case IndexLookUpReader, IndexMergeReader: if isProbeSide(n) { d, err = analyzeDurationNode(n, concurrency) } else { // build: set joinConcurrency == 1 newConcurrency := concurrency newConcurrency.tableConcurrency = 1 d, err = analyzeDurationNode(n, newConcurrency) } // concurrency: suffle -> StreamAgg/Window/MergeJoin -> Sort -> ShuffleReceiver case ShuffleReceiver: newConcurrency := concurrency newConcurrency.shuffleConcurrency = 1 d, err = analyzeDurationNode(n, newConcurrency) default: d, err = analyzeDurationNode(n, concurrency) } if err != nil { return 0, err } durations = append(durations, d) } } // get max duration sort.Slice(durations, func(p, q int) bool { return durations[p] > durations[q] }) return durations[0], nil } func isProbeSide(node *simplejson.Json) bool { return labelsContains(node, ProbeSide) } func isBuildSide(node *simplejson.Json) bool { return labelsContains(node, BuildSide) } func labelsContains(node *simplejson.Json, label string) bool { labels := node.Get("labels") length := len(labels.MustArray()) if length == 0 { return false } for i := range length { if labels.GetIndex(i).MustString() == label { return true } } return false } func getOperatorType(node *simplejson.Json) operator { operator := node.Get(OperatorName).MustString() switch { case strings.HasPrefix(operator, "IndexJoin"): return IndexJoin case strings.HasPrefix(operator, "IndexMergeJoin"): return IndexMergeJoin case strings.HasPrefix(operator, "IndexHashJoin"): return IndexHashJoin case strings.HasPrefix(operator, "Apply"): return Apply case strings.HasPrefix(operator, "Shuffle") && !strings.Contains(operator, "ShuffleReceiver"): return Shuffle case strings.HasPrefix(operator, "ShuffleReceiver"): return ShuffleReceiver case strings.HasPrefix(operator, "IndexLookUp"): return IndexLookUpReader case strings.HasPrefix(operator, "IndexMerge"): return IndexMergeReader case strings.HasPrefix(operator, "IndexFullScan"): return IndexFullScan case strings.HasPrefix(operator, "IndexRangeScan"): return IndexRangeScan case strings.HasPrefix(operator, "TableFullScan"): return TableFullScan case strings.HasPrefix(operator, "TableRangeScan"): return TableRangeScan case strings.HasPrefix(operator, "TableRowIDScan"): return TableRowIDScan case strings.HasPrefix(operator, "Selection"): return Selection default: return Default } } func getBuildChildrenWithDriverSide(node *simplejson.Json) *simplejson.Json { nodes := node.Get(Children) length := len(nodes.MustArray()) // no children nodes if length == 0 { return nil } for i := range length { n := nodes.GetIndex(i) if isBuildSide(n) { return n } } return nil } func getConcurrency(node *simplejson.Json, operator operator, concurrency concurrency) concurrency { // concurrency, copConcurrency rootGroupInfo := node.Get(RootGroupExecInfo) rootGroupInfoCount := len(rootGroupInfo.MustArray()) if rootGroupInfoCount > 0 { for i := range rootGroupInfoCount { switch operator { case IndexJoin, IndexMergeJoin, IndexHashJoin: tmpJoinConcurrencyStr := rootGroupInfo.GetIndex(i).GetPath("inner", "concurrency").MustString() tmpJoinConcurrency, _ := strconv.Atoi(tmpJoinConcurrencyStr) joinTaskCountStr := rootGroupInfo.GetIndex(i).GetPath("inner", "task").MustString() joinTaskCount, _ := strconv.Atoi(joinTaskCountStr) joinTaskCountAvg := joinTaskCount / concurrency.applyConcurrency * concurrency.shuffleConcurrency * concurrency.tableConcurrency // task count as concurrency if joinTaskCountAvg < tmpJoinConcurrency { tmpJoinConcurrency = joinTaskCountAvg } if tmpJoinConcurrency > 0 { concurrency.joinConcurrency = tmpJoinConcurrency * concurrency.joinConcurrency } case Apply: tmpApplyConcurrencyStr := rootGroupInfo.GetIndex(i).GetPath("Concurrency").MustString() tmpApplyConcurrency, _ := strconv.Atoi(tmpApplyConcurrencyStr) if tmpApplyConcurrency > 0 { concurrency.applyConcurrency = tmpApplyConcurrency * concurrency.applyConcurrency } case IndexLookUpReader, IndexMergeReader: tmpTableConcurrencyStr := rootGroupInfo.GetIndex(i).GetPath("table_task", "concurrency").MustString() tmpTableConcurrency, _ := strconv.Atoi(tmpTableConcurrencyStr) tableTaskNumStr := rootGroupInfo.GetIndex(i).GetPath("table_task", "num").MustString() tableTaskNum, _ := strconv.Atoi(tableTaskNumStr) tableTaskNumAvg := tableTaskNum / concurrency.joinConcurrency * concurrency.applyConcurrency * concurrency.shuffleConcurrency if tableTaskNumAvg < tmpTableConcurrency { tmpTableConcurrency = tableTaskNumAvg } if tmpTableConcurrency > 0 { concurrency.tableConcurrency = tmpTableConcurrency * concurrency.copConcurrency } case Shuffle: tmpShuffleConcurrencyStr := rootGroupInfo.GetIndex(i).Get("ShuffleConcurrency").MustString() tmpShuffleConcurrency, _ := strconv.Atoi(tmpShuffleConcurrencyStr) if tmpShuffleConcurrency > 0 { concurrency.shuffleConcurrency = tmpShuffleConcurrency * concurrency.shuffleConcurrency } } tmpCopConcurrencyStr := rootGroupInfo.GetIndex(i).GetPath("cop_task", "distsql_concurrency").MustString() tmpCopConcurrency, _ := strconv.Atoi(tmpCopConcurrencyStr) if tmpCopConcurrency > 0 { concurrency.copConcurrency = tmpCopConcurrency * concurrency.copConcurrency } } } return concurrency } func getCopTaskDuration(node *simplejson.Json, concurrency concurrency) string { storeType := node.GetPath(StoreType).MustString() // task == 1 ts := node.GetPath(CopExecInfo, fmt.Sprintf("%s_task", storeType), "time").MustString() if ts == "" { switch node.GetPath(TaskType).MustString() { case "cop": // cop task count taskCountStr := node.GetPath(CopExecInfo, fmt.Sprintf("%s_task", storeType), "tasks").MustString() taskCount, _ := strconv.Atoi(taskCountStr) maxTS := node.GetPath(CopExecInfo, fmt.Sprintf("%s_task", storeType), "proc max").MustString() maxDuration, err := time.ParseDuration(maxTS) if err != nil { ts = maxTS break } avgTS := node.GetPath(CopExecInfo, fmt.Sprintf("%s_task", storeType), "avg").MustString() avgDuration, err := time.ParseDuration(avgTS) if err != nil { ts = maxTS break } var tsDuration time.Duration n := float64(taskCount) / float64( concurrency.joinConcurrency*concurrency.tableConcurrency*concurrency.applyConcurrency*concurrency.shuffleConcurrency) if n > float64(concurrency.copConcurrency) { tsDuration = time.Duration(float64(avgDuration) * n / float64(concurrency.copConcurrency)) } else { tsDuration = time.Duration(float64(avgDuration) * n) } if tsDuration < maxDuration { ts = maxTS } else { ts = tsDuration.String() } // tiflash case "batchCop", "mpp": ts = node.GetPath(CopExecInfo, fmt.Sprintf("%s_task", storeType), "proc max").MustString() default: ts = "0s" } } return ts } func getOperatorDuration(ts string, concurrency concurrency) string { t, err := time.ParseDuration(ts) if err != nil { return "0s" } return time.Duration(float64(t) / float64(concurrency.joinConcurrency*concurrency.tableConcurrency*concurrency.applyConcurrency*concurrency.shuffleConcurrency)). String() } func formatBinaryPlanJSON(bp []byte) ([]byte, error) { // new simple json vp, err := simplejson.NewJson(bp) if err != nil { return nil, err } if vp.Get(DiscardedDueToTooLong).MustBool() { return vp.MarshalJSON() } vp.Set(DiscardedDueToTooLong, false) // main err = formatNode(vp.Get(MainTree)) if err != nil { return nil, err } // ctes err = formatChildrenNodes(vp.Get(CteTrees)) if err != nil { return nil, err } return vp.MarshalJSON() } // formatNode // format diskBytes memoryByte to string // format rootBasicExecInfo rootGroupExecInfo copExecInfo field to json // for example: // {"copExecInfo" : "tikv_task:{time:0s, loops:1}, scan_detail: {total_process_keys: 8, total_process_keys_size: 360, total_keys: 9, rocksdb: {delete_skipped_count: 0, key_skipped_count: 8, block: {cache_hit_count: 1, read_count: 0, read_byte: 0 Bytes}}}"}. func formatNode(node *simplejson.Json) error { var err error for _, key := range needSetNA { if node.Get(key).MustString() == "-1" || node.Get(key).MustString() == "" { node.Set(key, "N/A") } } if len(node.Get("labels").MustArray()) == 0 { node.Set("labels", []string{}) } if len(node.Get(AccessObjects).MustArray()) == 0 { node.Set(AccessObjects, []interface{}{}) } // actRows string -> uint64 actRows, err := strconv.ParseUint(node.Get(ActRows).MustString(), 10, 64) if err != nil { actRows = 0 } node.Set(ActRows, float64(actRows)) if node.Get(EstRows).MustFloat64() == 0 { node.Set(EstRows, 0) } for _, key := range needJSONFormat { if key == RootGroupExecInfo { slist := node.Get(key).MustStringArray() newSlist := []interface{}{} for _, s := range slist { sJSON, err := formatJSON(s) if err != nil { newSlist = append(newSlist, s) } newSlist = append(newSlist, sJSON) } node.Set(key, newSlist) } else { s := node.Get(key).MustString() sJSON, err := formatJSON(s) if err != nil { continue } node.Set(key, sJSON) } } c := node.Get(Children) err = formatChildrenNodes(c) if err != nil { return err } return nil } func formatChildrenNodes(nodes *simplejson.Json) error { length := len(nodes.MustArray()) // no children nodes if length == 0 { return nil } for i := range length { c := nodes.GetIndex(i) err := formatNode(c) if err != nil { return err } } return nil } func formatJSON(s string) (*simplejson.Json, error) { s = `{` + s + `}` s = strings.ReplaceAll(s, "{", `{"`) s = strings.ReplaceAll(s, "}", `"}`) s = strings.ReplaceAll(s, ":", `":"`) s = strings.ReplaceAll(s, ",", `","`) s = strings.ReplaceAll(s, `" `, `"`) s = strings.ReplaceAll(s, `}"`, `}`) s = strings.ReplaceAll(s, `"{`, `{`) s = strings.ReplaceAll(s, `{""}`, "{}") return simplejson.NewJson([]byte(s)) } ///////////////// func GenerateBinaryPlanText(db *gorm.DB, b string) (string, error) { type binaryPlanText struct { Text string `gorm:"column:binary_plan_text"` } ret := &binaryPlanText{} err := db.Raw(fmt.Sprintf("select tidb_decode_binary_plan('%s') as binary_plan_text", b)).Find(ret).Error if err != nil { return "", err } return ret.Text, err } ================================================ FILE: pkg/apiserver/utils/binary_plan_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "fmt" "strings" "testing" "github.com/bitly/go-simplejson" "github.com/stretchr/testify/assert" ) var bpTestStr = "SiwKRgoGU2hvd18yKQAFAYjwPzAFOAFAAWoVdGltZTozNC44wrVzLCBsb29wczoygAH//w0COAGIAf///////////wEYAQ==" func TestGenerateBinaryPlan(t *testing.T) { _, err := GenerateBinaryPlan(bpTestStr) if err != nil { t.Fatalf("generate Visual plan failed: %v", err) } } func TestGenerateBinaryPlanJson(t *testing.T) { _, err := GenerateBinaryPlanJSON(bpTestStr) if err != nil { t.Fatalf("generate Visual plan failed: %v", err) } } func TestUseComparisonOperator(t *testing.T) { assert.True(t, useComparisonOperator("eq(test.t.a, 1)")) assert.False(t, useComparisonOperator("eq(minus(test.t1.b, 1), 1)")) assert.True(t, useComparisonOperator("eq(test.t.a, 1), eq(test.t.a, 2)")) assert.False(t, useComparisonOperator("eq(test.t.a, 1), eq(test.t.b, 1)")) assert.True(t, useComparisonOperator("in(test.t.a, 1, 2, 3, 4)")) assert.False(t, useComparisonOperator("in(test.t.a, 1, 2, 3, 4), in(test.t.b, 1, 2, 3, 4)")) assert.False(t, useComparisonOperator("in(test.t.a, 1, 2, 3, 4, test.t.b)")) assert.True(t, useComparisonOperator("isnull(test2.t1.a)")) assert.True(t, useComparisonOperator("not(isnull(test2.t1.a))")) assert.False(t, useComparisonOperator("eq(test2.t1.a, test2.t2.a)")) assert.True(t, useComparisonOperator("eq(1, test2.t2.a)")) assert.False(t, useComparisonOperator("in(test.t.a, 1, 2, test.t.b, 4)")) assert.False(t, useComparisonOperator("in(test.t.a, 1, 2, 3, 4), eq(1, test2.t2.a), eq(test.t.a, 1), eq(test.t.a, 2), isnull(test2.t1.a)")) assert.True(t, useComparisonOperator("in(test.t.a, 1, 2, 3, 4), eq(1, test.t.a), eq(test.t.a, 1), eq(test.t.a, 2), isnull(test.t.a)")) assert.True(t, useComparisonOperator("not(isnull(test2.table1.a))")) assert.False(t, useComparisonOperator(`eq(information_schema.cluster_slow_query.conn_id, 4842609848539415539), eq(information_schema.cluster_slow_query.digest, "6e88a1c8f7bfb008e6ead01c1ad9e47922c145e95a3a037381ac8a088e25e60b")`)) assert.True(t, useComparisonOperator("lt(imdbload.movie_info.movie_id, 1000)")) } func TestFormatJSON(t *testing.T) { _, err := formatJSON(`tikv_task:{time:0s, loops:1}, scan_detail: {total_process_keys: 8, total_process_keys_size: 360, total_keys: 9, rocksdb: {delete_skipped_count: 0, key_skipped_count: 8, block: {cache_hit_count: 1, read_count: 0, read_byte: 0 Bytes}}}`) assert.Nil(t, err) } func TestTooLong(t *testing.T) { bp := "AgQgAQ==" vp, err := GenerateBinaryPlanJSON(bp) assert.Nil(t, err) vpJSON, err := simplejson.NewJson([]byte(vp)) assert.Nil(t, err) assert.True(t, vpJSON.Get("discardedDueToTooLong").MustBool()) } func TestBinaryPlanIsNil(t *testing.T) { vp, err := GenerateBinaryPlanJSON("") assert.Nil(t, err) assert.Len(t, vp, 0) } func TestHighEstError(t *testing.T) { vp, err := GenerateBinaryPlanJSON("1gmQCtEJCgdMaW1pdF84Eu0ICg5UYWJsZVJlYWRlcl8xMxKpBgoITAUfXDEyEpsDCgxTZWxlY3Rpb25fMTESxwEKEAUx8IZGdWxsU2Nhbl8xMCHw694zF1ZHQSlZHdfaNxuEQDDTuA44AkACShUKEwoIaW1kYmxvYWQSB2tleXdvcmRSEGtlZXAgb3JkZXI6ZmFsc2VqWnRpa3ZfdGFzazp7cHJvYyBtYXg6MjAxbXMsIG1pbjowcywgYXZnOiAxMDAuNW1zLCBwODA6MjAFIRRwOTU6MjAFC1BpdGVyczoyMzcsIHRhc2tzOjJ9cP8RAQQBeBEKLP8BIUc/mTzKe0dBKQUUCD9/QAGxJFI7Z3QoY2FzdCgRtQAuDbSQLnBob25ldGljX2NvZGUsIGRvdWJsZSBCSU5BUlkpLCAyMClqWFLFABA2Mm1zLDrFAAAzBa0AcAHDCR8FwwkL/sMAWBNvZmZzZXQ6MCwgY291bnQ6NTAwar0CVpwAADNOnAAELjUBFgmeBSEJngULSp4AoCwgc2Nhbl9kZXRhaWw6IHt0b3RhbF9wcm9jZXNzX2tleXM6IDIzNjYyIZVCHABAX3NpemU6IDE0MzIzNTkxLCAJIx03bDksIHJvY2tzZGI6IHtkZWxldGVfc2tpcHBlZF8J4hggMCwga2V5PhYAADINdUBibG9jazoge2NhY2hlX2hpdBE3HDI3OCwgcmVhLkgABQ84Ynl0ZTogMCBCeXRlc319bkQCDHcjSEFZRCgBQAFSDWRhdGE6TG1ELFoVdGltZToyNzIuNyFXNGxvb3BzOjFi5AFjb3BfQaUkOiB7bnVtOiAyLEX2DCAyNjkpgiBtaW46IDIuODQBOCBhdmc6IDEzNi5FTwhwOTUuKQAIYXhfIXA5Ngw1OTYsASVOFwAIdG90BRcBQQWTAREUd2FpdDogJeMMcnBjXxGRAQwFwAAgAcEFexBjb3ByXyVLEDogZGlzgT4EZCwBClh0c3FsX2NvbmN1cnJlbmN5OiAxfXCFND51AyUxAAABAQhAf0AlMU6yAlo3AVbNAwQYAQ==") assert.Nil(t, err) assert.True(t, strings.Contains(vp, HighEstError)) } func TestDiskSpill(t *testing.T) { vp, err := GenerateBinaryPlanJSON("6Q5ICuQOCglIYXNoQWdnXzgS8w0KCgEOdEpvaW5fORL6BQoOSW5kZXhSZWFkZXJfMTQSrQMKEAUTUEZ1bGxTY2FuXzEzIQAAAADndopBKQEJ8HmAhC5BMIAOOAJAAkonCiUKBHRlc3QSAnQxGhkKB1BSSU1BUlkSBnNfd19pZBIGc19pX2lkUhBrZWVwIG9yZGVyOmZhbHNlaq4CdGlrdl90YXNrOntwcm9jIG1heDoxbXMsIG1pbjowcywgYXZnOiA3NTDCtXMsIHA4MAkeCHA5NQkJUGl0ZXJzOjE0LCB0YXNrczo0fSwgcwGyiGRldGFpbDoge3RvdGFsX3Byb2Nlc3Nfa2V5czogMTc5MiwgRhoALF9zaXplOiA5NDk3NhEgDTK4ODEwLCByb2Nrc2RiOiB7ZGVsZXRlX3NraXBwZWRfY291bnQ6IDMyODgsIGtleV86GQBYNTE1MCwgYmxvY2s6IHtjYWNoZV9oaXQROBgxNywgcmVhFUgBawEPRGJ5dGU6IDAgQnl0ZXN9fX1w/xEBBAF4EQo0/wEaAQIhq6qqqjk/UkE9nhwIOAFAAVIWaSHcADo+zwFoWhN0aW1lOjcuMm1zLCBsb29wczoxYt8BY29wKY8kIHtudW06IDQsICGTECAyLjMzASogbWluOiAxLjM3AQ0lnAwxLjc5AQ0hkxknBGF4JXMtPgA5IW8IcDk1QhQACHRvdAUUBDogBV4BDxh3YWl0OiAxAVMMcnBjXxGGAQwFsxAgNy4wNQEeEGNvcHJfOUugcmF0aW86IDAuMDAsIGRpc3RzcWxfY29uY3VycmVuY3k6IDE1fXCShxM9OggSjgY6/QIIOBK1Rv0CADdF/Qhtc5A9XwjAhD1C/gIAMrb+Agi1AnRO/gIAOAHdIWhZ/wQuNQXyDHA4MDo1bAAxJTJxAAQzN3ECCDExNZIEAwwzODA4UeQuHgNtBBAyMDE4MmFXZT9pBQgzODZBmnoFAwQ0MWErSgUDEDgwMzUsZgUDBDIy9gUDIAEhVVVVVRq/VzqmAWYGAww3WhZ0YQYIMTcuRXBpBxA5ODJi5kIJAwwxMTUsaQsIMy4wBS4hoxQgNDEyLjSFnGUNEDYyOC4yBQ9lDwgxLjMpzjYPAxAyMDE2LAEiJXktXQAwJY0FEgg6IDVBBHkOZW5hAmUOBYsBDgW9DCA3MC4FL14QAwQ5NmoQAwztgPICfREgIYlXpqxvjrhCgUggopQabUIwgJB6JUNQFENBUlRFU0lBTiBpbm5lciBqb2luLUEQNTQuNnOVR1g5NTRihAFidWlsZF9oYXNoX3RhYmxlOqmKEDozNjAuRcUcZmV0Y2g6MjEJvgUvRDozMzkuM21zfSwgcHJvYmU6ey7BAwAxJSIQYWw6NTXBIQRheAkJCSoUNTQuN3MsDVUBZFQ4bXN9cOT51wd4gOy4CyHlutab7Ui7BdkQAABC+EAF1SQ1Z3JvdXAgYnk6wcFQLnN0b2NrLnNfaV9pZCwgZnVuY3M6pds8KDEpLT5Db2x1bW4jMzdaESV6BYcsbG9vcHM6MXD0URgB") assert.Nil(t, err) assert.True(t, strings.Contains(vp, DiskSpill)) } func TestGoodFilterOnTableFullScan(t *testing.T) { vp, err := GenerateBinaryPlanJSON("zQeYCsgHCg1UYWJsZVJlYWRlcl83Ev8ECgtTZWxlY3Rpb25fNhLLAQoPBSLwfUZ1bGxTY2FuXzUhAADA0p/kTUIpAAAAgEhlfEEwiKmZDjgCQAJKGAoWCghpbWRibG9hZBIKbW92aWVfaW5mb1IQa2VlcCBvcmRlcjpmYWxzZWpbdGlrdl90YXNrOntwcm9jIG1heDo2NDVtcywgbWluOjBzLCBhdmc6IDQyMwEUGHA4MDo0ODYFCxA5NTo1NQULXGl0ZXJzOjI5MzY0LCB0YXNrczo2Nn1w/xEBBAF4EQpY/wEhAAC4xj7/TUIp3lAMtdp/qUAw7Q8BuBBSJmx0KBG3AC4ZthELKGQsIDEwMDApasoCUrUABDcyAZY2tQAMNDEuNwEWAbcENTEFIQW3BDg4ARZWtwCoLCBzY2FuX2RldGFpbDoge3RvdGFsX3Byb2Nlc3Nfa2V5czogMjk3NzQ5OAHuQh4ASF9zaXplOiAyNjU1OTgyODQ4LCAJJRk7sDUwNTAsIHJvY2tzZGI6IHtkZWxldGVfc2tpcHBlZF9jb3VudDogMCwga2V5XzoWAGgzMTUwMzIzMCwgYmxvY2s6IHtjYWNoZV9oaXQROSQ0NzgzNiwgcmVhLkwABQ84Ynl0ZTogMCBCeXRlc319XqQBGF15cUA6/w82pAEkAUABUhBkYXRhOl2dbFoTdGltZToyLjQxcywgbG9vcHM6M2LlAWNvcF9BCig6IHtudW06IDY2LEVcFCA3MDMuNiF+IaoQIDEuMTEBDUlkBDU3La8EOTUBMgg4LjQBHAhtYXglfy1BGDcyMjA3NCwhyS4XABA1NjQ0NimRBRcIOiAzQb0wdG90X3dhaXQ6IDE3MAFODHJwY18VkgENBcAwIDMwLjJzLCBjb3ByXyVREDogZGlzYZcEZCwBCpR0c3FsX2NvbmN1cnJlbmN5OiAxNX1w/r0DeP///////////wEYAQ==") assert.Nil(t, err) assert.True(t, strings.Contains(vp, GoodFilterOnTableFullScan)) } func TestBadIndexForIndexLookUp(t *testing.T) { vp, err := GenerateBinaryPlanJSON("uA6YCrMOCg1JbmRleExvb2tVcF84EpYHCgtTZWxlY3Rpb25fNxLqAQoPBSLwQEZ1bGxTY2FuXzUhAACAxaCT8kEpAAAAABEGNEEwkYxQOAJAAko4CjYKCGltZGJsb2FkEghha2FfbmFtZRogChNhDQwwX2lkeF9wZXJzb24SCQkI8FhfaWRSEGtlZXAgb3JkZXI6ZmFsc2VqW3Rpa3ZfdGFzazp7cHJvYyBtYXg6NTA1bXMsIG1pbjowcywgYXZnOiAyMzguN21zLCBwODA6NTA1bXMsIHA5NTo1MAUsLGl0ZXJzOjEyOTIsIAFOFHM6M31w/xEBBAF4EQo8/wEaAQEpZ2ZmZtoEMEEwignTKFIsZ3QocGx1cyhpDdcELmENygQucAXGXF9pZCwgMSksIDEwKVoYdGltZToyNTUuNgGSDGxvb3ABfBw4NWLlAWNvcAnPJCB7bnVtOiAzLCAB0xQgNjI2LjcBLhxtaW46IDQuMAUNAGEF2ww5NC45ARsB0B0pUGF4X3Byb2Nfa2V5czogOTYwMDAwLAElThcACHRvdAUXEDogODQyAU0BERh3YWl0OiAzAQ8McnBjXxGRAQwFwwwgODg0CZZQY29wcl9jYWNoZTogZGlzYWJsZWQsAQpcdHNxbF9jb25jdXJyZW5jeTogMTV9ar8CUrgBADQljAHiADA1uAA1BekMcDgwOg0fAeYNC062AVAsIHNjYW5fZGV0YWlsOiB7dG90YWwF2whlc3MtDBwxMzEyMjczLAHlOh0AOF9zaXplOiA2MDM2NDU1OBEjKUQJOKA2LCByb2Nrc2RiOiB7ZGVsZXRlX3NraXBwZWRfY291bnQ6IDAsIGtleT4WAAk/KDMsIGJsb2NrOiB7JTEMX2hpdBE4HDkzMiwgcmVhLkkABQ84Ynl0ZTogMCBCeXRlc319WpwCFBK3BQoQVCF9EFJvd0lEZYoMNhoBAnmNProCEEoWChQKSo0DRmsDBFoVJfMQOC45cyxRswg0MjhKswIIMTU5TbUQMjQxLjFBPiHTCCAxLingRbUEODJJOSHOECAxOTEuRWY6tAIQMjAwMDNCswIQMTcyMjAlmSXXGDogMTEuN3MJEUmyADJFwkGnRbMFkQEOBcIYIDEzLjFzLL6zAgDGUrMCBDE5RZ8B4FWzBDY3adgUcDgwOjEwdZoIMTU2IQtNtBA5OTUsIIlqCDE1Oaa2AgQ2NgXpVrYCHDE0NjkxOTM4PrcCEDU4ODU0QVrStwIUOTIxMTU1arYCGDM4MTYxOTHuugIgIT4xsa60gddBNqICEAFAAVoWJbYQOS44NXNVeRgyODNijAFpwZWxLmnoBSxgIDkuNjhzLCBmZXRjaF9oYW5kbGU6IDM1NCmYOGJ1aWxkOiAzNjPCtXMsIEkzFDkuMzNzfSGTCGJsZVZUAAA1QfpFUQQ2OEE/LtQESH1wi/2MCHj///////////8BGAE=") assert.Nil(t, err) assert.True(t, strings.Contains(vp, BadIndexForIndexLookUp)) } func TestIndexJoinBuildSideTooLarge(t *testing.T) { vp, err := GenerateBinaryPlanJSON("tQ+gCrAPCgxJbmRleEpvaW5fMTISjAYKDlRhYmxlUmVhZGVyXzE4ErUDChAFE/SoAUZ1bGxTY2FuXzE3IQAAwNKf5E1CKQAAAIBIZXxBMIipmQ44AkACShAKDgoIaW1kYmxvYWQSAm1pUhBrZWVwIG9yZGVyOmZhbHNlassCdGlrdl90YXNrOntwcm9jIG1heDo2MzVtcywgbWluOjJtcywgYXZnOiA0MzguNG1zLCBwODA6NDk0bXMsIHA5NTo1OTRtcywgaXRlcnM6MjkzNjQsIHRhc2tzOjY2fSwgc2Nhbl9kZXRhaWw6IHt0b3RhbF9wcm9jZXNzX2tleXM6IDI5Nzc0OTg0LCB0b3RhbF9wcm9jZXNzX2tleXNfc2l6ZTogMjY1NTk4Mjg0OCwgdG90YWxfa2V5czogMjk3NzUwNTAsIHJvY2tzZGI6IHtkZWxldGVfc2tpcHBlZF9jb3VudDogMCwga2V5X3NraXBwZWRfY291bnQ6IDMxNTAzMjMwLCBibG9jazoge2NhY2hlX2hpdF9jb3VudDogNDc4MzYsIHJlYWRfY291bnQ6IDAsIHJlYWRfYnl0ZTogMCBCeXRlc319fXD///////////8BeP//////AQ0sARoBASGpOPsATIYQPqYBJAFAAVIVZGF0YTo+2AEsWhl0aW1lOjc0Ni45IV4UbG9vcHM6JV5UYuABY29wX3Rhc2s6IHtudW06IDY2LCW4FCAxLjQycym5ECA1LjQzATsAYSG9BDcwKbsEOTUBJQQzNgElCGF4XyFrJWgwOiA3MjIwNzQsIHA5NS4XACg1NjQ0NjQsIHRvdAUXGDogMzcuOXMJEQx3YWl0AToBZwxycGNfFY0BDQXBWCA0Ni41cywgY29wcl9jYWNoZTogZGlzQdkEZCwBCmx0c3FsX2NvbmN1cnJlbmN5OiAxNX1wl4a/igR4IT8FARQBEqoGCg0lJG0PDDkSzwNtDhBSYW5nZWUPADhhBSQAAADwPzCP9gI4SgQDEGl0UkZyATBAOiBkZWNpZGVkIGJ5IFtpbWRlJSwubW92aWVfaW5mby4BBSBfdHlwZV9pZF1BPQhlcCAuOgMEuQJOOgMEMW0hWgxpbjowcTcMNjPCtWEqBDgwBRQhggUlAGltMAg1NjFxMCHfBDN9jjMDEDQ3ODg3JYQEYWwllwhlc3MlyAhfc2llMBQyNDcyMTIRIgBrZWgoNTI4NDksIHJvY2vGKgMYMjA5NzMsIGInAxgyODQzOTcsaRkcY291bnQ6IDANDxhieXRlOiAwgigDJAIhpOLsw2cYb0A2yQE6JwM98QRaF0VmFDM4LjFzLG0lGDU4OTIwYuNCJQMlXm0oEDc3NC43MasUIDQ0MC42JaYAYWEsCDEuMmU5IasAICWsOisDBDQzQicDADMlayWLEDogMi41QfRhM2kiCDIuOWFyYRZlIxQ1ODcyNSwFEAXAECAxbTEwzigDZR4BAQABfS0gIU4beDyqUxhCIUYAgNkUIUc0tAFpbm5lciBqb2luLCAFDIl8cVgsLCBvdXRlciBrZXk6fhcDDUE2LAAIaW5maThILmlkLCBlcXVhbCBjb25kOmVxKIpRAHGKLkcACClaGCUnBDI3PeccOTMwNDcwYmgJxAB7xRYQOjU5LjSFbplQBDMwaV9pXoGKKG5zdHJ1Y3Q6MTkuoS0cZmV0Y2g6NDAFXKxidWlsZDo4OS40bXN9LCBwcm9iZToyMS41c3CVm6wMeP///////////wEYAQ==") assert.Nil(t, err) assert.True(t, strings.Contains(vp, IndexJoinBuildSideTooLarge)) } func TestPseudoEst(t *testing.T) { vp, err := GenerateBinaryPlanJSON("nhHwdQqABQoLSGFzaEpvaW5fMjUSugEKDFNlbGVjdGlvbl8yNhJPCg5DVEVGdWxsU2Nhbl8yNykAAAAAAAAQQDAJOAFAAUoKGghDVEU6Y3RlMVIKZGF0YTpDVEVfMFoTdGltZTo2LjJtcywgbG9vcHM6M3DwCxoBASEFQRTQVUApmpkBAQAJDUt4Uhtub3QoaXNudWxsKHRlc3QyLnRhYmxlMS5hKSlaE0ZQABQycPgFeP8RAQgBErw2vQAIOBJQOr0AADkJvRRAv0AwAjg2vQAIMlIKFb0IMVoUDW0MMTZtcza+ACQCIVZVVVVllQBBCUwEALkNTF6+ABAyLmMpKRlRADgZUTq/ABghEQ7jWw+ZFVkMEEAwBgGlWFI2aW5uZXIgam9pbiwgZXF1YWw6W2VxOiIBBCwgPTIBdABdDXQMMTkuMx10NGKKAWJ1aWxkX2hhc2hfJWAoOnt0b3RhbDoxOC4FniRmZXRjaDo2LjI5ATgFLnw6MTIuNW1zfSwgcHJvYmU6e2NvbmN1cnJlbmN5OjUsIAlBBDk0ASUQLCBtYXgBdwAxAT8JMCAzMjIuNMK1cywNXUA5NC4ybXN9cNSNARKJBgoFQ0EzCBLFBS59AhwxMxLYBAoOVEEDNFJlYWRlcl8xNhLhAgoQBRMARlGTCDE1IUVRCHh+QC2TABRBUSACQAJKEQoPCgUlGAQSBgXruDFSHmtlZXAgb3JkZXI6ZmFsc2UsIHN0YXRzOnBzZXVkb2rrAXRpa3ZfdGFzazp7RQYFxwRsb0HBEDF9LCBzYQccZGV0YWlsOiApOjxfcHJvY2Vzc19rZXlzOiA5LRAyFwAkX3NpemU6IDQ3NxEeCS2oMTAsIHJvY2tzZGI6IHtkZWxldGVfc2tpcHBlZF9jb3VudDogMCwga2V5XzoWAEw5LCBibG9jazoge2NhY2hlX2hpdB0yCHJlYS5BAAUPQGJ5dGU6IDAgQnl0ZXN9fX1wcTwE/wF9SClPBKBDMk8BEAFAAVIVZSI+fgFRaAQuMl1oHDNijgFjb3BfJUgcIHtudW06IDFJGgwgMS40TRoAYy0FKTIlMAQ6ICVwDHJwY18RNQEMJYoMIDEuM2WREGNvcHJfGfVQcmF0aW86IDAuMDAsIGRpc3RzcWxfLqICGCAxNX1wmwMd5AgaAQMJ5wTQUhHnABBBNgHncocElRplHUkXADI2iARNhhVQRBFOb24tUmVjdXJzaXZlIENURU7NBKEdBBKKbQwAMT4MAwA4QgwDDDIxEuBCDAMEMjAF1ggwwS2NnwSIw4GfUgwDADKCDAMA6u4MA2kMADJR7i4jA20MCDEwNhEeaQwAM94LAwAy/gsDYgsDGKuqqqqqg/JNJClOQSROCwMEMjBNHgAxrTtJHggzYo9qCwNJ0SEeLQUpMiUwfgwDZXDSDAMEqAI6DAMB6AjKTvsN6PUhXgwDwWHd1WXQCe5SDAMZUEoMA3VSZdssbG9vcHM6M3DwCxgB") assert.Nil(t, err) assert.True(t, strings.Contains(vp, PseudoEst)) } func TestTiKVHugeTableScan(t *testing.T) { vp, err := GenerateBinaryPlanJSON("2hTwWArVFAoNUHJvamVjdGlvbl8xOBLAEgoIQXBwbHlfMjASvQUKCExpbWl0XzIzEtUECg5UYWJsZVJlYWRlcl8zMRKkAwoRRXhjaGFuZ2VTZW5kZXJfMzASlAIKETYUORKVAQoQBTZQRnVsbFNjYW5fMjghAAAAAIA17EApAQnwbQAAEEAwgIAEOARAA0oOCgwKBHRwY2gSBHBhcnRSEGtlZXAgb3JkZXI6ZmFsc2VqL3RpZmxhc2hfdGFzazp7dGltZTo0NTguMW1zLCBsb29wczoxLCB0aHJlYWRzOjF9cP///////////wF4////CQwAAUqDAAWBTFIRb2Zmc2V0OjAsIGNvdW50OjRq/nIAenIAABkxQkBUeXBlOiBQYXNzVGhyb3VnaP56ABF6GKuqqqqq3qI9bywEOAFAAVIWZGF0YToRfwBTMcEIWhV0JVUUOTEuM21zNVUQYj9jb3ApdnQge251bTogMSwgbWF4OiAwcywgcHJvY19rZXlzOiAlO0hwcl9jYWNoZTogZGlzYWJsZWR9VooBCBoBAWKhAEaNAVacAAAzVlsAHBKFDAoMU2VsTd4UMzISowsKTZtUMzUSvgoKDFN0cmVhbUFnZ181MhLPCTbiAhA1MxL8Bi4kABA0MRKqAy5TAAw1MRLPQuECKDUwIQCUrdq20pRCQeFIs7nhsUEwzM2bvAQ4AkACShIKEE3jIAhsaW5laXRlbUrnAhhjdGlrdl90ReQhXCVqNDQuMjVzLCBtaW46Mzc1IZwkYXZnOiA5MDUuOAEOGHA4MDoxLjIhj2A5NToxLjc2cywgaXRlcnM6MTE3MzA2OCwgAVMUczoyNjIwWpEBUCEAfLmpGNuUQimamZlRXJysQTChyw29EFIwZ3QoYZ8ALhG7CC5sX2GpEGtleSwgBRkBDgQucBEVCClqZFrJAAA3EckAOAW7CckIMjUuZcgNyR3KADjSygAQnFxPzeEphxwAAADwPzC8FCGEKFIuZnVuY3M6c3VtQs4AWHF1YW50aXR5KS0+Q29sdW1uIzI5at8CVsYABDMzDcYINDI5IYEJxgQ4Nmk5DcYBuymQADghvmaQAQgsIHOhIThkZXRhaWw6IHt0b3RhbF8h/ghlc3NtXSwxMjAwMDIzMjQ0LCBGIABEX3NpemU6IDIzODM4MTcyMDcyIfIFR2mcCT94NTg2NCwgcm9ja3NkYjoge2RlbGV0ZV9za2lwcGVkX4n5YcgIa2V5PhYACUIJgRxibG9jazoge2XqDF9oaXQROyQxNTEwNCwgcmVhFU4YMzg5NjI4MA0VOGJ5dGU6IDYxLjcgR0J9fV6LAhjN7KdUMEZWZUg1wYG7ABGFu3m+DDQxWhSFtggzbTdlASRsb29wczo4Yu0BPrYEQfuNuQg1LjVhImFQECA0MzEueVMIMS4wARohtxAgMi4zMSHnBGF4JYwtShg0Njc5NDgsASMyFwAQNTgzNTMlnAUXGDogNDVtMS5BKDR0b3Rfd2FpdDogMW0xNAWmDHJwY1+lSxA0NTg1LAUPBcYYIDFoMTZtMqFZAGNKSwVwLCBkaXN0c3FsX2NvbmN1cnJlbmN5OiAxNX1wpgMu5gYIzWyvOkABqfoAH3kAVeVd8QA3Uk0BEDRwyOIBjl4AQrcFADGttzadAQQxMla3BRgaAQIhzey2CbkEmpkBAQTpPw25GBBsdCgwLCARtQQ3KVKqAAw4cJwLLqkAGCLiTVUwRnZRRwQQQA1OUBRDQVJURVNJQU4gaW5uZXIgam9pbh1SgaRJSQwyYhpDPWsQT0ZGLCBF9gELPm4ABIJQUm4ABLMBSiEFNjYFDG5hbWU2EgAMbWZncjYSABBicmFuZDYTAAh0eXA6NwCBCDYkABxjb250YWluZTpOAAByhW0McHJpY0IwABBtbWVudFoOAQAPOg4BPHCYOnj///////////8BGAE=") fmt.Println(vp) assert.Nil(t, err) assert.True(t, strings.Contains(vp, TiKVHugeTableScan)) } func TestCopTasksDuration(t *testing.T) { vp, err := GenerateBinaryPlanJSON("1QfICtAHCgxQcm9qZWN0aW9uXzUS2wYKBlNvcnRfNhKABgoNVGFibGVSZWFkZXJfORKsAwoPBRL0tgFGdWxsU2Nhbl84IQAAACAXuMBBKQAAAACAhC5BMMCEPTgCQAJKDwoNCgR0ZXN0EgVzdG9ja1IQa2VlcCBvcmRlcjpmYWxzZWrFAnRpa3ZfdGFzazp7cHJvYyBtYXg6NjFtcywgbWluOjBzLCBhdmc6IDE5LjNtcywgcDgwOjMwbXMsIHA5NTo0MW1zLCBpdGVyczoxNTQ2LCB0YXNrczoxNDZ9LCBzY2FuX2RldGFpbDoge3RvdGFsX3Byb2Nlc3Nfa2V5czogMTAwMDAwMCwgdG90YWxfcHJvY2Vzc19rZXlzX3NpemU6IDM3MDM0MzczMywgdG90YWxfa2V5czogMTAyNjM3NSwgcm9ja3NkYjoge2RlbGV0ZV9za2lwcGVkX2NvdW50OiAxNjY1Mywga2V5X3NraXBwZWRfY291bnQ6IDIwNjkzNTAsIGJsb2NrOiB7Y2FjaGVfaGl0X2NvdW50OiA3MTQ2LCByZWFkX2NvdW50OiAwLCByZWFkX2J5dGU6IDAgQnl0ZXN9fX1w////////////AXj///////////8BIVVVVTWsWYJBKQAAAACAhC5BJZskAUABUhRkYXRhOjrLAYBaF3RpbWU6NzI0LjhtcywgbG9vcHM6OTg1YucBY29wX3Qhpigge251bTogMTQ2LCWsECA2My42AS4gbWluOiAxLjEyAQ0cYXZnOiAyMi4FDSBwOTU6IDQ2LjcBGkxtYXhfcHJvY19rZXlzOiA5MTg0LAEiRhUACHRvdAUVHDogMi45N3MsBREYd2FpdDogMgVLDHJwY18ZjQEOBcAwIDMuMjRzLCBjb3ByXzlVsHJhdGlvOiAwLjAwLCBkaXN0c3FsX2NvbmN1cnJlbmN5OiAxNX1wu/8aeP///wkCIAEhaqF/AXpulzrdAigBQAFSFnRlc3Quc0HYDC5zX29B1AxfY250LUQMOTQ1LkXFLUQYNzhw0IzvCwFQFBmVJZhBKTItAwFQABEyUAAMaV9pZBVLADQpYRVLCGINQx23RDFw6OMBeP///////////wEYAQ==") assert.Nil(t, err) assert.True(t, strings.Contains(vp, "187.85333")) } func TestIndexJoinAndLookUPDuration(t *testing.T) { vp, err := GenerateBinaryPlanJSON("sB/YCqsfCgtTdHJlYW1BZ2dfORKrHgoMSW5kZXhKb2luXzM1EuANCgxTZWxlY3Rpb25fMzASkQwKDgUiNExvb2tVcF8yORK+BQoRBRPwUlJhbmdlU2Nhbl8yNxoBASF6g4vNd8oQQSlOr2RnSmOmQDDZATgCQAJKRQpDCgR0ZXN0EgpvcmRlcl9saW5lGi8KB1BSSU1BUlkSB29sX3dfaWQSAQkEZF8NCQBvAQkwCW9sX251bWJlclIrcgFxKDpbNiA4IDMwMTksCQkoMzkpLCBrZWVwIG8BXfBYOmZhbHNlWhR0aW1lOjEuOTRtcywgbG9vcHM6M2LJAWNvcF90YXNrOiB7bnVtOiAyLCBtYXg6IDk0OC44wrVzLCBtaW46IDg1MC43wrVzLCBhdmc6IDg5OS4JDwhwOTUyLQBEYXhfcHJvY19rZXlzOiAxMjEsASNCFAAIcnBjAccFcAEMBZ4QIDEuNzUBn/BMY29wcl9jYWNoZV9oaXRfcmF0aW86IDAuMDAsIGRpc3RzcWxfY29uY3VycmVuY3k6IDE1fWqgAnRpa3ZfdGFzazp7cHJvYyBtYXg6MHMJwgAwEbsBCQhwODAFEQG9ARAgaXRlcnM6NSwgIQoYczoyfSwgcyHINGRldGFpbDoge3RvdGFsBcUIZXNzDdwQMjE3LCBGGQAQX3NpemUBkgQ0MBUgKQ2YMjE5LCByb2Nrc2RiOiB7ZGVsZXRlX3NraXBwZWRfY291bnQ6IDI4Ib0AeUIXACw0NSwgYmxvY2s6IHs5Gg01FDgsIHJlYRVEADAND0RieXRlOiAwIEJ5dGVzfX19cP8RAQQBeBEKQP8BEu0EChFUYWJsZVJvd0lEScEQOBoBAiFiwQIMFAoSCkLBAgRSEFp1AgwyLjIzIdZJdQgyYsNidQIMMS4xMQEqRXMUOTUyLjnCQYJFcwgxLjAFRiG0GSnybwIMMi4wMgFvAGPObwIEoQL+bwL+bwIubwIQMjE1NTJRj01vADNBF3pvAgQxNFZvAgQzMWpvAgQ0Mk1hTX+2cAIgIdUN7z4KnvZAMhgFCAFAAY2kDDQuNTYhlkkvEDRijwFpoZNp2XGLkTAEOTgBKyhmZXRjaF9oYW5kbGF7BC45BUEQYnVpbGRBLAA4ickcd2FpdDogMTlFVgB9Ye0IYmxlTlcACDIuMwVBIG51bTogMSwgYy5rBBA1fXDwly5AAwgaAQE+ygAoUoQBZXEodGVzdC6lhKXhCC5vbKXMFCwgOCksIFYgACx3X2lkLCA2KSwgZ2VOQADBAwAspe0QKSwgbHRuIwAIMzkpNVEAN2VWKVEMNXDgRC7+AwTwDTbSBgwzNBLqRtIGBDMxAecAAAUBDPA/MNjJyQgqCiiNCCAFc3RvY2saGQrVxAQGcwXOAQgUaV9pZFJtya4wIGRlY2lkZWQgYnkgWzEFBT0ELnMFKQQsIEo4AQFCACk1NBEvKS4IOCldQcRW8AYEMy4lygn7CDlizkJ7BAQ0LMnwFDYuMDltc8ksDCAxLjUFDYV5CDMuMyU/AHDB6hUnNnkEBDk2QucGARMIdG90xTUIOiA1AVzB6UVUBDQsBQxNxQQzLgVg0oYEAKlShgSF1qFFwd0FxQgxLjIFg8H6BR4BzgUJyfwANmEPEHNrczo0mvwGATFCFQfR/AwxNDQ4No0EBDc0ivwGWo0EADWBcGL8Bgg0MzXujgQMEp8GCi7jCQwzErEBQg8HBDMyQvsCCA8KDTL7Akb+BgRqU04hBsXEMpsBEDgzMy4zxeghnUULIZ0FKSmdADkxnQA2WsMHCBoBAhmhGAw4AkACUh2RHnEtLHF1YW50aXR5LCAxNpEaCDkuOAVnaR8INmLSQh8DADZtHwgzLjZFLgG6FCA1NDUuMgWyTVypsQG2GSk2IQMIMTI3QiIDBRReIwMANi4jAwA5GkII0iMDAKhSIwMF+AHOdSOyiAGWIQpuJQMQNzk5ODE2JQOSIQoAMIHtQiAKADBqrwcAMxJ4CuIgChAaAQIhIgEBCPJQQC5mAvmyCDI0LiVPUUdusgcIMy43oQs6sgetp+2yCDYuOBb5CemyFDQzLjXCtXKyBwQxMAmCpV4AM0ayBwT0UN3zTCGQ1iKAmZ4TQSn4TwpLQircQDAMAc44UpsBaW5uZXIgam9pbiwgBQwEOkkOfAga7Q00MzQsIG91dGVyIGtleTpapwYNORkjcYEAaeXcEGVxdWFsDlIIBGQ60f42IwgJJwVdLjoAcbMIMTcuxZspbAgyYmoJpxaiDAA6qeUusAgANYk8ADopFSRzdHJ1Y3Q6MjAxEgUJACwpiCHBJZYpgAAxCR0wfSwgcHJvYmU6MTEyLg5CCQxwpMgMLkcBHHheyDji5hhBWRUAASVHGDJmdW5jczoSHQoAKA6vDQxpbmN0StIAKC0+Q29sdW1uIzMwGqoIOt0APHCUBXj///////////8BGAE=") assert.Nil(t, err) assert.True(t, strings.Contains(vp, "8.1ms")) assert.True(t, strings.Contains(vp, "2.23")) } func TestApplyAndLookUPDuration(t *testing.T) { vp, err := GenerateBinaryPlanJSON("/haICvkWCg1Qcm9qZWN0aW9uXzEwEoYWCghBcHBseV8xMhL9BQoyHwBgMxLTBAoOVGFibGVSZWFkZXJfMTUS3AIKEAUTUEZ1bGxTY2FuXzE0IQAAAAAgZSFBKQEJ8FsAiMNAMAM4AkACSg0KCwoFdGVzdDISAnR0Uh5rZWVwIG9yZGVyOmZhbHNlLCBzdGF0czpwc2V1ZG9q6gF0aWt2X3Rhc2s6e3RpbWU6MW1zLCBsb29wczoxfSwgcwFwfGRldGFpbDoge3RvdGFsX3Byb2Nlc3Nfa2V5czogMywgRhcAIF9zaXplOiAxMhUeCS2gNCwgcm9ja3NkYjoge2RlbGV0ZV9za2lwcGVkX2NvdW50OiAwLCBrZXk+FgBMMywgYmxvY2s6IHtjYWNoZV9oaXQdMghyZWEuQQAFD0RieXRlOiAwIEJ5dGVzfX19cP8RAQQBeBEKDP8BIVUBAQjY50AuSgEkAUABUhVkYXRhOj55AQRaFCkmCC41OTkpHDJijgFjb3BfJUdIIHtudW06IDEsIG1heDogMS41NAEqITUtBSkUJUcIOiAxAR0McnBjXxE2AQwFZAE7AR0QY29wcl8Z9aByYXRpbzogMC4wMCwgZGlzdHNxbF9jb25jdXJyZW5jeTogMTV9cLcCeBnkKBoBASGqqqqqij/zQucAAEhFLRgudHQuYSwgFQwcYiwgY2FzdCgVEbBhLCBkZWNpbWFsKDIwLDApIEJJTkFSWSktPkNvbHVtbiM4WhR0aW1lOjEuNjcBtgRsb0FDDDJiDUMdmwwxcIwXPXyoEuUOCgpIYXNoQWdnXzI0EoIOCg5JbmRleExvb2tVcF8yNRKTBgoMU2VsZWlAGDIzEsUBChAFJHUOBDIxSg4DAAZlDggXChVxDjADdHQxGgcKAmlhEgFhghgDBEp0eRchuiXMGDBzLCBtaW4FCBBhdmc6IAERCHA4MAURCHA5NQUIFGl0ZXJzOiHjGGFza3M6M31WdwIIGgEBSXEIQL9AabsIUhtnOXAAMSFxOX0AKU13CDIuNl13CDdixUJ3AgAzTXcYOTU2LjTCtQ2yFCA2MjkuNgUPBbkIODA4DQ8Bty4tAAhheF8B8E2qBDIsASE6EgAAclGtBDUsBQxJrQgzLjdF6M6uAgxqlwJ0/nABNXCOtAQANmq0BAQyNxUeibQEOSzatAQANv60BF60BAgSiwZ5OBgxNxK9AQoRhakQUm93SUTFIwQyMjoVA1VoDEoOCgwuFQP+DAP+DANhDAACaQwQAPA/MAJhuShSIWZ1bmNzOnN1bZWDCDEuYpluBDExdRIENTaZbwg0YsFiEgMIODYyZQFhwhQgNjk3LjMFD2UQCDc5NW0uDHA5NToZK+4OA2Wp/g4D/g4Dkg4DADNqDgNCwgdh9dLCBwAy/g4DXg4DICH/lVB1tWEZQS5FAggBQAFNIgw1Ljg4WSIQNWKPAWnBY/GtFrwIicQMMi4wMgErMGZldGNoX2hhbmRsZToBFuWlEGJ1aWxkAQ8ANqlKDHdhaXQBDzA5OMK1c30sIHRhYmxlVlcAADdFpOXiCDIsIDatBwx9cLxL/RQgGgECIdy6VnbBMtEAAAMB0QRSHnkW7XYEMTF5FAA3FfEAOQWw6YIsNnC0ESFvNAp7AvzuLSIIiMNABU9QKXNlbWkgam9pbiwgZXF1YWw6W2VxEVoIOCwgDWQQNyldWhMlIQQ3Lj1KCDFiLy7bBwhPRkYONAgUY2hlOk9ODQoMSGl0UhKuCBwwLjAwMCVwGx3oFCFvNMohEUKUAAAW2fUujwgAWhZdCAw3Ljg3IaEJ2wAxcl0IBBgB") assert.Nil(t, err) assert.True(t, strings.Contains(vp, "5.91ms")) assert.True(t, strings.Contains(vp, "2.56ms")) } func TestShuffleDuration(t *testing.T) { vp, err := GenerateBinaryPlanJSON("6xhYCuYYCg1JbmRleE1lcmdlXzE2ErQEChAFEjxSYW5nZVNjYW5fOBoBASEAAQEM0IFAKQEI8J8AACRAOAJAAkoXChUKBXRlc3QyEgN0dDMaBwoCaWESAWFSK3JhbmdlOlsxLDFdLCBrZWVwIG9yZGVyOmZhbHNlLCBzdGF0czpwc2V1ZG9aFHRpbWU6NS4yOG1zLCBsb29wczoxYqkBY29wX3Rhc2s6IHtudW06IDEsIG1heDogMS4wM21zLCBwcm9jX2tleXM6IDAsIHRvdF9wcm9jOiAxAUcMcnBjXwU2BDIsBQwFZBAgMi4zOQEe8Ftjb3ByX2NhY2hlX2hpdF9yYXRpbzogMC4wMCwgZGlzdHNxbF9jb25jdXJyZW5jeTogMTV9LCBiYWNrb2Zme3JlZ2lvbk1pc3M6IDJtc31q6AF0aWt2X3Rhc2s6ewVqADEBZgRsbwXLDH0sIHMhSThkZXRhaWw6IHt0b3RhbF8Bvwxlc3NfLsIAOhcAJF9zaXplOiAwLCAJMwkrgDEsIHJvY2tzZGI6IHtkZWxldGVfc2tpcHBlZF9jb3VudAUyCGtleUoWAAxibG9jIVEZ/BkyCHJlYS5BAAUPCGJ5dAGBKCBCeXRlc319fXD/EQEEAXgRCgz/ARKjRjcCADm2NwIMYhIBYlU3CDIsMqY3AgQ3Nj1sBGKZajcCADIBKSF3LUkAMEkbTicCBDg4ASwAY/4nAjYnAgDnQicCCDBzLP4mAv4mAv4mAqomAlCoBQoMU2VsZWN0aW9uXzExEqYBChE6bgQEMTCFbCyAMQdBKauqqqqqCqpmbAQUYxIBY1IuiWwYKDMsK2luZoI4AgxqHHRpNqwDLoUBVuACKBoBASlWVVVVVdWkBY0kUhxsdChwbHVzKIX9QC50dDMuYywgMSksIDEwKVoUhQMIOC4xMpoCAKlimgIIMy43hZY2mwIAdI7RBAg1LjP+qgJOqgIA6EKqAv7RBP7RBP7RBLrRBASlBEaaAgAyttIEDGQSAWSV0gg0LDSCmgJNOAg1LjkyOAIAmmI4AgwxLjE1gac2OAJe+gYAOAUs/tME/tME/tME/tME/tMEOtMEAKRKKAIAM7YoAgxlEgFlVSgINSw1higCABOFYAQ1LmX6GmUIdicCSjAJYicCADdBU/4nAv4nAv4nAv4nAv4nAjonAgTDATL6Bjg1EmYKEVRhYmxlUm93SUQSZwskMTQpYQKtfOkPpcVjDEoOCgwuXAsIUh5rckYLXrkGIAIptM7wlofZoAVWCFIlZ0K5BghhLCDZxhRiKSwgMilWTQAgIaHrVV42sONAGVMIAUABjbYENzAWXwlNjzxwggR4////////////ARgB") assert.Nil(t, err) assert.True(t, strings.Contains(vp, "8.16ms")) } ================================================ FILE: pkg/apiserver/utils/error.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "github.com/joomcode/errorx" ) var ErrNS = errorx.NewNamespace("error.api") var ErrExpNotEnabled = ErrNS.NewType("experimental_feature_not_enabled") ================================================ FILE: pkg/apiserver/utils/export.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "encoding/base64" "encoding/csv" "fmt" "io" "os" "path/filepath" "reflect" "strings" "time" "github.com/Xeoncross/go-aesctr-with-hmac" "github.com/gin-gonic/gin" "github.com/gtank/cryptopasta" "github.com/oleiade/reflections" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/rest" ) // TODO: Better to be a streaming interface. func GenerateCSVFromRaw(rawData []interface{}, fields []string, timeFields []string) (data [][]string) { timeFieldsMap := make(map[string]struct{}) for _, f := range timeFields { timeFieldsMap[f] = struct{}{} } fieldsMap := make(map[string]string) t := reflect.TypeOf(rawData[0]) fieldsNum := t.NumField() allFields := make([]string, fieldsNum) for i := range fieldsNum { field := t.Field(i) allFields[i] = strings.ToLower(field.Tag.Get("json")) fieldsMap[allFields[i]] = field.Name } if len(fields) == 1 && fields[0] == "*" { fields = allFields } data = [][]string{fields} //nolint:prealloc timeLayout := "01-02 15:04:05" for _, overview := range rawData { row := make([]string, 0, len(fields)) for _, field := range fields { fieldName := fieldsMap[field] s, _ := reflections.GetField(overview, fieldName) var val string switch t := s.(type) { case int: if _, ok := timeFieldsMap[field]; ok { val = time.Unix(int64(t), 0).Format(timeLayout) } else { val = fmt.Sprintf("%d", t) } case uint: val = fmt.Sprintf("%d", t) case float64: val = fmt.Sprintf("%f", t) default: val = fmt.Sprintf("%s", t) } row = append(row, val) } data = append(data, row) } return } // TODO: Better to be a streaming interface. func ExportCSV(data [][]string, filename, tokenNamespace string) (token string, err error) { csvFile, err := os.CreateTemp("", filename) if err != nil { return } defer csvFile.Close() // #nosec // generate encryption key secretKey := *cryptopasta.NewEncryptionKey() pr, pw := io.Pipe() go func() { csvwriter := csv.NewWriter(pw) _ = csvwriter.WriteAll(data) _ = pw.Close() }() err = aesctr.Encrypt(pr, csvFile, secretKey[0:16], secretKey[16:]) if err != nil { return } // generate token by filepath and secretKey secretKeyStr := base64.StdEncoding.EncodeToString(secretKey[:]) token, err = NewJWTString(tokenNamespace, secretKeyStr+" "+csvFile.Name()) return } // FIXME: Remove or refine this function, as it is not general. func DownloadByToken(token, tokenNamespace string, c *gin.Context) { tokenPlain, err := ParseJWTString(tokenNamespace, token) if err != nil { rest.Error(c, rest.ErrBadRequest.WrapWithNoMessage(err)) return } arr := strings.Fields(tokenPlain) if len(arr) != 2 { rest.Error(c, rest.ErrBadRequest.New("invalid token")) return } secretKey, err := base64.StdEncoding.DecodeString(arr[0]) if err != nil { rest.Error(c, rest.ErrBadRequest.WrapWithNoMessage(err)) return } filePath := arr[1] fileInfo, err := os.Stat(filePath) if err != nil { rest.Error(c, err) return } f, err := os.Open(filepath.Clean(filePath)) if err != nil { rest.Error(c, err) return } c.Writer.Header().Set("Content-type", "text/csv") c.Writer.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileInfo.Name())) err = aesctr.Decrypt(f, c.Writer, secretKey[0:16], secretKey[16:]) if err != nil { log.Error("decrypt csv failed", zap.Error(err)) } // delete it anyway _ = f.Close() _ = os.Remove(filePath) } ================================================ FILE: pkg/apiserver/utils/gorm.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import "gorm.io/gorm/schema" func GetGormColumnName(gormStr string) string { gormStrMap := schema.ParseTagSetting(gormStr, ";") // The key will be converted to uppercase in: // https://github.com/go-gorm/gorm/blob/master/schema/utils.go#L33 return gormStrMap["COLUMN"] } ================================================ FILE: pkg/apiserver/utils/gorm_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "testing" "github.com/pingcap/check" ) func TestT(t *testing.T) { check.CustomVerboseFlag = true check.TestingT(t) } var _ = check.Suite(&testGormSuite{}) type testGormSuite struct{} func (t *testGormSuite) Test_GetGormColumnName(c *check.C) { c.Assert(GetGormColumnName(`column:db`), check.Equals, `db`) c.Assert(GetGormColumnName(`primaryKey;index`), check.Equals, ``) c.Assert(GetGormColumnName(`column:db;primaryKey;index`), check.Equals, `db`) } ================================================ FILE: pkg/apiserver/utils/jwt.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "fmt" "time" jwt "github.com/golang-jwt/jwt/v4" "github.com/gtank/cryptopasta" ) var hmacSampleSecret = cryptopasta.NewEncryptionKey() // Claims is a struct that will be encoded to a JWT. type Claims struct { Data string `json:"data"` jwt.StandardClaims } func newClaims(issuer string, data string, expireIn time.Duration) *Claims { return &Claims{ Data: data, StandardClaims: jwt.StandardClaims{ //nolint:staticcheck // StandardClaims is deprecated, but we use it here temporarily ExpiresAt: time.Now().Add(expireIn).Unix(), Issuer: issuer, }, } } // NewJWTString create a JWT string by given data, expire in 24 hours. func NewJWTString(issuer string, data string) (string, error) { return NewJWTStringWithExpire(issuer, data, 24*time.Hour) } func NewJWTStringWithExpire(issuer string, data string, expireIn time.Duration) (string, error) { claims := newClaims(issuer, data, expireIn) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(hmacSampleSecret[:]) if err != nil { return "", err } return tokenString, nil } // ParseJWTString parse the JWT string and return the raw data. func ParseJWTString(requiredIssuer string, tokenStr string) (string, error) { claims := &Claims{} token, err := jwt.ParseWithClaims(tokenStr, claims, func(_ *jwt.Token) (interface{}, error) { return hmacSampleSecret[:], nil }) if err != nil { return "", err } if !token.Valid { return "", fmt.Errorf("token is invalid or expired") } if claims.Issuer != requiredIssuer { return "", fmt.Errorf("token is invalid (invalid issuer)") } return claims.Data, nil } ================================================ FILE: pkg/apiserver/utils/mw_experimental.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "net/http" "github.com/gin-gonic/gin" "github.com/pingcap/tidb-dashboard/util/rest" ) func MWForbidByExperimentalFlag(enableExp bool) gin.HandlerFunc { return func(c *gin.Context) { if !enableExp { c.Status(http.StatusForbidden) rest.Error(c, ErrExpNotEnabled.NewWithNoMessage()) c.Abort() return } c.Next() } } ================================================ FILE: pkg/apiserver/utils/ngm.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "context" "fmt" "net" "net/http" "net/http/httputil" "net/url" "sync/atomic" "time" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/fx" "golang.org/x/sync/singleflight" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/utils/topology" "github.com/pingcap/tidb-dashboard/util/rest" ) var ( NgmErrNS = errorx.NewNamespace("ngm") ErrNgmNotStart = NgmErrNS.NewType("ngm_not_started") ) type NgmState string const ( NgmStateNotSupported NgmState = "not_supported" NgmStateNotStarted NgmState = "not_started" NgmStateStarted NgmState = "started" ) const ( ngmCacheTTL = time.Second * 5 ) type ngmAddrCacheEntity struct { address string err error cacheAt time.Time } type NgmProxy struct { lifecycleCtx context.Context etcdClient *clientv3.Client ngmReqGroup singleflight.Group ngmAddrCache atomic.Value timeout time.Duration } func NewNgmProxy(lc fx.Lifecycle, etcdClient *clientv3.Client, config *config.Config) (*NgmProxy, error) { s := &NgmProxy{ etcdClient: etcdClient, timeout: time.Duration(config.NgmTimeout) * time.Second, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { s.lifecycleCtx = ctx return nil }, }) return s, nil } func (n *NgmProxy) Route(targetPath string) gin.HandlerFunc { return func(c *gin.Context) { ngmAddr, err := n.getNgmAddrFromCache() if err != nil { rest.Error(c, err) return } c.Request.URL.Path = targetPath ngmURL, _ := url.Parse(ngmAddr) proxy := httputil.NewSingleHostReverseProxy(ngmURL) proxy.Transport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: defaultTransportDialContext(&net.Dialer{ Timeout: n.timeout, KeepAlive: n.timeout, }), ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } proxy.ServeHTTP(c.Writer, c.Request) } } func (n *NgmProxy) getNgmAddrFromCache() (string, error) { fn := func() (string, error) { // Check whether cache is valid, and use the cache if possible. if v := n.ngmAddrCache.Load(); v != nil { entity := v.(*ngmAddrCacheEntity) if entity.cacheAt.Add(ngmCacheTTL).After(time.Now()) { return entity.address, entity.err } } addr, err := n.resolveNgmAddress() n.ngmAddrCache.Store(&ngmAddrCacheEntity{ address: addr, err: err, cacheAt: time.Now(), }) return addr, err } resolveResult, err, _ := n.ngmReqGroup.Do("any_key", func() (interface{}, error) { return fn() }) return resolveResult.(string), err } func (n *NgmProxy) resolveNgmAddress() (string, error) { addr, err := topology.FetchNgMonitoringTopology(n.lifecycleCtx, n.etcdClient) if err == nil && addr != "" { return fmt.Sprintf("http://%s", addr), nil } return "", ErrNgmNotStart.Wrap(err, "NgMonitoring component is not started") } func defaultTransportDialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) { return dialer.DialContext } ================================================ FILE: pkg/apiserver/utils/subset.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "strings" "github.com/samber/lo" ) func IsSubsetICaseInsensitive(a []string, b []string) bool { lowercaseA := lo.Map(a, func(x string, _ int) string { return strings.ToLower(x) }) lowercaseB := lo.Map(b, func(x string, _ int) string { return strings.ToLower(x) }) return len(lo.Intersect(lowercaseA, lowercaseB)) == len(lowercaseB) } ================================================ FILE: pkg/apiserver/utils/tidb_conn.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "gorm.io/gorm" "github.com/pingcap/tidb-dashboard/pkg/tidb" "github.com/pingcap/tidb-dashboard/util/rest" ) const ( // The key that attached the TiDB connection in the gin Context. tiDBConnectionKey = "tidb" ) // MWConnectTiDB creates a middleware that attaches TiDB connection to the context, according to the identity // information attached in the context. If a connection cannot be established, subsequent handlers will be skipped // and errors will be generated. // // This middleware must be placed after the `MWAuthRequired()` middleware, otherwise it will panic. func MWConnectTiDB(tidbClient *tidb.Client) gin.HandlerFunc { return func(c *gin.Context) { sessionUser := GetSession(c) if sessionUser == nil { panic("invalid sessionUser") } if !sessionUser.HasTiDBAuth { // Only TiDBAuth is able to access. Raise error in this case. // The error is privilege error instead of authorization error so that user will not be redirected. rest.Error(c, rest.ErrForbidden.NewWithNoMessage()) c.Abort() return } db, err := tidbClient.OpenSQLConn(sessionUser.TiDBUsername, sessionUser.TiDBPassword) if err != nil { if errorx.IsOfType(err, tidb.ErrTiDBAuthFailed) { // If TiDB conn is ok when login but fail this time, it means TiDB credential has been changed since // login. In this case, we return unauthorized error, so that the front-end can let user to login again. rest.Error(c, rest.ErrUnauthenticated.NewWithNoMessage()) } else { // For other kind of connection errors, for example, PD goes away, return these errors directly. // In front-end we will simply display these errors but not ask user to login again. rest.Error(c, err) } c.Abort() return } defer func() { // We allow tiDBConnectionKey to be cleared by `TakeTiDBConnection`. dbInContext := c.MustGet(tiDBConnectionKey) if dbInContext != nil { dbInContext2 := dbInContext.(*gorm.DB) if dbInContext2 != nil { _ = CloseTiDBConnection(dbInContext2) } } }() c.Set(tiDBConnectionKey, db) c.Next() } } // TakeTiDBConnection takes out the TiDB connection stored in the gin context by `MWConnectTiDB` middleware. // Subsequent handlers in this context cannot access the TiDB connection any more. // // The TiDB connection will no longer be closed automatically after all handlers are finished. You must manually // close the taken out connection. func TakeTiDBConnection(c *gin.Context) *gorm.DB { db := GetTiDBConnection(c) c.Set(tiDBConnectionKey, nil) return db } func CloseTiDBConnection(db *gorm.DB) error { sqlDB, err := db.DB() if err != nil { return err } return sqlDB.Close() } // GetTiDBConnection gets the TiDB connection stored in the gin context by `MWConnectTiDB` middleware. // // The connection will be closed automatically after all handlers are finished. Thus you must not use it outside // the request lifetime. If you want to extend the lifetime, use `TakeTiDBConnection`. func GetTiDBConnection(c *gin.Context) *gorm.DB { db := c.MustGet(tiDBConnectionKey).(*gorm.DB) return db } ================================================ FILE: pkg/apiserver/visualplan/module.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package visualplan import "go.uber.org/fx" var Module = fx.Options( fx.Provide(newService), fx.Invoke(registerRouter), ) ================================================ FILE: pkg/apiserver/visualplan/service.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package visualplan import ( "net/http" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "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/util/featureflag" "github.com/pingcap/tidb-dashboard/util/rest" ) var ErrNS = errorx.NewNamespace("error.api.visualplan") type ServiceParams struct { fx.In } type Service struct { FeatureVisualPlan *featureflag.FeatureFlag params ServiceParams } func newService(p ServiceParams, ff *featureflag.Registry) *Service { return &Service{params: p, FeatureVisualPlan: ff.Register("visualplan", ">= 6.2.0")} } func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/visualplan") endpoint.Use( auth.MWAuthRequired(), s.FeatureVisualPlan.VersionGuard(), ) { endpoint.POST("/generate", s.GenerateVisualPlan) } } type GenerateVisualPlanRequest struct { BinaryPlan string `json:"binary_plan"` } // // @Summary Generate VisualPlan // // @Router /visualplan/generate [post] // // @Security JwtAuth // // @Param request body GenerateVisualPlanRequest true "Request body" // // @Success 200 {object} string // // @Failure 401 {object} rest.ErrorResponse // // @Failure 500 {object} rest.ErrorResponse. func (s *Service) GenerateVisualPlan(c *gin.Context) { var bp GenerateVisualPlanRequest if err := c.ShouldBindJSON(&bp); err != nil { rest.Error(c, err) return } vp, err := utils.GenerateBinaryPlanJSON(bp.BinaryPlan) if err != nil { rest.Error(c, rest.ErrBadRequest.New("generate visual plan failed: %v", err)) return } c.Data(http.StatusOK, gin.MIMEJSON, []byte(vp)) } ================================================ FILE: pkg/config/config.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package config import ( "crypto/tls" "net/url" "strings" "go.etcd.io/etcd/client/pkg/v3/transport" "github.com/pingcap/tidb-dashboard/pkg/utils/version" ) const ( defaultPublicPathPrefix = "/dashboard" UIPathPrefix = "/dashboard/" APIPathPrefix = "/dashboard/api/" SwaggerPathPrefix = "/dashboard/api/swagger/" ) type Config struct { DataDir string TempDir string PDEndPoint string PublicPathPrefix string ClusterTLSConfig *tls.Config // TLS config for mTLS authentication between TiDB components. ClusterTLSInfo *transport.TLSInfo // TLS info for mTLS authentication between TiDB components. TiDBTLSConfig *tls.Config // TLS config for mTLS authentication between TiDB and MySQL client. EnableTelemetry bool EnableExperimental bool EnableKeyVisualizer bool DisableCustomPromAddr bool FeatureVersion string // assign the target TiDB version when running TiDB Dashboard as standalone mode NgmTimeout int // in seconds } func Default() *Config { return &Config{ DataDir: "/tmp/dashboard-data", TempDir: "", PDEndPoint: "http://127.0.0.1:2379", PublicPathPrefix: defaultPublicPathPrefix, ClusterTLSConfig: nil, ClusterTLSInfo: nil, TiDBTLSConfig: nil, EnableTelemetry: false, EnableExperimental: false, EnableKeyVisualizer: true, DisableCustomPromAddr: false, FeatureVersion: version.PDVersion, NgmTimeout: 30, // s } } func (c *Config) GetClusterHTTPScheme() string { if c.ClusterTLSConfig != nil { return "https" } return "http" } func (c *Config) NormalizePDEndPoint() error { if !strings.HasPrefix(c.PDEndPoint, "http://") && !strings.HasPrefix(c.PDEndPoint, "https://") { c.PDEndPoint = "http://" + c.PDEndPoint } pdEndPoint, err := url.Parse(c.PDEndPoint) if err != nil { return err } pdEndPoint.Scheme = c.GetClusterHTTPScheme() c.PDEndPoint = pdEndPoint.String() return nil } func (c *Config) NormalizePublicPathPrefix() { if c.PublicPathPrefix == "" { c.PublicPathPrefix = defaultPublicPathPrefix } c.PublicPathPrefix = strings.TrimRight(c.PublicPathPrefix, "/") } ================================================ FILE: pkg/config/dynamic_config.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package config import ( "slices" "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" ) const ( KeyVisualDBPolicy = "db" KeyVisualKVPolicy = "kv" DefaultKeyVisualPolicy = KeyVisualDBPolicy DefaultProfilingAutoCollectionDurationSecs = 30 MaxProfilingAutoCollectionDurationSecs = 120 DefaultProfilingAutoCollectionIntervalSecs = 3600 ) var ( KeyVisualPolicies = []string{KeyVisualDBPolicy, KeyVisualKVPolicy} ErrVerificationFailed = ErrorNS.NewType("verification failed") ) type KeyVisualConfig struct { AutoCollectionDisabled bool `json:"auto_collection_disabled"` Policy string `json:"policy"` PolicyKVSeparator string `json:"policy_kv_separator"` } func (c *KeyVisualConfig) validatePolicy() error { if slices.Contains(KeyVisualPolicies, c.Policy) { return nil } return ErrVerificationFailed.New("policy must be in %v", KeyVisualPolicies) } type ProfilingConfig struct { AutoCollectionTargets []model.RequestTargetNode `json:"auto_collection_targets"` AutoCollectionDurationSecs uint `json:"auto_collection_duration_secs"` AutoCollectionIntervalSecs uint `json:"auto_collection_interval_secs"` } type SSOCoreConfig struct { Enabled bool `json:"enabled"` ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` DiscoveryURL string `json:"discovery_url"` Scopes string `json:"scopes"` IsReadOnly bool `json:"is_read_only"` } type SSOConfig struct { CoreConfig SSOCoreConfig `json:"core_config"` AuthURL string `json:"auth_url"` TokenURL string `json:"token_url"` UserInfoURL string `json:"user_info_url"` SignOutURL string `json:"sign_out_url"` } type DynamicConfig struct { KeyVisual KeyVisualConfig `json:"keyvisual"` Profiling ProfilingConfig `json:"profiling"` SSO SSOConfig `json:"sso"` } func (c *DynamicConfig) Clone() *DynamicConfig { newCfg := *c newCfg.Profiling.AutoCollectionTargets = make([]model.RequestTargetNode, len(c.Profiling.AutoCollectionTargets)) copy(newCfg.Profiling.AutoCollectionTargets, c.Profiling.AutoCollectionTargets) return &newCfg } func (c *DynamicConfig) Validate() error { if !c.KeyVisual.AutoCollectionDisabled { if err := c.KeyVisual.validatePolicy(); err != nil { return err } } if len(c.Profiling.AutoCollectionTargets) > 0 { if c.Profiling.AutoCollectionDurationSecs == 0 { return ErrVerificationFailed.New("auto_collection_duration_secs cannot be 0") } if c.Profiling.AutoCollectionDurationSecs > MaxProfilingAutoCollectionDurationSecs { return ErrVerificationFailed.New("auto_collection_duration_secs cannot be greater than %d", MaxProfilingAutoCollectionDurationSecs) } if c.Profiling.AutoCollectionIntervalSecs == 0 { return ErrVerificationFailed.New("auto_collection_interval_secs cannot be 0") } } else { if c.Profiling.AutoCollectionDurationSecs != 0 { return ErrVerificationFailed.New("auto_collection_duration_secs must be 0") } if c.Profiling.AutoCollectionIntervalSecs != 0 { return ErrVerificationFailed.New("auto_collection_interval_secs must be 0") } } return nil } // Adjust is used to fill the default config for the existing config of the old version. func (c *DynamicConfig) Adjust() { if !c.KeyVisual.AutoCollectionDisabled { if err := c.KeyVisual.validatePolicy(); err != nil { c.KeyVisual.Policy = DefaultKeyVisualPolicy } } if len(c.Profiling.AutoCollectionTargets) > 0 { if c.Profiling.AutoCollectionDurationSecs == 0 { c.Profiling.AutoCollectionDurationSecs = DefaultProfilingAutoCollectionDurationSecs } if c.Profiling.AutoCollectionDurationSecs > MaxProfilingAutoCollectionDurationSecs { c.Profiling.AutoCollectionDurationSecs = MaxProfilingAutoCollectionDurationSecs } if c.Profiling.AutoCollectionIntervalSecs == 0 { c.Profiling.AutoCollectionIntervalSecs = DefaultProfilingAutoCollectionIntervalSecs } } else { c.Profiling.AutoCollectionDurationSecs = 0 c.Profiling.AutoCollectionIntervalSecs = 0 } } ================================================ FILE: pkg/config/dynamic_config_manager.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package config import ( "context" "encoding/json" "sync" "time" "github.com/cenkalti/backoff/v4" "github.com/joomcode/errorx" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/fx" "go.uber.org/zap" ) const ( DynamicConfigPath = "/dashboard/dynamic_config" Timeout = time.Second MaxCheckInterval = 30 * time.Second MaxElapsedTime = 0 // never stop if MaxElapsedTime == 0 ) var ( ErrorNS = errorx.NewNamespace("error.dynamic_config") ErrUnableToLoad = ErrorNS.NewType("unable_to_load") ErrNotReady = ErrorNS.NewType("not_ready") ) type DynamicConfigOption func(dc *DynamicConfig) type DynamicConfigManager struct { mu sync.RWMutex lifecycleCtx context.Context config *Config etcdClient *clientv3.Client dynamicConfig *DynamicConfig pushChannels []chan *DynamicConfig } func NewDynamicConfigManager(lc fx.Lifecycle, config *Config, etcdClient *clientv3.Client) *DynamicConfigManager { m := &DynamicConfigManager{ config: config, etcdClient: etcdClient, } lc.Append(fx.Hook{ OnStart: m.Start, OnStop: m.Stop, }) return m } func (m *DynamicConfigManager) Start(ctx context.Context) error { m.lifecycleCtx = ctx go func() { var dc *DynamicConfig ebo := backoff.NewExponentialBackOff() ebo.MaxInterval = MaxCheckInterval ebo.MaxElapsedTime = MaxElapsedTime bo := backoff.WithContext(ebo, ctx) if err := backoff.Retry(func() error { var err error dc, err = m.load() return err }, bo); err != nil { log.Error("Failed to start DynamicConfigManager", zap.Error(err)) return } if dc == nil { dc = &DynamicConfig{} } dc.Adjust() dc.KeyVisual.AutoCollectionDisabled = !m.config.EnableKeyVisualizer if err := backoff.Retry(func() error { return m.Set(dc) }, bo); err != nil { log.Error("Failed to start DynamicConfigManager", zap.Error(err)) } }() return nil } func (m *DynamicConfigManager) Stop(_ context.Context) error { m.mu.Lock() defer m.mu.Unlock() for _, ch := range m.pushChannels { close(ch) } return nil } func (m *DynamicConfigManager) NewPushChannel() <-chan *DynamicConfig { m.mu.Lock() defer m.mu.Unlock() ch := make(chan *DynamicConfig, 1000) m.pushChannels = append(m.pushChannels, ch) if m.dynamicConfig != nil { ch <- m.dynamicConfig.Clone() } return ch } func (m *DynamicConfigManager) Get() (*DynamicConfig, error) { m.mu.RLock() defer m.mu.RUnlock() if m.dynamicConfig == nil { return nil, ErrNotReady.NewWithNoMessage() } return m.dynamicConfig.Clone(), nil } func (m *DynamicConfigManager) Set(newDc *DynamicConfig) error { if err := m.store(newDc); err != nil { return err } m.mu.Lock() defer m.mu.Unlock() m.dynamicConfig = newDc for _, ch := range m.pushChannels { ch <- m.dynamicConfig.Clone() } return nil } func (m *DynamicConfigManager) Modify(opts ...DynamicConfigOption) error { newDc, err := m.Get() if err != nil { return err } for _, opt := range opts { opt(newDc) } if err := newDc.Validate(); err != nil { return err } return m.Set(newDc) } func (m *DynamicConfigManager) load() (*DynamicConfig, error) { ctx, cancel := context.WithTimeout(m.lifecycleCtx, Timeout) defer cancel() resp, err := m.etcdClient.Get(ctx, DynamicConfigPath) if err != nil { log.Warn("Failed to load dynamic config from etcd", zap.Error(err)) return nil, ErrUnableToLoad.WrapWithNoMessage(err) } switch len(resp.Kvs) { case 0: log.Warn("Dynamic config does not exist in etcd") return nil, nil case 1: // the log contains the sso client secret, so we should not log it // log.Info("Load dynamic config from etcd", zap.ByteString("json", resp.Kvs[0].Value)) var dc DynamicConfig if err = json.Unmarshal(resp.Kvs[0].Value, &dc); err != nil { return nil, err } return &dc, nil default: log.Error("etcd is unreachable") return nil, backoff.Permanent(ErrUnableToLoad.New("unreachable")) } } func (m *DynamicConfigManager) store(dc *DynamicConfig) error { bs, err := json.Marshal(dc) if err != nil { return err } ctx, cancel := context.WithTimeout(m.lifecycleCtx, Timeout) defer cancel() _, err = m.etcdClient.Put(ctx, DynamicConfigPath, string(bs)) // the log contains the sso client secret, so we should not log it // log.Info("Save dynamic config to etcd", zap.ByteString("json", bs)) return err } ================================================ FILE: pkg/dbstore/dbstore.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package dbstore import ( "context" "os" "path" "github.com/pingcap/log" "go.uber.org/fx" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" "moul.io/zapgorm2" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap/tidb-dashboard/pkg/config" ) type DB struct { *gorm.DB } func NewDBStore(lc fx.Lifecycle, config *config.Config) (*DB, error) { err := os.MkdirAll(config.DataDir, 0o777) // #nosec if err != nil { log.Error("Failed to create Dashboard storage directory", zap.Error(err)) return nil, err } p := path.Join(config.DataDir, "dashboard.sqlite.db") log.Info("Dashboard initializing local storage file", zap.String("path", p)) gormDB, err := gorm.Open(sqlite.Open(p), &gorm.Config{ Logger: zapgorm2.New(log.L()), }) if err != nil { log.Error("Failed to open Dashboard storage file", zap.Error(err)) return nil, err } db := &DB{gormDB} lc.Append(fx.Hook{ OnStop: func(context.Context) error { return utils.CloseTiDBConnection(db.DB) }, }) return db, nil } ================================================ FILE: pkg/httpc/client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package httpc import ( "context" "crypto/tls" "io" "net" "net/http" "time" "github.com/joomcode/errorx" "github.com/pingcap/log" "go.uber.org/fx" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/config" ) const ( defaultTimeout = time.Second * 10 ) type Client struct { http.Client header http.Header } func NewHTTPClient(lc fx.Lifecycle, config *config.Config) *Client { cli := http.Client{ Transport: &http.Transport{ DialTLS: func(network, addr string) (net.Conn, error) { conn, err := tls.Dial(network, addr, config.ClusterTLSConfig) return conn, err }, TLSClientConfig: config.ClusterTLSConfig, }, Timeout: defaultTimeout, } lc.Append(fx.Hook{ OnStop: func(context.Context) error { cli.CloseIdleConnections() return nil }, }) return &Client{ Client: cli, } } // Clone is a temporary solution to the unexpected shared pointer field and race problem // TODO: use latest `/util/client` for better api experience. func (c *Client) Clone() *Client { return &Client{ Client: c.Client, header: c.header.Clone(), } } func (c Client) WithTimeout(timeout time.Duration) *Client { c.Timeout = timeout return &c } func (c *Client) CloneAndAddRequestHeader(key, value string) *Client { cc := c.Clone() if cc.header == nil { cc.header = http.Header{} } cc.header.Add(key, value) return cc } // TODO: Replace using go-resty. func (c *Client) SendRequest( ctx context.Context, uri string, method string, body io.Reader, errType *errorx.Type, errOriginComponent string, ) ([]byte, error) { res, err := c.Send(ctx, uri, method, body, errType, errOriginComponent) if err != nil { return nil, err } defer res.Response.Body.Close() return io.ReadAll(res.Response.Body) } func (c *Client) Send( ctx context.Context, uri string, method string, body io.Reader, errType *errorx.Type, errOriginComponent string, ) (*Response, error) { req, err := http.NewRequestWithContext(ctx, method, uri, body) if err != nil { e := errType.Wrap(err, "Failed to build %s API request", errOriginComponent) log.Warn("SendRequest failed", zap.String("uri", uri), zap.Error(err)) return nil, e } req.Header = c.header resp, err := c.Do(req) if err != nil { e := errType.Wrap(err, "Failed to send %s API request", errOriginComponent) log.Warn("SendRequest failed", zap.String("uri", uri), zap.Error(err)) return nil, e } if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) e := errType.New("Request failed with status code %d from %s API: %s", resp.StatusCode, errOriginComponent, string(data)) log.Warn("SendRequest failed", zap.String("uri", uri), zap.Error(err)) return nil, e } return &Response{resp}, nil } type Response struct { *http.Response } func (r *Response) Body() ([]byte, error) { defer r.Response.Body.Close() return io.ReadAll(r.Response.Body) } ================================================ FILE: pkg/httpc/client_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package httpc import ( "context" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" "go.uber.org/fx/fxtest" "github.com/pingcap/tidb-dashboard/pkg/config" ) func newTestClient(t *testing.T) *Client { lc := fxtest.NewLifecycle(t) config := &config.Config{} return NewHTTPClient(lc, config) } func Test_Clone(t *testing.T) { c := newTestClient(t) cc := c.Clone() require.NotSame(t, c, cc) require.Nil(t, c.header) require.Nil(t, cc.header) } func Test_CloneAndAddRequestHeader(t *testing.T) { c := newTestClient(t) cc := c.CloneAndAddRequestHeader("1", "11") require.Nil(t, c.header) require.Equal(t, "11", cc.header.Get("1")) cc2 := cc.CloneAndAddRequestHeader("2", "22") require.Equal(t, "11", cc.header.Get("1")) require.Equal(t, "", cc.header.Get("2")) require.Equal(t, "11", cc2.header.Get("1")) require.Equal(t, "22", cc2.header.Get("2")) } func Test_Send_withHeader(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(r.Header.Get("1"))) })) defer ts.Close() c := newTestClient(t) resp1, _ := c.Send(context.Background(), ts.URL, http.MethodGet, nil, nil, "") d1, _ := resp1.Body() require.Equal(t, "", string(d1)) cc := c.CloneAndAddRequestHeader("1", "11") resp2, _ := cc.Send(context.Background(), ts.URL, http.MethodGet, nil, nil, "") d2, _ := resp2.Body() require.Equal(t, "11", string(d2)) resp3, _ := c.Send(context.Background(), ts.URL, http.MethodGet, nil, nil, "") d3, _ := resp3.Body() require.Equal(t, "", string(d3)) } ================================================ FILE: pkg/keyvisual/decorator/decorator.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package decorator contains all implementations of LabelStrategy. package decorator import ( "encoding/hex" "github.com/pingcap/tidb-dashboard/pkg/config" ) // LabelKey is the decoration key. type LabelKey struct { Key string `json:"key" binding:"required"` Labels []string `json:"labels" binding:"required"` } // LabelStrategy requires cross-border determination and key decoration scheme. // It supports dynamic reload configuration and generation of an actuator. type LabelStrategy interface { ReloadConfig(cfg *config.KeyVisualConfig) NewLabeler() Labeler } // Labeler is an executor of LabelStrategy, and its functions should not be called concurrently. type Labeler interface { // CrossBorder determines whether two keys not belong to the same logical range. CrossBorder(startKey, endKey string) bool // Label returns the Label information of the keys. Label(keys []string) []LabelKey } // NaiveLabelStrategy is one of the simplest LabelStrategy. func NaiveLabelStrategy() LabelStrategy { return naiveLabelStrategy{} } type naiveLabelStrategy struct{} type naiveLabeler struct{} func (s naiveLabelStrategy) ReloadConfig(_ *config.KeyVisualConfig) {} func (s naiveLabelStrategy) NewLabeler() Labeler { return naiveLabeler{} } // CrossBorder always returns false. So naiveLabelStrategy believes that there are no cross-border situations. func (e naiveLabeler) CrossBorder(_, _ string) bool { return false } // Label only encodes the keys. func (e naiveLabeler) Label(keys []string) []LabelKey { labelKeys := make([]LabelKey, len(keys)) for i, key := range keys { str := hex.EncodeToString([]byte(key)) labelKeys[i] = LabelKey{ Key: str, Labels: []string{str}, } } return labelKeys } ================================================ FILE: pkg/keyvisual/decorator/decorator_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package decorator import ( "testing" "github.com/pingcap/check" ) func TestDecorator(t *testing.T) { check.TestingT(t) } ================================================ FILE: pkg/keyvisual/decorator/separator.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package decorator import ( "strings" "sync/atomic" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/config" ) // SeparatorLabelStrategy implements the LabelStrategy interface. It obtains label information after splitting the key. func SeparatorLabelStrategy(cfg *config.KeyVisualConfig) LabelStrategy { s := &separatorLabelStrategy{} s.Separator.Store(cfg.PolicyKVSeparator) return s } type separatorLabelStrategy struct { Separator atomic.Value } type separatorLabeler struct { Separator string } // ReloadConfig reset separator. func (s *separatorLabelStrategy) ReloadConfig(cfg *config.KeyVisualConfig) { s.Separator.Store(cfg.PolicyKVSeparator) log.Debug("Reload config", zap.String("separator", cfg.PolicyKVSeparator)) } func (s *separatorLabelStrategy) NewLabeler() Labeler { return &separatorLabeler{ Separator: s.Separator.Load().(string), } } // CrossBorder is temporarily not considering cross-border logic. func (e *separatorLabeler) CrossBorder(_, _ string) bool { return false } // Label uses separator to split key. func (e *separatorLabeler) Label(keys []string) []LabelKey { labelKeys := make([]LabelKey, len(keys)) for i, key := range keys { var labels []string if e.Separator == "" { labels = []string{key} } else { labels = strings.Split(key, e.Separator) } labelKeys[i] = LabelKey{ Key: key, Labels: labels, } } return labelKeys } ================================================ FILE: pkg/keyvisual/decorator/tidb.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package decorator import ( "context" "encoding/hex" "fmt" "sync" "time" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/region" "github.com/pingcap/tidb-dashboard/pkg/tidb" "github.com/pingcap/tidb-dashboard/pkg/tidb/model" ) // TiDBLabelStrategy implements the LabelStrategy interface. It obtains Label Information from TiDB. func TiDBLabelStrategy(lc fx.Lifecycle, wg *sync.WaitGroup, etcdClient *clientv3.Client, tidbClient *tidb.Client) LabelStrategy { s := &tidbLabelStrategy{ EtcdClient: etcdClient, tidbClient: tidbClient, SchemaVersion: -1, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { wg.Go(func() { s.Background(ctx) }) return nil }, }) return s } type tableDetail struct { Name string DB string ID int64 Indices map[int64]string } type tidbLabelStrategy struct { Config *config.Config EtcdClient *clientv3.Client TableMap sync.Map tidbClient *tidb.Client SchemaVersion int64 TidbAddress []string } type tidbLabeler struct { TableMap *sync.Map Buffer model.KeyInfoBuffer } func (s *tidbLabelStrategy) ReloadConfig(_ *config.KeyVisualConfig) {} func (s *tidbLabelStrategy) Background(ctx context.Context) { ticker := time.NewTicker(time.Minute) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: s.updateMap(ctx) } } } func (s *tidbLabelStrategy) NewLabeler() Labeler { return &tidbLabeler{ TableMap: &s.TableMap, } } // CrossBorder does not allow cross tables or cross indexes within a table. func (e *tidbLabeler) CrossBorder(startKey, endKey string) bool { startInfo, _ := e.Buffer.DecodeKey(region.Bytes(startKey)) startIsMeta, startTableID := startInfo.MetaOrTable() startIndex := startInfo.IndexInfo() endInfo, _ := e.Buffer.DecodeKey(region.Bytes(endKey)) endIsMeta, endTableID := endInfo.MetaOrTable() endIndex := endInfo.IndexInfo() if startIsMeta || endIsMeta { return startIsMeta != endIsMeta } if startTableID != endTableID { return true } return startIndex != endIndex } // Label will parse the ID information of the table and index. func (e *tidbLabeler) Label(keys []string) []LabelKey { labelKeys := make([]LabelKey, len(keys)) for i, key := range keys { labelKeys[i] = e.label(key) } if keys[0] == "" { labelKeys[0] = globalStart } endIndex := len(keys) - 1 if keys[endIndex] == "" { labelKeys[endIndex] = globalEnd } return labelKeys } func (e *tidbLabeler) label(key string) (label LabelKey) { keyBytes := region.Bytes(key) label.Key = hex.EncodeToString(keyBytes) keyInfo, _ := e.Buffer.DecodeKey(keyBytes) isMeta, tableID := keyInfo.MetaOrTable() if isMeta { label.Labels = append(label.Labels, "meta") return } var detail *tableDetail if v, ok := e.TableMap.Load(tableID); ok { detail = v.(*tableDetail) label.Labels = append(label.Labels, detail.DB, detail.Name) } else { label.Labels = append(label.Labels, fmt.Sprintf("table_%d", tableID)) } if isCommonHandle, rowID := keyInfo.RowInfo(); isCommonHandle { label.Labels = append(label.Labels, "row") } else if rowID != 0 { label.Labels = append(label.Labels, fmt.Sprintf("row_%d", rowID)) } else if indexID := keyInfo.IndexInfo(); indexID != 0 { if detail == nil { label.Labels = append(label.Labels, fmt.Sprintf("index_%d", indexID)) } else if name, ok := detail.Indices[indexID]; ok { label.Labels = append(label.Labels, name) } else { label.Labels = append(label.Labels, fmt.Sprintf("index_%d", indexID)) } } return } var globalStart = LabelKey{ Key: "", Labels: []string{"meta"}, } var globalEnd = LabelKey{ Key: "", Labels: []string{}, } ================================================ FILE: pkg/keyvisual/decorator/tidb_requests.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package decorator import ( "context" "encoding/json" "fmt" "net/url" "strconv" "strings" "time" "github.com/joomcode/errorx" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/tidb/model" "github.com/pingcap/tidb-dashboard/util/distro" ) const ( schemaVersionPath = "/tidb/ddl/global_schema_version" etcdGetTimeout = time.Second tableInfosBatchSize = 512 ) var ( ErrNS = errorx.NewNamespace("error.keyvisual") ErrNSDecorator = ErrNS.NewSubNamespace("decorator") ErrInvalidData = ErrNSDecorator.NewType("invalid_data") ) func (s *tidbLabelStrategy) updateMap(ctx context.Context) { // check schema version ectx, cancel := context.WithTimeout(ctx, etcdGetTimeout) resp, err := s.EtcdClient.Get(ectx, schemaVersionPath) cancel() if err != nil || len(resp.Kvs) != 1 { if s.SchemaVersion != -1 { log.Warn("failed to get tidb schema version", zap.Error(err)) } else { log.Debug("failed to get tidb schema version, maybe not a db cluster", zap.Error(err)) } return } schemaVersion, err := strconv.ParseInt(string(resp.Kvs[0].Value), 10, 64) if err != nil { if s.SchemaVersion != -1 { log.Warn("failed to get tidb schema version", zap.Error(err)) } else { log.Debug("failed to get tidb schema version, maybe not a db cluster", zap.Error(err)) } return } if schemaVersion == s.SchemaVersion { log.Debug("schema version has not changed, skip this update") return } log.Debug("schema version has changed", zap.Int64("old", s.SchemaVersion), zap.Int64("new", schemaVersion)) // get all database info var dbInfos []*model.DBInfo if err := s.request("/schema", &dbInfos); err != nil { log.Error("fail to send schema request", zap.String("component", distro.R().TiDB), zap.Error(err)) return } // get all table info updateSuccess := true for _, db := range dbInfos { if db.State == model.StateNone { continue } var tableInfos []*model.TableInfo encodeName := url.PathEscape(db.Name.O) if err := s.request(fmt.Sprintf("/schema/%s?id_name_only=true", encodeName), &tableInfos); err != nil { log.Error("fail to send schema request", zap.String("component", distro.R().TiDB), zap.Error(err)) updateSuccess = false continue } if len(tableInfos) == 0 { continue } if tableInfos[0].Version != nil { // ?id_name_only=true doesn't work, fallback. log.Debug("use fallback") s.updateTableMap(db.Name.O, tableInfos) continue } /* Split into small batches */ log.Debug("use batch") var tableIDBatches [][]string batch := make([]string, 0, tableInfosBatchSize) n := 0 for _, info := range tableInfos { batch = append(batch, strconv.FormatInt(info.ID, 10)) n++ if n == tableInfosBatchSize { tableIDBatches = append(tableIDBatches, batch) batch = make([]string, 0, tableInfosBatchSize) n = 0 } } if len(batch) > 0 { tableIDBatches = append(tableIDBatches, batch) } for _, batch := range tableIDBatches { var tableInfoBatch map[string]*model.TableInfo if err := s.request(fmt.Sprintf("/schema?table_ids=%s", strings.Join(batch, ",")), &tableInfoBatch); err != nil { log.Error("fail to send schema request", zap.String("component", distro.R().TiDB), zap.Error(err)) updateSuccess = false continue } if len(tableInfoBatch) == 0 { continue } tableInfoBatchSlice := make([]*model.TableInfo, 0, len(tableInfoBatch)) for _, info := range tableInfoBatch { tableInfoBatchSlice = append(tableInfoBatchSlice, info) } s.updateTableMap(db.Name.O, tableInfoBatchSlice) } } // update schema version if updateSuccess { s.SchemaVersion = schemaVersion } } func (s *tidbLabelStrategy) updateTableMap(dbname string, tableInfos []*model.TableInfo) { if len(tableInfos) == 0 { return } for _, table := range tableInfos { indices := make(map[int64]string, len(table.Indices)) for _, index := range table.Indices { indices[index.ID] = index.Name.O } detail := &tableDetail{ Name: table.Name.O, DB: dbname, ID: table.ID, Indices: indices, } s.TableMap.Store(table.ID, detail) if partition := table.GetPartitionInfo(); partition != nil { for _, partitionDef := range partition.Definitions { detail := &tableDetail{ Name: fmt.Sprintf("%s/%s", table.Name.O, partitionDef.Name.O), DB: dbname, ID: partitionDef.ID, Indices: indices, } s.TableMap.Store(partitionDef.ID, detail) } } } } func (s *tidbLabelStrategy) request(path string, v interface{}) error { data, err := s.tidbClient.SendGetRequest(path) if err != nil { return err } if err = json.Unmarshal(data, v); err != nil { return ErrInvalidData.Wrap(err, "%s schema API unmarshal failed", distro.R().TiDB) } return nil } ================================================ FILE: pkg/keyvisual/decorator/tidb_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package decorator import ( "github.com/pingcap/check" ) var _ = check.Suite(&testTiDBSuite{}) type testTiDBSuite struct{} ================================================ FILE: pkg/keyvisual/input/api.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package input import ( "encoding/hex" "encoding/json" "fmt" "net/url" "sort" "github.com/joomcode/errorx" regionpkg "github.com/pingcap/tidb-dashboard/pkg/keyvisual/region" "github.com/pingcap/tidb-dashboard/pkg/pd" "github.com/pingcap/tidb-dashboard/util/distro" ) const ScanRegionsLimit = 51200 var ( ErrNS = errorx.NewNamespace("error.keyvisual") ErrNSInput = ErrNS.NewSubNamespace("input") ErrInvalidData = ErrNSInput.NewType("invalid_data") ) // RegionInfo records detail region info for api usage. type RegionInfo struct { ID uint64 `json:"id"` StartKey string `json:"start_key"` EndKey string `json:"end_key"` WrittenBytes uint64 `json:"written_bytes"` ReadBytes uint64 `json:"read_bytes"` WrittenKeys uint64 `json:"written_keys"` ReadKeys uint64 `json:"read_keys"` ApproximateSize int64 `json:"approximate_size"` ApproximateKeys int64 `json:"approximate_keys"` } // RegionsInfo contains some regions with the detailed region info. type RegionsInfo struct { Count int `json:"count"` Regions []*RegionInfo `json:"regions"` } func (rs *RegionsInfo) Len() int { return rs.Count } func (rs *RegionsInfo) GetKeys() []string { keys := make([]string, rs.Count+1) keys[0] = rs.Regions[0].StartKey endKeys := keys[1:] for i, region := range rs.Regions { endKeys[i] = region.EndKey } return keys } func (rs *RegionsInfo) GetValues(tag regionpkg.StatTag) []uint64 { values := make([]uint64, rs.Count) switch tag { case regionpkg.WrittenBytes: for i, region := range rs.Regions { values[i] = region.WrittenBytes } case regionpkg.ReadBytes: for i, region := range rs.Regions { values[i] = region.ReadBytes } case regionpkg.WrittenKeys: for i, region := range rs.Regions { values[i] = region.WrittenKeys } case regionpkg.ReadKeys: for i, region := range rs.Regions { values[i] = region.ReadKeys } case regionpkg.Integration: for i, region := range rs.Regions { values[i] = region.WrittenBytes + region.ReadBytes } default: panic("unreachable") } return values } func read(data []byte) (*RegionsInfo, error) { regions := &RegionsInfo{} if err := json.Unmarshal(data, regions); err != nil { return nil, ErrInvalidData.Wrap(err, "%s regions API unmarshal failed", distro.R().PD) } for _, region := range regions.Regions { startBytes, err := hex.DecodeString(region.StartKey) if err != nil { return nil, ErrInvalidData.Wrap(err, "%s regions API unmarshal failed", distro.R().PD) } region.StartKey = regionpkg.String(startBytes) endBytes, err := hex.DecodeString(region.EndKey) if err != nil { return nil, ErrInvalidData.Wrap(err, "%s regions API unmarshal failed", distro.R().PD) } region.EndKey = regionpkg.String(endBytes) } sort.Slice(regions.Regions, func(i, j int) bool { return regions.Regions[i].StartKey < regions.Regions[j].StartKey }) return regions, nil } func NewAPIPeriodicGetter(pdClient *pd.Client) regionpkg.RegionsInfoGenerator { return func() (regionpkg.RegionsInfo, error) { var mergedRegionsInfo RegionsInfo startKey := "" for { regionsInfo, err := scanRegions(pdClient, startKey, "", ScanRegionsLimit) if err != nil { return nil, err } // Decode the the hex encode code start key and end key. for _, region := range regionsInfo.Regions { startBytes, err := hex.DecodeString(region.StartKey) if err != nil { return nil, ErrInvalidData.Wrap(err, "%s regions API unmarshal failed", distro.R().PD) } region.StartKey = regionpkg.String(startBytes) endBytes, err := hex.DecodeString(region.EndKey) if err != nil { return nil, ErrInvalidData.Wrap(err, "%s regions API unmarshal failed", distro.R().PD) } region.EndKey = regionpkg.String(endBytes) } mergedRegionsInfo.Regions = append(mergedRegionsInfo.Regions, regionsInfo.Regions...) mergedRegionsInfo.Count += regionsInfo.Count if regionsInfo.Count == 0 || regionsInfo.Regions[len(regionsInfo.Regions)-1].EndKey == "" { break } startKey = regionsInfo.Regions[len(regionsInfo.Regions)-1].EndKey } return &mergedRegionsInfo, nil } } func scanRegions(pdclient *pd.Client, key, endKey string, limit int) (*RegionsInfo, error) { values := url.Values{ "key": {key}, "end_key": {endKey}, "limit": {fmt.Sprintf("%d", limit)}, } url := "/regions/key" + "?" + values.Encode() data, err := pdclient.SendGetRequest(url) if err != nil { return nil, err } var regionsInfo RegionsInfo err = json.Unmarshal(data, ®ionsInfo) if err != nil { return nil, err } return ®ionsInfo, nil } ================================================ FILE: pkg/keyvisual/input/file.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package input import ( "context" "io" "os" "path/filepath" "time" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/storage" "github.com/pingcap/tidb-dashboard/util/distro" ) type fileInput struct { StartTime time.Time EndTime time.Time Now time.Time } // FileInput reads files in the specified time range from the ./data directory. func FileInput(startTime, endTime time.Time) StatInput { return &fileInput{ StartTime: startTime, EndTime: endTime, Now: time.Now(), } } func (input *fileInput) GetStartTime() time.Time { return input.Now.Add(input.StartTime.Sub(input.EndTime)) } func (input *fileInput) Background(_ context.Context, stat *storage.Stat) { log.Info("keyvisual load files from", zap.Time("start-time", input.StartTime)) fileTime := input.StartTime for !fileTime.After(input.EndTime) { regions, err := readFile(fileTime) fileTime = fileTime.Add(time.Minute) if err == nil { stat.Append(regions, input.Now.Add(fileTime.Sub(input.EndTime))) } } log.Info("keyvisual load files to", zap.Time("end-time", input.EndTime)) } func readFile(fileTime time.Time) (*RegionsInfo, error) { fileName := fileTime.Format("./data/20060102-15-04.json") file, err := os.Open(filepath.Clean(fileName)) if err != nil { return nil, ErrInvalidData.Wrap(err, "%s regions API unmarshal failed, from file %s", distro.R().PD, fileName) } defer file.Close() // #nosec data, err := io.ReadAll(file) if err != nil { return nil, ErrInvalidData.Wrap(err, "%s regions API unmarshal failed, from file %s", distro.R().PD, fileName) } return read(data) } ================================================ FILE: pkg/keyvisual/input/input.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package input defines several different data inputs. package input import ( "context" "time" "github.com/pingcap/log" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/region" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/storage" ) // StatInput is the interface that different data inputs need to implement. type StatInput interface { GetStartTime() time.Time Background(ctx context.Context, stat *storage.Stat) } func NewStatInput(provider *region.DataProvider) StatInput { if provider.FileStartTime == 0 && provider.FileEndTime == 0 { if provider.PeriodicGetter == nil { log.Fatal("Empty DataProvider is not allowed") } return PeriodicInput(provider.PeriodicGetter) } startTime := time.Unix(provider.FileStartTime, 0) endTime := time.Unix(provider.FileEndTime, 0) return FileInput(startTime, endTime) } ================================================ FILE: pkg/keyvisual/input/periodic.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package input import ( "context" "time" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/region" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/storage" ) type periodicInput struct { PeriodicGetter region.RegionsInfoGenerator } func PeriodicInput(periodicGetter region.RegionsInfoGenerator) StatInput { return &periodicInput{ PeriodicGetter: periodicGetter, } } func (input *periodicInput) GetStartTime() time.Time { return time.Now() } func (input *periodicInput) Background(ctx context.Context, stat *storage.Stat) { ticker := time.NewTicker(time.Minute) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: regions, err := input.PeriodicGetter() if err != nil { log.Warn("can not get RegionsInfo", zap.Error(err)) continue } endTime := time.Now() stat.Append(regions, endTime) } } } ================================================ FILE: pkg/keyvisual/manager.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package keyvisual import ( "context" "net/http" "sync" "github.com/gin-gonic/gin" "github.com/pingcap/log" "go.uber.org/fx" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/util/rest" ) func (s *Service) managerHook() fx.Hook { var wg sync.WaitGroup return fx.Hook{ OnStart: func(ctx context.Context) error { wg.Go(func() { s.managerLoop(ctx) }) return nil }, OnStop: func(context.Context) error { wg.Wait() return nil }, } } func (s *Service) managerLoop(ctx context.Context) { ch := s.cfgManager.NewPushChannel() for { select { case <-ctx.Done(): s.stopService() return case cfg, ok := <-ch: if !ok { s.stopService() return } s.resetKeyVisualConfig(ctx, cfg) } } } func (s *Service) resetKeyVisualConfig(ctx context.Context, cfg *config.DynamicConfig) { if !cfg.KeyVisual.AutoCollectionDisabled { if s.keyVisualCfg != nil && s.keyVisualCfg.Policy != cfg.KeyVisual.Policy { s.stopService() } s.reloadKeyVisualConfig(&cfg.KeyVisual) s.startService(ctx) } else { s.stopService() s.reloadKeyVisualConfig(&cfg.KeyVisual) } } func (s *Service) startService(ctx context.Context) { if s.IsRunning() { return } if err := s.Start(ctx); err != nil { log.Error("Can not start key visual service", zap.Error(err)) } else { log.Info("Key visual service is started") } } func (s *Service) stopService() { if !s.IsRunning() { return } if err := s.Stop(context.Background()); err != nil { log.Error("Can not stop key visual service", zap.Error(err)) } else { log.Info("Key visual service is stopped") } } // @Summary Get Key Visual Dynamic Config // @Success 200 {object} config.KeyVisualConfig // @Router /keyvisual/config [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) getDynamicConfig(c *gin.Context) { dc, err := s.cfgManager.Get() if err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, dc.KeyVisual) } // @Summary Set Key Visual Dynamic Config // @Param request body config.KeyVisualConfig true "Request body" // @Success 200 {object} config.KeyVisualConfig // @Router /keyvisual/config [put] // @Security JwtAuth // @Failure 400 {object} rest.ErrorResponse // @Failure 401 {object} rest.ErrorResponse // @Failure 500 {object} rest.ErrorResponse func (s *Service) setDynamicConfig(c *gin.Context) { var req config.KeyVisualConfig if err := c.ShouldBindJSON(&req); err != nil { rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) return } var opt config.DynamicConfigOption = func(dc *config.DynamicConfig) { dc.KeyVisual = req } if err := s.cfgManager.Modify(opt); err != nil { rest.Error(c, err) return } c.JSON(http.StatusOK, req) } ================================================ FILE: pkg/keyvisual/matrix/average.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix // AverageSplitStrategy adopts the strategy of equal distribution when buckets are split. func AverageSplitStrategy() SplitStrategy { return averageSplitStrategy{} } type averageSplitStrategy struct{} type averageSplitter struct{} func (averageSplitStrategy) NewSplitter(_ []chunk, _ []string) Splitter { return averageSplitter{} } func (averageSplitter) Split(dst, src chunk, tag splitTag, _ int) { CheckPartOf(dst.Keys, src.Keys) if len(dst.Keys) == len(src.Keys) { switch tag { case splitTo: copy(dst.Values, src.Values) case splitAdd: for i, v := range src.Values { dst.Values[i] += v } default: panic("unreachable") } return } start := 0 for startKey := src.Keys[0]; !equal(dst.Keys[start], startKey); { start++ } end := start + 1 switch tag { case splitTo: for i, key := range src.Keys[1:] { for !equal(dst.Keys[end], key) { end++ } value := src.Values[i] / uint64(end-start) for ; start < end; start++ { dst.Values[start] = value } end++ } case splitAdd: for i, key := range src.Keys[1:] { for !equal(dst.Keys[end], key) { end++ } value := src.Values[i] / uint64(end-start) for ; start < end; start++ { dst.Values[start] += value } end++ } default: panic("unreachable") } } ================================================ FILE: pkg/keyvisual/matrix/average_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "github.com/pingcap/check" ) var _ = check.Suite(&testAverageSuite{}) type testAverageSuite struct{} ================================================ FILE: pkg/keyvisual/matrix/axis.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "github.com/pingcap/tidb-dashboard/pkg/keyvisual/decorator" ) // Axis stores consecutive buckets. Each bucket has StartKey, EndKey, and some statistics. The EndKey of each bucket is // the StartKey of its next bucket. The actual data structure is stored in columns. Therefore satisfies: // len(Keys) == len(ValuesList[i]) + 1. In particular, ValuesList[0] is the base column. type Axis struct { Keys []string ValuesList [][]uint64 } // CreateAxis checks the given parameters and uses them to build the Axis. func CreateAxis(keys []string, valuesList [][]uint64) Axis { keysLen := len(keys) if keysLen <= 1 { panic("Keys length must be greater than 1") } if len(valuesList) == 0 { panic("ValuesList length must be greater than 0") } for _, values := range valuesList { if keysLen != len(values)+1 { panic("Keys length must be equal to Values length + 1") } } return Axis{ Keys: keys, ValuesList: valuesList, } } // CreateEmptyAxis constructs a minimal empty Axis with the given parameters. func CreateEmptyAxis(startKey, endKey string, valuesListLen int) Axis { keys := []string{startKey, endKey} values := []uint64{0} valuesList := make([][]uint64, valuesListLen) for i := range valuesList { valuesList[i] = values } return CreateAxis(keys, valuesList) } // Shrink reduces all statistical values. func (axis *Axis) Shrink(ratio uint64) { for _, values := range axis.ValuesList { for i := range values { values[i] /= ratio } } } // Range returns a sub Axis with specified range. func (axis *Axis) Range(startKey string, endKey string) Axis { start, end, ok := KeysRange(axis.Keys, startKey, endKey) if !ok { return CreateEmptyAxis(startKey, endKey, len(axis.ValuesList)) } keys := axis.Keys[start:end] valuesList := make([][]uint64, len(axis.ValuesList)) for i := range valuesList { valuesList[i] = axis.ValuesList[i][start : end-1] } return CreateAxis(keys, valuesList) } // Focus uses the base column as the chunk for the Focus operation to obtain the partitioning scheme, and uses this to // reduce other columns. func (axis *Axis) Focus(labeler decorator.Labeler, threshold uint64, ratio int, target int) Axis { if target >= len(axis.Keys)-1 { return *axis } baseChunk := createChunk(axis.Keys, axis.ValuesList[0]) newChunk := baseChunk.Focus(labeler, threshold, ratio, target, MergeColdLogicalRange) valuesListLen := len(axis.ValuesList) newValuesList := make([][]uint64, valuesListLen) newValuesList[0] = newChunk.Values for i := 1; i < valuesListLen; i++ { baseChunk.SetValues(axis.ValuesList[i]) newValuesList[i] = baseChunk.Reduce(newChunk.Keys).Values } return CreateAxis(newChunk.Keys, newValuesList) } // Divide uses the base column as the chunk for the Divide operation to obtain the partitioning scheme, and uses this to // reduce other columns. func (axis *Axis) Divide(labeler decorator.Labeler, target int) Axis { if target >= len(axis.Keys)-1 { return *axis } baseChunk := createChunk(axis.Keys, axis.ValuesList[0]) newChunk := baseChunk.Divide(labeler, target, MergeColdLogicalRange) valuesListLen := len(axis.ValuesList) newValuesList := make([][]uint64, valuesListLen) newValuesList[0] = newChunk.Values for i := 1; i < valuesListLen; i++ { baseChunk.SetValues(axis.ValuesList[i]) newValuesList[i] = baseChunk.Reduce(newChunk.Keys).Values } return CreateAxis(newChunk.Keys, newValuesList) } type FocusMode int const ( NotMergeLogicalRange FocusMode = iota MergeColdLogicalRange ) type chunk struct { // Keys and ValuesList[i] from Axis Keys []string Values []uint64 } func createChunk(keys []string, values []uint64) chunk { keysLen := len(keys) if keysLen <= 1 { panic("Keys length must be greater than 1") } if keysLen != len(values)+1 { panic("Keys length must be equal to Values length + 1") } return chunk{ Keys: keys, Values: values, } } func createZeroChunk(keys []string) chunk { keysLen := len(keys) if keysLen <= 1 { panic("Keys length must be greater than 1") } return createChunk(keys, make([]uint64, keysLen-1)) } func (c *chunk) SetValues(values []uint64) { if len(values)+1 != len(c.Keys) { panic("Keys length must be equal to Values length + 1") } c.Values = values } func (c *chunk) SetZeroValues() { newValues := make([]uint64, len(c.Values)) c.SetValues(newValues) } // Set all values to 0. func (c *chunk) Clear() { MemsetUint64(c.Values, 0) } // Calculation // Reduce generates new chunks based on the more sparse newKeys. func (c *chunk) Reduce(newKeys []string) chunk { keys := c.Keys CheckReduceOf(keys, newKeys) newValues := make([]uint64, len(newKeys)-1) if len(keys) == len(newKeys) { copy(newValues, c.Values) return createChunk(newKeys, newValues) } endKeys := newKeys[1:] j := 0 for i, value := range c.Values { if i > 0 && equal(keys[i], endKeys[j]) { j++ } newValues[j] += value } return createChunk(newKeys, newValues) } // GetFocusRows estimates the number of rows generated by executing a Focus with a specified threshold. func (c *chunk) GetFocusRows(threshold uint64) (count int) { start := 0 var bucketSum uint64 generateBucket := func(end int) { if end > start { count++ start = end bucketSum = 0 } } for i, value := range c.Values { if value >= threshold || bucketSum >= threshold { generateBucket(i) } bucketSum += value } generateBucket(len(c.Values)) return } // Given a `threshold`, merge the rows with less traffic, // and merge the most `ratio` rows at a time. // `target` is the estimated final number of rows. func (c *chunk) Focus(labeler decorator.Labeler, threshold uint64, ratio int, target int, mode FocusMode) chunk { newKeys := make([]string, 0, target) newValues := make([]uint64, 0, target) newKeys = append(newKeys, c.Keys[0]) start := 0 var bucketSum uint64 generateBucket := func(end int) { if end > start { newKeys = append(newKeys, c.Keys[end]) newValues = append(newValues, bucketSum) start = end bucketSum = 0 } } for i, value := range c.Values { if value >= threshold || bucketSum >= threshold || i-start >= ratio || labeler.CrossBorder(c.Keys[start], c.Keys[i]) { generateBucket(i) } bucketSum += value } generateBucket(len(c.Values)) newChunk := createChunk(newKeys, newValues) if mode == MergeColdLogicalRange && len(newValues) >= target { newChunk = newChunk.MergeColdLogicalRange(labeler, threshold, target) } return newChunk } func (c *chunk) MergeColdLogicalRange(labeler decorator.Labeler, threshold uint64, target int) chunk { threshold /= 4 // TODO: This var can be adjusted newKeys := make([]string, 0, target) newValues := make([]uint64, 0, target) newKeys = append(newKeys, c.Keys[0]) coldStart := 0 coldEnd := 0 var coldRangeSum uint64 mergeColdRange := func() { if coldEnd <= coldStart { return } newKeys = append(newKeys, c.Keys[coldEnd]) newValues = append(newValues, coldRangeSum) coldStart = coldEnd coldRangeSum = 0 } generateRange := func(end int) { if end <= coldEnd { return } var rangeSum uint64 for i := coldEnd; i < end; i++ { rangeSum += c.Values[i] } if coldRangeSum > threshold || rangeSum > threshold { mergeColdRange() } if rangeSum > threshold { newKeys = append(newKeys, c.Keys[coldEnd+1:end+1]...) newValues = append(newValues, c.Values[coldEnd:end]...) coldStart = end } else { coldRangeSum += rangeSum } coldEnd = end } for i := range c.Values { if labeler.CrossBorder(c.Keys[i], c.Keys[i+1]) { generateRange(i + 1) } } generateRange(len(c.Values)) mergeColdRange() return createChunk(newKeys, newValues) } // Divide uses binary search to find a suitable threshold, which can reduce the number of buckets of Axis to near the target. func (c *chunk) Divide(labeler decorator.Labeler, target int, mode FocusMode) chunk { if target >= len(c.Values) { return *c } // get upperThreshold var upperThreshold uint64 = 1 for _, value := range c.Values { upperThreshold += value } // search threshold var lowerThreshold uint64 = 1 targetFocusRows := target * 2 / 3 // TODO: This var can be adjusted for lowerThreshold < upperThreshold { mid := (lowerThreshold + upperThreshold) >> 1 if c.GetFocusRows(mid) > targetFocusRows { lowerThreshold = mid + 1 } else { upperThreshold = mid } } threshold := lowerThreshold focusRows := c.GetFocusRows(threshold) ratio := len(c.Values)/(target-focusRows) + 1 return c.Focus(labeler, threshold, ratio, target, mode) } ================================================ FILE: pkg/keyvisual/matrix/axis_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "github.com/pingcap/check" ) var _ = check.Suite(&testAxisSuite{}) type testAxisSuite struct{} func (s *testAxisSuite) TestChunkReduce(c *check.C) { testcases := []struct { keys []string values []uint64 newKeys []string newValues []uint64 }{ { []string{"", "a", "b", "c", ""}, []uint64{1, 10, 100, 1000}, []string{"", "b", ""}, []uint64{11, 1100}, }, { []string{"", "a", "b", "c", ""}, []uint64{1, 10, 100, 1000}, []string{"", ""}, []uint64{1111}, }, } for _, testcase := range testcases { originChunk := createChunk(testcase.keys, testcase.values) reduceChunk := originChunk.Reduce(testcase.newKeys) c.Assert(reduceChunk.Values, check.DeepEquals, testcase.newValues) } } ================================================ FILE: pkg/keyvisual/matrix/distance.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "context" "math" "runtime" "sort" "sync" "go.uber.org/fx" ) // TODO: // * Multiplexing data between requests // * Limit memory usage func DistanceSplitStrategy(lc fx.Lifecycle, wg *sync.WaitGroup, ratio float64, level int, count int) SplitStrategy { pow := make([]float64, level) for i := range pow { pow[i] = math.Pow(ratio, float64(i)) } s := &distanceSplitStrategy{ SplitRatio: ratio, SplitLevel: level, SplitCount: count, SplitRatioPow: pow, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { s.StartWorkers(ctx, wg) return nil }, OnStop: func(context.Context) error { s.StopWorkers() return nil }, }) return s } type distanceSplitStrategy struct { SplitRatio float64 SplitLevel int SplitCount int SplitRatioPow []float64 ScaleWorkerCh chan *scaleTask } type distanceSplitter struct { Scale [][]float64 } func (s *distanceSplitStrategy) NewSplitter(chunks []chunk, compactKeys []string) Splitter { axesLen := len(chunks) keysLen := len(compactKeys) // generate key distance matrix dis := make([][]int, axesLen) for i := range axesLen { dis[i] = make([]int, keysLen) } // a column with the maximum value is virtualized on the right and left virtualColumn := make([]int, keysLen) MemsetInt(virtualColumn, axesLen) // calculate left distance updateLeftDis(dis[0], virtualColumn, chunks[0].Keys, compactKeys) for i := 1; i < axesLen; i++ { updateLeftDis(dis[i], dis[i-1], chunks[i].Keys, compactKeys) } // calculate the nearest distance on both sides end := axesLen - 1 updateRightDis(dis[end], virtualColumn, chunks[end].Keys, compactKeys) for i := end - 1; i >= 0; i-- { updateRightDis(dis[i], dis[i+1], chunks[i].Keys, compactKeys) } return &distanceSplitter{ Scale: s.GenerateScale(chunks, compactKeys, dis), } } func (e *distanceSplitter) Split(dst, src chunk, tag splitTag, axesIndex int) { CheckPartOf(dst.Keys, src.Keys) if len(dst.Keys) == len(src.Keys) { switch tag { case splitTo: copy(dst.Values, src.Values) case splitAdd: for i, v := range src.Values { dst.Values[i] += v } default: panic("unreachable") } return } start := 0 for startKey := src.Keys[0]; !equal(dst.Keys[start], startKey); { start++ } end := start + 1 switch tag { case splitTo: for i, key := range src.Keys[1:] { for !equal(dst.Keys[end], key) { end++ } value := src.Values[i] for ; start < end; start++ { dst.Values[start] = uint64(float64(value) * e.Scale[axesIndex][start]) } end++ } case splitAdd: for i, key := range src.Keys[1:] { for !equal(dst.Keys[end], key) { end++ } value := src.Values[i] for ; start < end; start++ { dst.Values[start] += uint64(float64(value) * e.Scale[axesIndex][start]) } end++ } default: panic("unreachable") } } // multi-threaded calculate scale matrix. var workerCount int func init() { workerCount = min(runtime.NumCPU(), 20) } type scaleTask struct { *sync.WaitGroup Dis []int Keys []string CompactKeys []string Scale *[]float64 } func (s *distanceSplitStrategy) StartWorkers(ctx context.Context, wg *sync.WaitGroup) { s.ScaleWorkerCh = make(chan *scaleTask, workerCount*100) wg.Add(workerCount) for i := 0; i < workerCount; i++ { go func() { defer wg.Done() s.GenerateScaleColumnWork(ctx, s.ScaleWorkerCh) }() } } func (s *distanceSplitStrategy) StopWorkers() { close(s.ScaleWorkerCh) } func (s *distanceSplitStrategy) GenerateScale(chunks []chunk, compactKeys []string, dis [][]int) [][]float64 { var wg sync.WaitGroup axesLen := len(chunks) scale := make([][]float64, axesLen) wg.Add(axesLen) for i := range axesLen { s.ScaleWorkerCh <- &scaleTask{ WaitGroup: &wg, Dis: dis[i], Keys: chunks[i].Keys, CompactKeys: compactKeys, Scale: &scale[i], } } wg.Wait() return scale } func (s *distanceSplitStrategy) GenerateScaleColumnWork(ctx context.Context, ch <-chan *scaleTask) { var maxDis int // Each split interval needs to be sorted after copying to tempDis var tempDis []int // Used as a mapping from distance to scale tempMapCap := 256 tempMap := make([]float64, tempMapCap) for { select { case <-ctx.Done(): return case task, ok := <-ch: if !ok { return } dis := task.Dis keys := task.Keys compactKeys := task.CompactKeys // The maximum distance between the StartKey and EndKey of a bucket // is considered the bucket distance. dis, maxDis = toBucketDis(dis) scale := make([]float64, len(dis)) *task.Scale = scale // When it is not enough to accommodate maxDis, expand the capacity. for tempMapCap <= maxDis { tempMapCap *= 2 tempMap = make([]float64, tempMapCap) } // generate scale column start := 0 for startKey := keys[0]; !equal(compactKeys[start], startKey); { start++ } end := start + 1 for _, key := range keys[1:] { for !equal(compactKeys[end], key) { end++ } if start+1 == end { // Optimize calculation when splitting into 1 scale[start] = 1.0 start++ } else { // Copy tempDis and calculate the top n levels tempDis = append(tempDis[:0], dis[start:end]...) tempLen := len(tempDis) sort.Ints(tempDis) // Calculate distribution factors and sums based on distance ordering level := 0 tempMap[tempDis[0]] = 1.0 tempValue := 1.0 tempSum := 1.0 for i := 1; i < tempLen; i++ { d := tempDis[i] if d != tempDis[i-1] { level++ if level >= s.SplitLevel || i >= s.SplitCount { tempMap[d] = 0 } else { // tempValue = math.Pow(s.SplitRatio, float64(level)) tempValue = s.SplitRatioPow[level] tempMap[d] = tempValue } } tempSum += tempValue } // Calculate scale for ; start < end; start++ { scale[start] = tempMap[dis[start]] / tempSum } } end++ } // task finish task.Done() } } } func updateLeftDis(dis, leftDis []int, keys, compactKeys []string) { CheckPartOf(compactKeys, keys) j := 0 keysLen := len(keys) for i := range dis { if j < keysLen && equal(compactKeys[i], keys[j]) { dis[i] = 0 j++ } else { dis[i] = leftDis[i] + 1 } } } func updateRightDis(dis, rightDis []int, keys, compactKeys []string) { j := 0 keysLen := len(keys) for i := range dis { if j < keysLen && equal(compactKeys[i], keys[j]) { dis[i] = 0 j++ } else { dis[i] = Min(dis[i], rightDis[i]+1) } } } func toBucketDis(dis []int) ([]int, int) { maxDis := 0 for i := len(dis) - 1; i > 0; i-- { dis[i] = Max(dis[i], dis[i-1]) maxDis = Max(maxDis, dis[i]) } return dis[1:], maxDis } ================================================ FILE: pkg/keyvisual/matrix/distance_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "compress/gzip" "context" "encoding/json" "fmt" "math" "os" "sync" "testing" "github.com/pingcap/check" "go.uber.org/fx" ) var _ = check.Suite(&testDistanceSuite{}) type testDistanceSuite struct{} type testDisData struct { Dis []int `json:"dis"` Keys []string `json:"keys"` CompactKeysLen int `json:"compact_keys_len"` } func BenchmarkGenerateScale(b *testing.B) { perr := func(err error) { if err != nil { panic("Can not load test data!") } } var data testDisData fin, err := os.Open("../testdata/dis.json.gzip") perr(err) defer func() { _ = fin.Close() }() ifs, err := gzip.NewReader(fin) perr(err) err = json.NewDecoder(ifs).Decode(&data) perr(err) n := 300 chunks := make([]chunk, n) disOrig := make([][]int, n) dis := make([][]int, n) for i := range chunks { chunks[i] = createZeroChunk(data.Keys) disOrig[i] = make([]int, len(data.Dis)) } rollbackDis := func() { copy(dis, disOrig) for i := range dis { copy(dis[i], data.Dis) } } compactKeys := []string{""} for i := 1; i < data.CompactKeysLen; i++ { compactKeys = append(compactKeys, fmt.Sprintf("t%05d", i)) } compactKeys = append(compactKeys, "") keymap := KeyMap{} keymap.SaveKeys(compactKeys) keymap.SaveKeys(data.Keys) var strategy SplitStrategy wg := &sync.WaitGroup{} app := fx.New( fx.Provide(func(lc fx.Lifecycle) SplitStrategy { return DistanceSplitStrategy(lc, wg, 1.0/math.Phi, 15, 50) }), fx.Populate(&strategy), ) _ = app.Start(context.Background()) s := strategy.(*distanceSplitStrategy) b.ResetTimer() for i := 0; i < b.N; i++ { b.StopTimer() rollbackDis() b.StartTimer() _ = s.GenerateScale(chunks, compactKeys, dis) } b.StopTimer() _ = app.Stop(context.Background()) wg.Wait() } ================================================ FILE: pkg/keyvisual/matrix/interface.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "github.com/pingcap/tidb-dashboard/pkg/keyvisual/decorator" ) type splitTag int const ( splitTo splitTag = iota // Direct assignment after split splitAdd // Add to original value after split ) // SplitStrategy is an allocation scheme. It is used to generate a Splitter for a plane to split a chunk of columns. type SplitStrategy interface { NewSplitter(chunks []chunk, compactKeys []string) Splitter } type Splitter interface { // Split a chunk of columns. Split(dst, src chunk, tag splitTag, axesIndex int) } // Strategy is part of the customizable strategy in Matrix generation. type Strategy struct { decorator.LabelStrategy SplitStrategy } ================================================ FILE: pkg/keyvisual/matrix/key.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "sync" ) // KeyMap is used for string intern. type KeyMap struct { sync.RWMutex sync.Map } // SaveKey interns a string. func (km *KeyMap) SaveKey(key *string) { uniqueKey, _ := km.LoadOrStore(*key, *key) *key = uniqueKey.(string) } // SaveKeys interns all strings without using mutex. func (km *KeyMap) SaveKeys(keys []string) { for i, key := range keys { uniqueKey, _ := km.LoadOrStore(key, key) keys[i] = uniqueKey.(string) } } func equal(keyA, keyB string) bool { return keyA == keyB } ================================================ FILE: pkg/keyvisual/matrix/key_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "github.com/pingcap/check" ) var _ = check.Suite(&testKeySuite{}) type testKeySuite struct{} ================================================ FILE: pkg/keyvisual/matrix/matrix.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package matrix abstracts the source data as Plane, and then pixelates it into a matrix for display on the front end. package matrix import ( "time" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/decorator" ) // Matrix is the front end displays the required data. type Matrix struct { Keys []string `json:"-"` DataMap map[string][][]uint64 `json:"data" binding:"required"` KeyAxis []decorator.LabelKey `json:"keyAxis" binding:"required"` TimeAxis []int64 `json:"timeAxis" binding:"required"` } // CreateMatrix uses the specified times and keys to build an initial matrix with no data. func CreateMatrix(labeler decorator.Labeler, times []time.Time, keys []string, valuesListLen int) Matrix { dataMap := make(map[string][][]uint64, valuesListLen) // collect label keys keyAxis := labeler.Label(keys) // collect unix times timeAxis := make([]int64, len(times)) for i, t := range times { timeAxis[i] = t.Unix() } return Matrix{ Keys: keys, DataMap: dataMap, KeyAxis: keyAxis, TimeAxis: timeAxis, } } // Range returns a sub Matrix with specified range. func (mx *Matrix) Range(startKey, endKey string) { start, end, ok := KeysRange(mx.Keys, startKey, endKey) if !ok { panic("unreachable") } mx.Keys = mx.Keys[start:end] mx.KeyAxis = mx.KeyAxis[start:end] for _, data := range mx.DataMap { for i, axis := range data { data[i] = axis[start : end-1] } } } ================================================ FILE: pkg/keyvisual/matrix/matrix_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "testing" "github.com/pingcap/check" ) func TestMatrix(t *testing.T) { check.TestingT(t) } var _ = check.Suite(&testMatrixSuite{}) type testMatrixSuite struct{} ================================================ FILE: pkg/keyvisual/matrix/plane.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "sync" "time" ) // Plane stores consecutive axes. Each axis has StartTime, EndTime. The EndTime of each axis is the StartTime of its // next axis. Therefore satisfies: // len(Times) == len(Axes) + 1. type Plane struct { Times []time.Time Axes []Axis } // CreatePlane checks the given parameters and uses them to build the Plane. func CreatePlane(times []time.Time, axes []Axis) Plane { if len(times) <= 1 { panic("Times length must be greater than 1") } return Plane{ Times: times, Axes: axes, } } // CreateEmptyPlane constructs a minimal empty Plane with the given parameters. func CreateEmptyPlane(startTime, endTime time.Time, startKey, endKey string, valuesListLen int) Plane { return CreatePlane([]time.Time{startTime, endTime}, []Axis{CreateEmptyAxis(startKey, endKey, valuesListLen)}) } // Compact compacts Plane into an axis. func (plane *Plane) Compact(strategy SplitStrategy) Axis { chunks := make([]chunk, len(plane.Axes)) for i, axis := range plane.Axes { chunks[i] = createChunk(axis.Keys, axis.ValuesList[0]) } compactChunk, splitter := compact(strategy, chunks) valuesListLen := len(plane.Axes[0].ValuesList) valuesList := make([][]uint64, valuesListLen) valuesList[0] = compactChunk.Values for j := 1; j < valuesListLen; j++ { compactChunk.SetZeroValues() for i, axis := range plane.Axes { chunks[i].SetValues(axis.ValuesList[j]) splitter.Split(compactChunk, chunks[i], splitAdd, i) } valuesList[j] = compactChunk.Values } return CreateAxis(compactChunk.Keys, valuesList) } // Pixel pixelates Plane into a matrix with a number of rows close to the target. func (plane *Plane) Pixel(strategy *Strategy, target int, displayTags []string) Matrix { valuesListLen := len(plane.Axes[0].ValuesList) if valuesListLen != len(displayTags) { panic("the length of displayTags and valuesList should be equal") } axesLen := len(plane.Axes) chunks := make([]chunk, axesLen) for i, axis := range plane.Axes { chunks[i] = createChunk(axis.Keys, axis.ValuesList[0]) } compactChunk, splitter := compact(strategy, chunks) labeler := strategy.NewLabeler() baseKeys := compactChunk.Divide(labeler, target, NotMergeLogicalRange).Keys matrix := CreateMatrix(labeler, plane.Times, baseKeys, valuesListLen) var wg sync.WaitGroup var mutex sync.Mutex generateFunc := func(j int) { defer wg.Done() data := make([][]uint64, axesLen) goCompactChunk := createZeroChunk(compactChunk.Keys) for i, axis := range plane.Axes { goCompactChunk.Clear() splitter.Split(goCompactChunk, createChunk(chunks[i].Keys, axis.ValuesList[j]), splitTo, i) data[i] = goCompactChunk.Reduce(baseKeys).Values } mutex.Lock() defer mutex.Unlock() matrix.DataMap[displayTags[j]] = data } wg.Add(valuesListLen) for j := range valuesListLen { go generateFunc(j) } wg.Wait() return matrix } func compact(strategy SplitStrategy, chunks []chunk) (compactChunk chunk, splitter Splitter) { // get compact chunk keys keySet := make(map[string]struct{}) unlimitedEnd := false for _, c := range chunks { end := len(c.Keys) - 1 endKey := c.Keys[end] if endKey == "" { unlimitedEnd = true } else { keySet[endKey] = struct{}{} } for _, key := range c.Keys[:end] { keySet[key] = struct{}{} } } var compactKeys []string if unlimitedEnd { compactKeys = MakeKeysWithUnlimitedEnd(keySet) } else { compactKeys = MakeKeys(keySet) } compactChunk = createZeroChunk(compactKeys) splitter = strategy.NewSplitter(chunks, compactChunk.Keys) for i, c := range chunks { splitter.Split(compactChunk, c, splitAdd, i) } return } ================================================ FILE: pkg/keyvisual/matrix/plane_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "github.com/pingcap/check" ) var _ = check.Suite(&testPlaneSuite{}) type testPlaneSuite struct{} ================================================ FILE: pkg/keyvisual/matrix/util.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "sort" ) // MemsetUint64 sets all elements of the uint64 slice to v. func MemsetUint64(slice []uint64, v uint64) { sliceLen := len(slice) if sliceLen == 0 { return } slice[0] = v for bp := 1; bp < sliceLen; bp <<= 1 { copy(slice[bp:], slice[:bp]) } } // MemsetInt sets all elements of the int slice to v. func MemsetInt(slice []int, v int) { sliceLen := len(slice) if sliceLen == 0 { return } slice[0] = v for bp := 1; bp < sliceLen; bp <<= 1 { copy(slice[bp:], slice[:bp]) } } // GetLastKey gets the last element of keys. func GetLastKey(keys []string) string { return keys[len(keys)-1] } // CheckPartOf checks that part keys are a subset of src keys. func CheckPartOf(src, part []string) { err := src[0] > part[0] || len(src) < len(part) srcLastKey := GetLastKey(src) partLastKey := GetLastKey(part) if srcLastKey != "" && (partLastKey == "" || srcLastKey < partLastKey) { err = true } if err { panic("The inclusion relationship is not satisfied between keys") } } // CheckReduceOf checks that part keys are a subset of src keys and have the same StartKey and EndKey. func CheckReduceOf(src, part []string) { if src[0] != part[0] || GetLastKey(src) != GetLastKey(part) || len(src) < len(part) { panic("The inclusion relationship is not satisfied between keys") } } // MakeKeys uses a key set to build a new Key-Axis. func MakeKeys(keySet map[string]struct{}) []string { keysLen := len(keySet) keys := make([]string, keysLen, keysLen+1) i := 0 for key := range keySet { keys[i] = key i++ } sort.Strings(keys) return keys } // MakeKeysWithUnlimitedEnd uses a key set to build a new Key-Axis, then add a "" to the keys, indicating that the last // bucket has an unlimited end. func MakeKeysWithUnlimitedEnd(keySet map[string]struct{}) []string { keys := MakeKeys(keySet) return append(keys, "") } // KeysRange finds a range that intersects [startKey, endKey) in keys. func KeysRange(keys []string, startKey string, endKey string) (start, end int, ok bool) { if endKey != "" && startKey >= endKey { panic("StartKey must be less than EndKey") } // ensure intersection if endKey != "" && endKey <= keys[0] { return -1, -1, false } axisEndKey := GetLastKey(keys) if axisEndKey != "" && startKey >= axisEndKey { return -1, -1, false } keysLen := len(keys) sortedKeysLen := keysLen if axisEndKey == "" { sortedKeysLen-- } // start index (contain) start = sort.Search(sortedKeysLen, func(i int) bool { return keys[i] > startKey }) if start > 0 { start-- } // end index (contain) end = keysLen if endKey != "" { end = sort.Search(sortedKeysLen, func(i int) bool { return keys[i] >= endKey }) if end < keysLen { end++ } } return start, end, true } // Max returns the larger of a and b. func Max(a, b int) int { if a > b { return a } return b } // Min returns the smaller of a and b. func Min(a, b int) int { if a < b { return a } return b } ================================================ FILE: pkg/keyvisual/matrix/util_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package matrix import ( "github.com/pingcap/check" ) var _ = check.Suite(&testUtilSuite{}) type testUtilSuite struct{} func (s *testUtilSuite) TestMemset(c *check.C) { s1 := []uint64{3, 3, 3, 3, 3} s2 := []uint64{0, 0, 0, 0, 0} s3 := []int{6, 6, 6, 6} s4 := []int{9, 9, 9, 9} MemsetUint64(s1, 0) MemsetInt(s3, 9) c.Assert(s1, check.DeepEquals, s2) c.Assert(s3, check.DeepEquals, s4) } ================================================ FILE: pkg/keyvisual/region/interface.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package region type RegionsInfo interface { Len() int GetKeys() []string GetValues(tag StatTag) []uint64 } type RegionsInfoGenerator func() (RegionsInfo, error) type DataProvider struct { // File mode (debug) FileStartTime int64 FileEndTime int64 // API or Core mode // This item takes effect only when both FileStartTime and FileEndTime are 0. PeriodicGetter RegionsInfoGenerator } ================================================ FILE: pkg/keyvisual/region/tag.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package region // StatTag is a tag for statistics of different dimensions. type StatTag int const ( // Integration is The overall value of all other dimension statistics. Integration StatTag = iota // WrittenBytes is the size of the data written per minute. WrittenBytes // ReadBytes is the size of the data read per minute. ReadBytes // WrittenKeys is the number of keys written to the data per minute. WrittenKeys // ReadKeys is the number of keys read to the data per minute. ReadKeys ) // IntoTag converts a string into a StatTag. func IntoTag(typ string) StatTag { switch typ { case "": return Integration case "integration": return Integration case "written_bytes": return WrittenBytes case "read_bytes": return ReadBytes case "written_keys": return WrittenKeys case "read_keys": return ReadKeys default: return WrittenBytes } } func (tag StatTag) String() string { switch tag { case Integration: return "integration" case WrittenBytes: return "written_bytes" case ReadBytes: return "read_bytes" case WrittenKeys: return "written_keys" case ReadKeys: return "read_keys" default: panic("unreachable") } } // StorageTags is the order of tags during storage. var StorageTags = []StatTag{WrittenBytes, ReadBytes, WrittenKeys, ReadKeys} // ResponseTags is the order of tags when responding. var ResponseTags = append([]StatTag{Integration}, StorageTags...) // GetDisplayTags returns the actual order of the ResponseTags under the specified baseTag. func GetDisplayTags(baseTag StatTag) []string { displayTags := make([]string, len(ResponseTags)) for i, tag := range ResponseTags { displayTags[i] = tag.String() if tag == baseTag { displayTags[0], displayTags[i] = displayTags[i], displayTags[0] } } return displayTags } ================================================ FILE: pkg/keyvisual/region/utils.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package region import ( "unsafe" ) // String converts slice of bytes to string without copy. func String(b []byte) string { if len(b) == 0 { return "" } return unsafe.String(&b[0], len(b)) // #nosec } // Bytes converts a string into a byte slice. Need to make sure that the byte slice is not modified. func Bytes(s string) []byte { if len(s) == 0 { return nil } return unsafe.Slice(unsafe.StringData(s), len(s)) // #nosec } ================================================ FILE: pkg/keyvisual/service.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package keyvisual import ( "context" "encoding/hex" "math" "net/http" "strconv" "sync" "time" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/fx" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/dbstore" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/decorator" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/input" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/matrix" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/region" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/storage" "github.com/pingcap/tidb-dashboard/pkg/pd" "github.com/pingcap/tidb-dashboard/pkg/tidb" "github.com/pingcap/tidb-dashboard/pkg/utils" ) const ( heatmapsMaxDisplayY = 1536 distanceStrategyRatio = 1.0 / math.Phi distanceStrategyLevel = 15 distanceStrategyCount = 50 ) var ( ErrNS = errorx.NewNamespace("error.keyvisual") ErrServiceStopped = ErrNS.NewType("service_stopped") defaultStatConfig = storage.StatConfig{ LayersConfig: []storage.LayerConfig{ {Len: 60, Ratio: 2 / 1}, // step 1 minutes, total 60, 1 hours (sum: 1 hours) {Len: 60 / 2 * 7, Ratio: 6 / 2}, // step 2 minutes, total 210, 7 hours (sum: 8 hours) {Len: 60 / 6 * 16, Ratio: 30 / 6}, // step 6 minutes, total 160, 16 hours (sum: 1 days) {Len: 60 / 30 * 24 * 6, Ratio: 4 * 60 / 30}, // step 30 minutes, total 288, 6 days (sum: 1 weeks) {Len: 24 / 4 * 28, Ratio: 0}, // step 4 hours, total 168, 4 weeks (sum: 5 weeks) }, } ) type Service struct { app *fx.App status *utils.ServiceStatus ctx context.Context cancel context.CancelFunc config *config.Config keyVisualCfg *config.KeyVisualConfig cfgManager *config.DynamicConfigManager customProvider *region.DataProvider etcdClient *clientv3.Client pdClient *pd.Client db *dbstore.DB tidbClient *tidb.Client stat *storage.Stat strategy *matrix.Strategy labelStrategy decorator.LabelStrategy } // FIXME: Simplify these things. func NewService( lc fx.Lifecycle, cfg *config.Config, cfgManager *config.DynamicConfigManager, customProvider *region.DataProvider, etcdClient *clientv3.Client, pdClient *pd.Client, db *dbstore.DB, tidbClient *tidb.Client, ) *Service { s := &Service{ status: utils.NewServiceStatus(), config: cfg, cfgManager: cfgManager, customProvider: customProvider, etcdClient: etcdClient, pdClient: pdClient, db: db, tidbClient: tidbClient, } lc.Append(s.managerHook()) return s } func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/keyvisual") endpoint.Use(auth.MWAuthRequired()) endpoint.GET("/config", s.getDynamicConfig) endpoint.PUT("/config", auth.MWRequireWritePriv(), s.setDynamicConfig) endpoint.Use(s.status.MWHandleStopped(stoppedHandler)) endpoint.GET("/heatmaps", s.heatmaps) } func (s *Service) IsRunning() bool { return s.status.IsRunning() } 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.Provide( newWaitGroup, newStrategy, newStat, s.provideLocals, s.newProvider, input.NewStatInput, s.newLabelStrategy, ), fx.Populate(&s.stat, &s.strategy, &s.labelStrategy), fx.Invoke( // Must be at the end s.status.Register, ), ) if err := s.app.Start(s.ctx); err != nil { s.cleanAfterError() return err } return nil } func (s *Service) newLabelStrategy( lc fx.Lifecycle, wg *sync.WaitGroup, etcdClient *clientv3.Client, tidbClient *tidb.Client, ) decorator.LabelStrategy { switch s.keyVisualCfg.Policy { case config.KeyVisualDBPolicy: log.Debug("New LabelStrategy", zap.String("policy", s.keyVisualCfg.Policy)) return decorator.TiDBLabelStrategy(lc, wg, etcdClient, tidbClient) case config.KeyVisualKVPolicy: log.Debug("New LabelStrategy", zap.String("policy", s.keyVisualCfg.Policy), zap.String("separator", s.keyVisualCfg.PolicyKVSeparator)) return decorator.SeparatorLabelStrategy(s.keyVisualCfg) default: panic("unreachable") } } func (s *Service) newProvider(pdClient *pd.Client) *region.DataProvider { if s.customProvider != nil { return s.customProvider } return ®ion.DataProvider{ PeriodicGetter: input.NewAPIPeriodicGetter(pdClient), } } func (s *Service) reloadKeyVisualConfig(cfg *config.KeyVisualConfig) { s.keyVisualCfg = cfg if s.labelStrategy != nil { s.labelStrategy.ReloadConfig(s.keyVisualCfg) } } func (s *Service) cleanAfterError() { s.cancel() // drop s.app = nil s.stat = nil s.strategy = nil s.labelStrategy = 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.stat = nil s.strategy = nil s.labelStrategy = nil s.ctx = nil s.cancel = nil return err } // @Summary Key Visual Heatmaps // @Description Heatmaps in a given range to visualize TiKV usage // @Param startkey query string false "The start of the key range" // @Param endkey query string false "The end of the key range" // @Param starttime query int false "The start of the time range (Unix)" // @Param endtime query int false "The end of the time range (Unix)" // @Param type query string false "Main types of data" Enums(written_bytes, read_bytes, written_keys, read_keys, integration) // @Success 200 {object} matrix.Matrix // @Router /keyvisual/heatmaps [get] // @Security JwtAuth // @Failure 401 {object} rest.ErrorResponse func (s *Service) heatmaps(c *gin.Context) { startKey := c.Query("startkey") endKey := c.Query("endkey") startTimeString := c.Query("starttime") endTimeString := c.Query("endtime") typ := c.Query("type") endTime := time.Now() startTime := endTime.Add(-360 * time.Minute) if startTimeString != "" { tsSec, err := strconv.ParseInt(startTimeString, 10, 64) if err != nil { log.Error("parse ts failed", zap.Error(err)) c.JSON(http.StatusBadRequest, "bad request") return } startTime = time.Unix(tsSec, 0) } if endTimeString != "" { tsSec, err := strconv.ParseInt(endTimeString, 10, 64) if err != nil { log.Error("parse ts failed", zap.Error(err)) c.JSON(http.StatusBadRequest, "bad request") return } endTime = time.Unix(tsSec, 0) } if !startTime.Before(endTime) || (endKey != "" && startKey >= endKey) { c.JSON(http.StatusBadRequest, "bad request") return } log.Debug("Request matrix", zap.Time("start-time", startTime), zap.Time("end-time", endTime), zap.String("start-key", startKey), zap.String("end-key", endKey), zap.String("type", typ), ) if startKeyBytes, err := hex.DecodeString(startKey); err == nil { startKey = string(startKeyBytes) } else { c.JSON(http.StatusBadRequest, "bad request") return } if endKeyBytes, err := hex.DecodeString(endKey); err == nil { endKey = string(endKeyBytes) } else { c.JSON(http.StatusBadRequest, "bad request") return } baseTag := region.IntoTag(typ) plane := s.stat.Range(startTime, endTime, startKey, endKey, baseTag) resp := plane.Pixel(s.strategy, heatmapsMaxDisplayY, region.GetDisplayTags(baseTag)) resp.Range(startKey, endKey) // TODO: An expedient to reduce data transmission, which needs to be deleted later. resp.DataMap = map[string][][]uint64{ typ: resp.DataMap[typ], } // ---------- c.JSON(http.StatusOK, resp) } func (s *Service) provideLocals() (*config.Config, *clientv3.Client, *pd.Client, *dbstore.DB, *tidb.Client) { return s.config, s.etcdClient, s.pdClient, s.db, s.tidbClient } func newWaitGroup(lc fx.Lifecycle) *sync.WaitGroup { wg := &sync.WaitGroup{} lc.Append(fx.Hook{ OnStop: func(_ context.Context) error { wg.Wait() return nil }, }) return wg } func newStrategy(lc fx.Lifecycle, wg *sync.WaitGroup, labelStrategy decorator.LabelStrategy) *matrix.Strategy { return &matrix.Strategy{ LabelStrategy: labelStrategy, SplitStrategy: matrix.DistanceSplitStrategy( lc, wg, distanceStrategyRatio, distanceStrategyLevel, distanceStrategyCount, ), } } func newStat( lc fx.Lifecycle, wg *sync.WaitGroup, _ *clientv3.Client, db *dbstore.DB, in input.StatInput, strategy *matrix.Strategy, ) *storage.Stat { stat := storage.NewStat(lc, wg, db, defaultStatConfig, strategy, in.GetStartTime()) lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { wg.Go(func() { in.Background(ctx, stat) }) return nil }, }) return stat } func stoppedHandler(c *gin.Context) { _ = c.AbortWithError(http.StatusNotFound, ErrServiceStopped.NewWithNoMessage()) } ================================================ FILE: pkg/keyvisual/storage/model.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package storage import ( "bytes" "encoding/gob" "time" "gorm.io/gorm" "github.com/pingcap/tidb-dashboard/pkg/dbstore" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/matrix" ) const tableAxisModelName = "keyviz_axis" type AxisModel struct { LayerNum uint8 `gorm:"unique_index:index_layer_time"` Time time.Time `gorm:"unique_index:index_layer_time"` Axis []byte } func (AxisModel) TableName() string { return tableAxisModelName } func NewAxisModel(layerNum uint8, time time.Time, axis matrix.Axis) (*AxisModel, error) { var buf bytes.Buffer enc := gob.NewEncoder(&buf) err := enc.Encode(axis) if err != nil { return nil, err } return &AxisModel{ layerNum, time, buf.Bytes(), }, nil } func (a *AxisModel) UnmarshalAxis() (matrix.Axis, error) { buf := bytes.NewBuffer(a.Axis) dec := gob.NewDecoder(buf) var axis matrix.Axis err := dec.Decode(&axis) return axis, err } func (a *AxisModel) Insert(db *dbstore.DB) error { return db.Create(a).Error } func (a *AxisModel) Delete(db *dbstore.DB) error { return db. Where("layer_num = ? AND time = ?", a.LayerNum, a.Time). Delete(&AxisModel{}). Error } // If the table `AxisModel` exists, return true, nil // or create table `AxisModel`. func CreateTableAxisModelIfNotExists(db *dbstore.DB) (bool, error) { if db.Migrator().HasTable(&AxisModel{}) { return true, nil } return false, db.Migrator().CreateTable(&AxisModel{}) } func ClearTableAxisModel(db *dbstore.DB) error { return db.Session(&gorm.Session{AllowGlobalUpdate: true}). Delete(&AxisModel{}). Error } func FindAxisModelsOrderByTime(db *dbstore.DB, layerNum uint8) ([]*AxisModel, error) { var axisModels []*AxisModel err := db. Where("layer_num = ?", layerNum). Order("time"). Find(&axisModels). Error return axisModels, err } func DeleteAxisModelsByLayerNum(db *dbstore.DB, layerNum uint8) error { return db. Where("layer_num = ?", layerNum). Delete(&AxisModel{}). Error } ================================================ FILE: pkg/keyvisual/storage/model_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package storage import ( "path" "testing" "time" "github.com/pingcap/check" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/pingcap/tidb-dashboard/pkg/dbstore" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/matrix" ) func TestDbstore(t *testing.T) { check.TestingT(t) } var _ = check.Suite(&testDbstoreSuite{}) type testDbstoreSuite struct { dir string db *dbstore.DB } func (t *testDbstoreSuite) SetUpTest(c *check.C) { t.dir = c.MkDir() gormDB, err := gorm.Open(sqlite.Open(path.Join(t.dir, "test.sqlite.db"))) if err != nil { c.Errorf("Open %s error: %v", path.Join(t.dir, "test.sqlite.db"), err) } t.db = &dbstore.DB{DB: gormDB} } func (t *testDbstoreSuite) TestCreateTableAxisModelIfNotExists(c *check.C) { isExist, err := CreateTableAxisModelIfNotExists(t.db) c.Assert(isExist, check.Equals, false) c.Assert(err, check.IsNil) isExist, err = CreateTableAxisModelIfNotExists(t.db) c.Assert(isExist, check.Equals, true) c.Assert(err, check.IsNil) } func (t *testDbstoreSuite) TestClearTableAxisModel(c *check.C) { _, err := CreateTableAxisModelIfNotExists(t.db) if err != nil { c.Fatalf("Create table AxisModel error: %v", err) } axisModel, err := NewAxisModel(0, time.Now(), matrix.Axis{}) if err != nil { c.Fatalf("NewAxisModel error: %v", err) } err = axisModel.Insert(t.db) if err != nil { c.Fatalf("AxisModel Insert error: %v", err) } var count int64 err = t.db.Table(tableAxisModelName).Count(&count).Error if err != nil { c.Fatalf("Count table AxisModel error: %v", err) } c.Assert(count, check.Equals, int64(1)) err = ClearTableAxisModel(t.db) c.Assert(err, check.IsNil) err = t.db.Table(tableAxisModelName).Count(&count).Error if err != nil { c.Fatalf("Count table AxisModel error: %v", err) } c.Assert(count, check.Equals, int64(0)) } func (t *testDbstoreSuite) TestAxisModelFunc(c *check.C) { _, err := CreateTableAxisModelIfNotExists(t.db) if err != nil { c.Fatalf("Create table AxisModel error: %v", err) } var layerNum uint8 endTime := time.Now() axis := matrix.Axis{ Keys: []string{"a", "b"}, ValuesList: [][]uint64{{1}, {1}, {1}, {1}}, } axisModel, err := NewAxisModel(layerNum, endTime, axis) if err != nil { c.Fatalf("NewAxisModel error: %v", err) } err = axisModel.Insert(t.db) c.Assert(err, check.IsNil) axisModels, err := FindAxisModelsOrderByTime(t.db, layerNum) if err != nil { c.Fatalf("FindAxisModelOrderByTime error: %v", err) } c.Assert(len(axisModels), check.Equals, 1) axisModelDeepEqual(axisModels[0], axisModel, c) obtainedAxis, err := axisModels[0].UnmarshalAxis() if err != nil { c.Fatalf("UnmarshalAxis error: %v", err) } c.Assert(obtainedAxis, check.DeepEquals, axis) err = axisModel.Delete(t.db) c.Assert(err, check.IsNil) var count int64 err = t.db.Table(tableAxisModelName).Count(&count).Error if err != nil { c.Fatalf("Count table AxisModel error: %v", err) } c.Assert(count, check.Equals, int64(0)) err = axisModel.Delete(t.db) c.Assert(err, check.IsNil) } func (t *testDbstoreSuite) TestAxisModelsFindAndDelete(c *check.C) { _, err := CreateTableAxisModelIfNotExists(t.db) if err != nil { c.Fatalf("Create table AxisModel error: %v", err) } var maxLayerNum uint8 = 2 axisModelNumEachLayer := 3 axisModelList := make([][]*AxisModel, maxLayerNum) for layerNum := range maxLayerNum { axisModelList[layerNum] = make([]*AxisModel, axisModelNumEachLayer) for i := range axisModelNumEachLayer { axisModelList[layerNum][i], err = NewAxisModel(layerNum, time.Now(), matrix.Axis{}) if err != nil { c.Fatalf("NewAxisModel error: %v", err) } err = axisModelList[layerNum][i].Insert(t.db) if err != nil { c.Fatalf("NewAxisModel error: %v", err) } } } var count int64 err = t.db.Table(tableAxisModelName).Count(&count).Error if err != nil { c.Fatalf("Count table AxisModel error: %v", err) } c.Assert(count, check.Equals, int64(int(maxLayerNum)*axisModelNumEachLayer)) findLayerNum := maxLayerNum - 1 axisModels, err := FindAxisModelsOrderByTime(t.db, findLayerNum) c.Assert(err, check.IsNil) axisModelsDeepEqual(axisModels, axisModelList[findLayerNum], c) err = DeleteAxisModelsByLayerNum(t.db, findLayerNum) c.Assert(err, check.IsNil) axisModels, err = FindAxisModelsOrderByTime(t.db, findLayerNum) c.Assert(err, check.IsNil) c.Assert(axisModels, check.HasLen, 0) err = t.db.Table(tableAxisModelName).Count(&count).Error if err != nil { c.Fatalf("Count table AxisModel error: %v", err) } c.Assert(count, check.Equals, int64(int(maxLayerNum-1)*axisModelNumEachLayer)) } func axisModelsDeepEqual(obtainedAxisModels []*AxisModel, expectedAxisModels []*AxisModel, c *check.C) { c.Assert(len(obtainedAxisModels), check.Equals, len(expectedAxisModels)) for i := range obtainedAxisModels { axisModelDeepEqual(obtainedAxisModels[i], expectedAxisModels[i], c) } } func axisModelDeepEqual(obtainedAxisModel *AxisModel, expectedAxisModel *AxisModel, c *check.C) { c.Assert(obtainedAxisModel.Time.Unix(), check.Equals, expectedAxisModel.Time.Unix()) obtainedAxisModel.Time = expectedAxisModel.Time c.Assert(obtainedAxisModel, check.DeepEquals, expectedAxisModel) } ================================================ FILE: pkg/keyvisual/storage/region.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package storage import ( "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/decorator" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/matrix" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/region" ) // Source data pre processing parameters. const ( // preThreshold = 128 // preRatioTarget = 512. preTarget = 3072 dirtyWrittenBytes uint64 = 1 << 32 ) // CreateStorageAxis converts the RegionsInfo to a StorageAxis. func CreateStorageAxis(regions region.RegionsInfo, labeler decorator.Labeler) matrix.Axis { regionsLen := regions.Len() if regionsLen <= 0 { panic("At least one RegionInfo") } keys := regions.GetKeys() valuesList := make([][]uint64, len(region.ResponseTags)) for i, tag := range region.ResponseTags { valuesList[i] = regions.GetValues(tag) } preAxis := matrix.CreateAxis(keys, valuesList) wash(&preAxis) axis := IntoStorageAxis(preAxis, labeler) log.Debug("New StorageAxis", zap.Int("region length", regionsLen), zap.Int("focus keys length", len(axis.Keys))) return axis } // IntoStorageAxis converts ResponseAxis to StorageAxis. func IntoStorageAxis(responseAxis matrix.Axis, labeler decorator.Labeler) matrix.Axis { // axis := preAxis.Focus(strategy, preThreshold, len(keys)/preRatioTarget, preTarget) axis := responseAxis.Divide(labeler, preTarget) storageValuesList := make([][]uint64, 0, len(axis.ValuesList)) storageValuesList = append(storageValuesList, axis.ValuesList[1:]...) return matrix.CreateAxis(axis.Keys, storageValuesList) } // IntoResponseAxis converts StorageAxis to ResponseAxis. func IntoResponseAxis(storageAxis matrix.Axis, baseTag region.StatTag) matrix.Axis { // add integration values valuesList := make([][]uint64, 1, len(region.ResponseTags)) writtenBytes := storageAxis.ValuesList[0] readBytes := storageAxis.ValuesList[1] integration := make([]uint64, len(writtenBytes)) for i := range integration { integration[i] = writtenBytes[i] + readBytes[i] } valuesList[0] = integration valuesList = append(valuesList, storageAxis.ValuesList...) // swap baseTag for i, tag := range region.ResponseTags { if tag == baseTag { valuesList[0], valuesList[i] = valuesList[i], valuesList[0] return matrix.CreateAxis(storageAxis.Keys, valuesList) } } panic("unreachable") } // TODO: Temporary solution, need to trace the source of dirty data. func wash(axis *matrix.Axis) { for i, value := range axis.ValuesList[1] { if value >= dirtyWrittenBytes { for j := range region.ResponseTags { axis.ValuesList[j][i] = 0 } } } } ================================================ FILE: pkg/keyvisual/storage/region_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package storage import ( "testing" "github.com/pingcap/check" ) func TestRegion(t *testing.T) { check.TestingT(t) } var _ = check.Suite(&testRegionSuite{}) type testRegionSuite struct{} ================================================ FILE: pkg/keyvisual/storage/stat.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package storage stores the input axes in order, and can get a Plane by time interval. package storage import ( "context" "sort" "sync" "time" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/dbstore" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/decorator" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/matrix" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/region" ) // LayerConfig is the configuration of layerStat. type LayerConfig struct { Len int Ratio int } // layerStat is a layer in Stat. It uses a circular queue structure and can store up to Len Axes. Whenever the data is // full, the Ratio Axes will be compacted into an Axis and added to the next layer. type layerStat struct { StartTime time.Time EndTime time.Time RingAxes []matrix.Axis RingTimes []time.Time LayerNum uint8 Head int Tail int Empty bool Len int Db *dbstore.DB // Hierarchical mechanism SplitStrategy matrix.SplitStrategy Ratio int Next *layerStat } func newLayerStat( layerNum uint8, conf LayerConfig, splitStrategy matrix.SplitStrategy, startTime time.Time, db *dbstore.DB, ) *layerStat { return &layerStat{ StartTime: startTime, EndTime: startTime, RingAxes: make([]matrix.Axis, conf.Len), RingTimes: make([]time.Time, conf.Len), LayerNum: layerNum, Head: 0, Tail: 0, Empty: true, Len: conf.Len, Db: db, SplitStrategy: splitStrategy, Ratio: conf.Ratio, Next: nil, } } // Reduce merges ratio axes and append to next layerStat. func (s *layerStat) Reduce(labeler decorator.Labeler) { if s.Ratio == 0 || s.Next == nil { _ = s.DeleteFirstAxisFromDb() s.StartTime = s.RingTimes[s.Head] s.RingAxes[s.Head] = matrix.Axis{} s.Head = (s.Head + 1) % s.Len return } times := make([]time.Time, 0, s.Ratio+1) times = append(times, s.StartTime) axes := make([]matrix.Axis, 0, s.Ratio) for i := 0; i < s.Ratio; i++ { _ = s.DeleteFirstAxisFromDb() s.StartTime = s.RingTimes[s.Head] times = append(times, s.StartTime) axes = append(axes, s.RingAxes[s.Head]) s.RingAxes[s.Head] = matrix.Axis{} s.Head = (s.Head + 1) % s.Len } plane := matrix.CreatePlane(times, axes) newAxis := plane.Compact(s.SplitStrategy) newAxis = IntoResponseAxis(newAxis, region.Integration) newAxis = IntoStorageAxis(newAxis, labeler) newAxis.Shrink(uint64(s.Ratio)) s.Next.Append(newAxis, s.StartTime, labeler) } // Append appends a key axis to layerStat. func (s *layerStat) Append(axis matrix.Axis, endTime time.Time, labeler decorator.Labeler) { if s.Head == s.Tail && !s.Empty { s.Reduce(labeler) } _ = s.InsertLastAxisToDb(axis, endTime) s.RingAxes[s.Tail] = axis s.RingTimes[s.Tail] = endTime s.Empty = false s.EndTime = endTime s.Tail = (s.Tail + 1) % s.Len } // Range gets the specify plane in the time range. func (s *layerStat) Range(startTime, endTime time.Time) (times []time.Time, axes []matrix.Axis) { if s.Next != nil { times, axes = s.Next.Range(startTime, endTime) } if s.Empty || (!startTime.Before(s.EndTime) || !endTime.After(s.StartTime)) { return times, axes } size := s.Tail - s.Head if size <= 0 { size += s.Len } start := sort.Search(size, func(i int) bool { return s.RingTimes[(s.Head+i)%s.Len].After(startTime) }) end := sort.Search(size, func(i int) bool { return !s.RingTimes[(s.Head+i)%s.Len].Before(endTime) }) if end != size { end++ } n := end - start start = (s.Head + start) % s.Len // add StartTime if len(times) == 0 { if start == s.Head { times = append(times, s.StartTime) } else { times = append(times, s.RingTimes[(start-1+s.Len)%s.Len]) } } if start+n <= s.Len { times = append(times, s.RingTimes[start:start+n]...) axes = append(axes, s.RingAxes[start:start+n]...) } else { times = append(times, s.RingTimes[start:s.Len]...) times = append(times, s.RingTimes[0:start+n-s.Len]...) axes = append(axes, s.RingAxes[start:s.Len]...) axes = append(axes, s.RingAxes[0:start+n-s.Len]...) } return times, axes } // StatConfig is the configuration of Stat. type StatConfig struct { LayersConfig []LayerConfig } // Stat is composed of multiple layerStats. type Stat struct { mutex sync.RWMutex layers []*layerStat keyMap matrix.KeyMap strategy *matrix.Strategy db *dbstore.DB } // NewStat generates a Stat based on the configuration. func NewStat( lc fx.Lifecycle, wg *sync.WaitGroup, db *dbstore.DB, cfg StatConfig, strategy *matrix.Strategy, startTime time.Time, ) *Stat { layers := make([]*layerStat, len(cfg.LayersConfig)) for i, c := range cfg.LayersConfig { layers[i] = newLayerStat(uint8(i), c, strategy, startTime, db) if i > 0 { layers[i-1].Next = layers[i] } } s := &Stat{ layers: layers, strategy: strategy, db: db, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { if err := s.Restore(); err != nil { return err } wg.Go(func() { s.rebuildRegularly(ctx) }) return nil }, }) return s } func (s *Stat) rebuildKeyMap() { s.keyMap.Lock() defer s.keyMap.Unlock() s.mutex.Lock() defer s.mutex.Unlock() s.keyMap.Map = sync.Map{} for _, layer := range s.layers { for _, axis := range layer.RingAxes { if len(axis.Keys) > 0 { s.keyMap.SaveKeys(axis.Keys) } } } } func (s *Stat) rebuildRegularly(ctx context.Context) { ticker := time.NewTicker(time.Hour * 24) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: s.rebuildKeyMap() } } } // Append adds the latest full statistics. func (s *Stat) Append(regions region.RegionsInfo, endTime time.Time) { if regions.Len() == 0 { return } labeler := s.strategy.NewLabeler() axis := CreateStorageAxis(regions, labeler) s.keyMap.RLock() defer s.keyMap.RUnlock() s.keyMap.SaveKeys(axis.Keys) s.mutex.Lock() defer s.mutex.Unlock() s.layers[0].Append(axis, endTime, labeler) } func (s *Stat) rangeRoot(startTime, endTime time.Time) ([]time.Time, []matrix.Axis) { s.mutex.RLock() defer s.mutex.RUnlock() return s.layers[0].Range(startTime, endTime) } // Range returns a sub Plane with specified range. func (s *Stat) Range(startTime, endTime time.Time, startKey, endKey string, baseTag region.StatTag) matrix.Plane { s.keyMap.RLock() defer s.keyMap.RUnlock() s.keyMap.SaveKey(&startKey) s.keyMap.SaveKey(&endKey) times, axes := s.rangeRoot(startTime, endTime) if len(times) <= 1 { return matrix.CreateEmptyPlane(startTime, endTime, startKey, endKey, len(region.ResponseTags)) } for i, axis := range axes { axis = axis.Range(startKey, endKey) axis = IntoResponseAxis(axis, baseTag) axes[i] = axis } return matrix.CreatePlane(times, axes) } ================================================ FILE: pkg/keyvisual/storage/stat_persist.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package storage import ( "time" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/keyvisual/matrix" ) func (s *layerStat) InsertLastAxisToDb(axis matrix.Axis, endTime time.Time) error { log.Debug("Insert Axis", zap.Uint8("layer num", s.LayerNum), zap.Time("time", endTime)) axisModel, err := NewAxisModel(s.LayerNum, endTime, axis) if err != nil { return err } return axisModel.Insert(s.Db) } func (s *layerStat) DeleteFirstAxisFromDb() error { log.Debug("Delete Axis", zap.Uint8("layer num", s.LayerNum), zap.Time("time", s.StartTime)) axisModel, err := NewAxisModel(s.LayerNum, s.StartTime, matrix.Axis{}) if err != nil { return err } return axisModel.Delete(s.Db) } // Restore data from db the first time service starts. func (s *Stat) Restore() error { s.keyMap.Lock() defer s.keyMap.Unlock() s.mutex.Lock() defer s.mutex.Unlock() // insert start `AxisModel` for each layer createStartAxisModels := func() error { log.Debug("Create start axisModel for each layer") for i, layer := range s.layers { startAxisModel, err := NewAxisModel(uint8(i), layer.StartTime, matrix.Axis{}) if err != nil { return err } if err := startAxisModel.Insert(s.db); err != nil { return err } } return nil } // table `AxisModel` preprocess isExist, err := CreateTableAxisModelIfNotExists(s.db) if err != nil { return err } if !isExist { return createStartAxisModels() } // load data from db for layerNum := uint8(0); ; layerNum++ { axisModels, err := FindAxisModelsOrderByTime(s.db, layerNum) if err != nil { return err } if len(axisModels) == 0 { break } if layerNum >= uint8(len(s.layers)) { log.Warn("Layer num is too large. Ignore and delete the redundant axisModels", zap.Uint8("layer num", layerNum), zap.Int("layers len", len(s.layers))) _ = DeleteAxisModelsByLayerNum(s.db, layerNum) continue } if len(axisModels) > 1 { s.layers[layerNum].Empty = false } else if layerNum == 0 { // no valid data was stored,clear log.Debug("Clear table AxisModel") if err := ClearTableAxisModel(s.db); err != nil { return err } return createStartAxisModels() } log.Debug("Load axisModels", zap.Uint8("layer num", layerNum), zap.Int("len", len(axisModels)-1)) // the first axisModel is only used to save starttime s.layers[layerNum].StartTime = axisModels[0].Time s.layers[layerNum].Head = 0 n := len(axisModels) - 1 if n > s.layers[layerNum].Len { log.Warn("The number of axisModel is longer than layer's len", zap.Int("number", n), zap.Int("layer len", s.layers[layerNum].Len), zap.Uint8("layer num", layerNum)) for _, p := range axisModels[s.layers[layerNum].Len+1:] { _ = p.Delete(s.db) } n = s.layers[layerNum].Len } s.layers[layerNum].EndTime = axisModels[n].Time s.layers[layerNum].Tail = (s.layers[layerNum].Head + n) % s.layers[layerNum].Len for i, axisModel := range axisModels[1 : n+1] { s.layers[layerNum].RingTimes[i] = axisModel.Time axis, err := axisModel.UnmarshalAxis() if err != nil { return err } s.keyMap.SaveKeys(axis.Keys) s.layers[layerNum].RingAxes[i] = axis } } return nil } ================================================ FILE: pkg/keyvisual/storage/stat_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package storage import ( "testing" "github.com/pingcap/check" ) func TestStat(t *testing.T) { check.TestingT(t) } var _ = check.Suite(&testStatSuite{}) type testStatSuite struct{} ================================================ FILE: pkg/pd/client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pd import ( "context" "fmt" "io" "net" "net/http" "strconv" "time" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/httpc" "github.com/pingcap/tidb-dashboard/util/distro" ) var ErrPDClientRequestFailed = ErrNS.NewType("client_request_failed") const ( defaultPDTimeout = time.Second * 10 ) type Client struct { httpScheme string baseURL string withoutPrefix bool httpClient *httpc.Client lifecycleCtx context.Context timeout time.Duration } func NewPDClient(lc fx.Lifecycle, httpClient *httpc.Client, config *config.Config) *Client { client := &Client{ httpClient: httpClient, httpScheme: config.GetClusterHTTPScheme(), baseURL: config.PDEndPoint, lifecycleCtx: nil, timeout: defaultPDTimeout, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { client.lifecycleCtx = ctx return nil }, }) return client } func (c Client) WithBaseURL(baseURL string) *Client { c.baseURL = baseURL return &c } func (c Client) WithAddress(host string, port int) *Client { c.baseURL = fmt.Sprintf("%s://%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(port))) return &c } func (c Client) WithTimeout(timeout time.Duration) *Client { c.timeout = timeout return &c } func (c Client) WithoutPrefix() *Client { c.withoutPrefix = true return &c } func (c Client) getPrefix() string { if c.withoutPrefix { return "" } return "/pd/api/v1" } func (c Client) AddRequestHeader(key, value string) *Client { c.httpClient = c.httpClient.CloneAndAddRequestHeader(key, value) return &c } func (c *Client) Get(relativeURI string) (*httpc.Response, error) { uri := fmt.Sprintf("%s%s%s", c.baseURL, c.getPrefix(), relativeURI) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrPDClientRequestFailed, distro.R().PD) } func (c *Client) SendGetRequest(relativeURI string) ([]byte, error) { res, err := c.Get(relativeURI) if err != nil { return nil, err } return res.Body() } func (c *Client) SendPostRequest(relativeURI string, body io.Reader) ([]byte, error) { uri := fmt.Sprintf("%s%s%s", c.baseURL, c.getPrefix(), relativeURI) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrPDClientRequestFailed, distro.R().PD) } ================================================ FILE: pkg/pd/client_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pd import ( "context" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" "go.uber.org/fx/fxtest" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/httpc" ) func newTestClient(t *testing.T) *Client { lc := fxtest.NewLifecycle(t) config := &config.Config{} c := NewPDClient(lc, httpc.NewHTTPClient(lc, config), config) c.lifecycleCtx = context.Background() return c } func Test_AddRequestHeader_returnDifferentHTTPClient(t *testing.T) { c := newTestClient(t) cc := c.AddRequestHeader("1", "11") require.NotSame(t, c.httpClient, cc.httpClient) } func Test_Get_withHeader(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(r.Header.Get("1"))) })) defer ts.Close() c := newTestClient(t).WithBaseURL(ts.URL) resp1, _ := c.Get("") d1, _ := resp1.Body() require.Equal(t, "", string(d1)) cc := c.AddRequestHeader("1", "11") resp2, _ := cc.Get("") d2, _ := resp2.Body() require.Equal(t, "11", string(d2)) resp3, _ := c.Get("") d3, _ := resp3.Body() require.Equal(t, "", string(d3)) } ================================================ FILE: pkg/pd/etcd.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pd import ( "context" "time" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/fx" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/utils" ) func NewEtcdClient(lc fx.Lifecycle, config *config.Config) (*clientv3.Client, error) { zapCfg := zap.NewProductionConfig() zapCfg.Encoding = log.ZapEncodingName cli, err := clientv3.New(clientv3.Config{ Endpoints: []string{config.PDEndPoint}, AutoSyncInterval: 30 * time.Second, DialTimeout: 5 * time.Second, DialKeepAliveTime: utils.DefaultGRPCKeepaliveParams.Time, DialKeepAliveTimeout: utils.DefaultGRPCKeepaliveParams.Timeout, PermitWithoutStream: utils.DefaultGRPCKeepaliveParams.PermitWithoutStream, DialOptions: utils.DefaultGRPCDialOptions, TLS: config.ClusterTLSConfig, LogConfig: &zapCfg, }) lc.Append(fx.Hook{ OnStop: func(context.Context) error { return cli.Close() }, }) return cli, err } ================================================ FILE: pkg/pd/pd.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pd import ( "github.com/joomcode/errorx" ) var ErrNS = errorx.NewNamespace("error.pd") ================================================ FILE: pkg/scheduling/client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package scheduling import ( "context" "fmt" "io" "net" "net/http" "strconv" "time" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/httpc" "github.com/pingcap/tidb-dashboard/util/distro" ) var ErrSchedulingClientRequestFailed = ErrNS.NewType("client_request_failed") const ( defaultSchedulingStatusAPITimeout = time.Second * 10 ) type Client struct { httpClient *httpc.Client httpScheme string lifecycleCtx context.Context timeout time.Duration } func NewSchedulingClient(lc fx.Lifecycle, httpClient *httpc.Client, config *config.Config) *Client { client := &Client{ httpClient: httpClient, httpScheme: config.GetClusterHTTPScheme(), lifecycleCtx: nil, timeout: defaultSchedulingStatusAPITimeout, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { client.lifecycleCtx = ctx return nil }, }) return client } func (c Client) WithTimeout(timeout time.Duration) *Client { c.timeout = timeout return &c } func (c Client) AddRequestHeader(key, value string) *Client { c.httpClient = c.httpClient.CloneAndAddRequestHeader(key, value) return &c } func (c *Client) Get(host string, port int, relativeURI string) (*httpc.Response, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(port)), relativeURI) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrSchedulingClientRequestFailed, distro.R().Scheduling) } func (c *Client) SendGetRequest(host string, port int, relativeURI string) ([]byte, error) { res, err := c.Get(host, port, relativeURI) if err != nil { return nil, err } return res.Body() } func (c *Client) SendPostRequest(host string, port int, relativeURI string, body io.Reader) ([]byte, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(port)), relativeURI) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrSchedulingClientRequestFailed, distro.R().Scheduling) } ================================================ FILE: pkg/scheduling/scheduling.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package scheduling import ( "github.com/joomcode/errorx" ) var ErrNS = errorx.NewNamespace("error.scheduling") ================================================ FILE: pkg/swaggerserver/handler.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package swaggerserver import ( "net/http" httpSwagger "github.com/swaggo/http-swagger" // Swagger doc. _ "github.com/pingcap/tidb-dashboard/swaggerspec" ) func Handler() http.Handler { return httpSwagger.Handler() } ================================================ FILE: pkg/ticdc/client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package ticdc import ( "context" "fmt" "io" "net" "net/http" "strconv" "time" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/httpc" "github.com/pingcap/tidb-dashboard/util/distro" ) var ErrTiCDCClientRequestFailed = ErrNS.NewType("client_request_failed") const ( defaultTiCDCStatusAPITimeout = time.Second * 10 ) type Client struct { httpClient *httpc.Client httpScheme string lifecycleCtx context.Context timeout time.Duration } func NewTiCDCClient(lc fx.Lifecycle, httpClient *httpc.Client, config *config.Config) *Client { client := &Client{ httpClient: httpClient, httpScheme: config.GetClusterHTTPScheme(), lifecycleCtx: nil, timeout: defaultTiCDCStatusAPITimeout, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { client.lifecycleCtx = ctx return nil }, }) return client } func (c Client) WithTimeout(timeout time.Duration) *Client { c.timeout = timeout return &c } func (c Client) AddRequestHeader(key, value string) *Client { c.httpClient = c.httpClient.CloneAndAddRequestHeader(key, value) return &c } func (c *Client) Get(host string, statusPort int, relativeURI string) (*httpc.Response, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(statusPort)), relativeURI) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrTiCDCClientRequestFailed, distro.R().TiCDC) } func (c *Client) SendGetRequest(host string, statusPort int, relativeURI string) ([]byte, error) { res, err := c.Get(host, statusPort, relativeURI) if err != nil { return nil, err } return res.Body() } func (c *Client) SendPostRequest(host string, statusPort int, relativeURI string, body io.Reader) ([]byte, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(statusPort)), relativeURI) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrTiCDCClientRequestFailed, distro.R().TiCDC) } ================================================ FILE: pkg/ticdc/ticdc.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package ticdc import ( "github.com/joomcode/errorx" ) var ErrNS = errorx.NewNamespace("error.ticdc") ================================================ FILE: pkg/tidb/client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tidb import ( "context" "database/sql/driver" "fmt" "net" "net/http" "os" "strconv" "strings" "time" "github.com/VividCortex/mysqlerr" "github.com/go-sql-driver/mysql" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/fx" "go.uber.org/zap" mysqlDriver "gorm.io/driver/mysql" "gorm.io/gorm" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/httpc" "github.com/pingcap/tidb-dashboard/util/distro" ) var ( ErrTiDBConnFailed = ErrNS.NewType("tidb_conn_failed") ErrTiDBAuthFailed = ErrNS.NewType("tidb_auth_failed") ErrTiDBClientRequestFailed = ErrNS.NewType("client_request_failed") ) const ( defaultTiDBStatusAPITimeout = time.Second * 10 defaultTiDBSQLExecutionTimeoutMs = 600000 // 600s // When this environment variable is set, SQL requests will be always sent to this specific TiDB instance. // Calling `WithSQLAPIAddress` to enforce a SQL request endpoint will fail when opening the connection. tidbOverrideSQLEndpointEnvVar = "TIDB_OVERRIDE_ENDPOINT" // When this environment variable is set, status requests will be always sent to this specific TiDB instance. // Calling `WithStatusAPIAddress` to enforce a status API request endpoint will fail when opening the connection. tidbOverrideStatusEndpointEnvVar = "TIDB_OVERRIDE_STATUS_ENDPOINT" ) type Client struct { lifecycleCtx context.Context forwarder *Forwarder statusAPIHTTPScheme string statusAPIAddress string // Empty means to use address provided by forwarder enforceStatusAPIAddresss bool // enforced status api address and ignore env override config statusAPIHTTPClient *httpc.Client statusAPITimeout time.Duration sqlAPITLSKey string // Non empty means use this key as MySQL TLS config sqlAPIAddress string // Empty means to use address provided by forwarder } func NewTiDBClient(lc fx.Lifecycle, config *config.Config, etcdClient *clientv3.Client, httpClient *httpc.Client) *Client { sqlAPITLSKey := "" if config.TiDBTLSConfig != nil { sqlAPITLSKey = "tidb" _ = mysql.RegisterTLSConfig(sqlAPITLSKey, config.TiDBTLSConfig) } client := &Client{ lifecycleCtx: nil, forwarder: newForwarder(lc, etcdClient), statusAPIHTTPScheme: config.GetClusterHTTPScheme(), statusAPIAddress: "", enforceStatusAPIAddresss: false, statusAPIHTTPClient: httpClient, statusAPITimeout: defaultTiDBStatusAPITimeout, sqlAPITLSKey: sqlAPITLSKey, sqlAPIAddress: "", } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { client.lifecycleCtx = ctx return nil }, }) return client } func (c Client) WithStatusAPITimeout(timeout time.Duration) *Client { c.statusAPITimeout = timeout return &c } func (c Client) WithStatusAPIAddress(host string, statusPort int) *Client { c.statusAPIAddress = net.JoinHostPort(host, strconv.Itoa(statusPort)) return &c } func (c Client) WithEnforcedStatusAPIAddress(host string, statusPort int) *Client { c.enforceStatusAPIAddresss = true c.statusAPIAddress = net.JoinHostPort(host, strconv.Itoa(statusPort)) return &c } func (c Client) WithSQLAPIAddress(host string, sqlPort int) *Client { c.sqlAPIAddress = net.JoinHostPort(host, strconv.Itoa(sqlPort)) return &c } func (c *Client) OpenSQLConn(user string, pass string) (*gorm.DB, error) { var err error overrideEndpoint := os.Getenv(tidbOverrideSQLEndpointEnvVar) // the `tidbOverrideSQLEndpointEnvVar` and the `Client.sqlAPIAddress` have the same override priority, if both exist, an error is returned if overrideEndpoint != "" && c.sqlAPIAddress != "" { log.Warn(fmt.Sprintf("Reject to establish a target specified %s SQL connection since `%s` is set", distro.R().TiDB, tidbOverrideSQLEndpointEnvVar)) return nil, ErrTiDBConnFailed.New("%s Dashboard is configured to only connect to specified %s host", distro.R().TiDB, distro.R().TiDB) } var addr string switch { case overrideEndpoint != "": addr = overrideEndpoint default: addr = c.sqlAPIAddress } if addr == "" { if addr, err = c.forwarder.getEndpointAddr(c.forwarder.sqlPort); err != nil { return nil, err } } dsnConfig := mysql.NewConfig() dsnConfig.Net = "tcp" dsnConfig.Addr = addr dsnConfig.User = user dsnConfig.Passwd = pass dsnConfig.Timeout = time.Second dsnConfig.ParseTime = true dsnConfig.Loc = time.Local dsnConfig.MultiStatements = true dsnConfig.TLSConfig = c.sqlAPITLSKey dsn := dsnConfig.FormatDSN() db, err := gorm.Open(mysqlDriver.Open(dsn)) if err != nil { if _, ok := err.(*net.OpError); ok || err == driver.ErrBadConn { if strings.HasPrefix(addr, "0.0.0.0:") { log.Warn(fmt.Sprintf("%s reported its address to be 0.0.0.0. Please specify `-advertise-address` command line parameter when running %s", distro.R().TiDB, distro.R().TiDB)) } if c.forwarder.sqlProxy.noAliveRemote.Load() { return nil, ErrNoAliveTiDB.NewWithNoMessage() } return nil, ErrTiDBConnFailed.Wrap(err, "failed to connect to %s", distro.R().TiDB) } else if mysqlErr, ok := err.(*mysql.MySQLError); ok { if mysqlErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR { return nil, ErrTiDBAuthFailed.New("bad %s username or password", distro.R().TiDB) } } log.Warn(fmt.Sprintf("Unknown error occurred while opening %s connection", distro.R().TiDB), zap.Error(err)) return nil, err } if err := db.Exec(fmt.Sprintf("SET SESSION max_execution_time = '%d'", defaultTiDBSQLExecutionTimeoutMs)).Error; err != nil { log.Error("Failed to set max_execution_time", zap.Error(err)) if d, err := db.DB(); err == nil && db != nil { if cerr := d.Close(); cerr != nil { log.Error("Failed to close database after setting max_execution_time", zap.Error(cerr)) } } return nil, ErrTiDBClientRequestFailed.Wrap(err, "failed to set max_execution_time") } return db, nil } func (c *Client) Get(relativeURI string) (*httpc.Response, error) { var err error overrideEndpoint := os.Getenv(tidbOverrideStatusEndpointEnvVar) // the `tidbOverrideStatusEndpointEnvVar` and the `Client.statusAPIAddress` have the same override priority, if both exist and have not enforced `Client.statusAPIAddress` then an error is returned if overrideEndpoint != "" && c.statusAPIAddress != "" && !c.enforceStatusAPIAddresss { log.Warn(fmt.Sprintf("Reject to establish a target specified %s status connection since `%s` is set", distro.R().TiDB, tidbOverrideStatusEndpointEnvVar)) return nil, ErrTiDBConnFailed.New("%s Dashboard is configured to only connect to specified %s host", distro.R().TiDB, distro.R().TiDB) } var addr string switch { case c.enforceStatusAPIAddresss: addr = c.statusAPIAddress case overrideEndpoint != "": addr = overrideEndpoint default: addr = c.statusAPIAddress } if addr == "" { if addr, err = c.forwarder.getEndpointAddr(c.forwarder.statusPort); err != nil { return nil, err } } uri := fmt.Sprintf("%s://%s%s", c.statusAPIHTTPScheme, addr, relativeURI) res, err := c.statusAPIHTTPClient. WithTimeout(c.statusAPITimeout). Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrTiDBClientRequestFailed, distro.R().TiDB) if err != nil && c.forwarder.statusProxy.noAliveRemote.Load() { return nil, ErrNoAliveTiDB.NewWithNoMessage() } return res, err } // FIXME: SendGetRequest should be extracted, as a common method. func (c *Client) SendGetRequest(relativeURI string) ([]byte, error) { res, err := c.Get(relativeURI) if err != nil { return nil, err } return res.Body() } ================================================ FILE: pkg/tidb/forwarder.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tidb import ( "context" "fmt" "net" "strconv" "time" "github.com/cenkalti/backoff/v4" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/utils/topology" "github.com/pingcap/tidb-dashboard/util/distro" ) var ErrNoAliveTiDB = ErrNS.NewType("no_alive_tidb") type forwarderConfig struct { TiDBRetrieveTimeout time.Duration TiDBPollInterval time.Duration ProxyTimeout time.Duration ProxyCheckInterval time.Duration } type Forwarder struct { lifecycleCtx context.Context config *forwarderConfig etcdClient *clientv3.Client sqlProxy *proxy sqlPort int statusProxy *proxy statusPort int } func (f *Forwarder) Start(ctx context.Context) error { f.lifecycleCtx = ctx var err error if f.sqlProxy, err = f.createProxy(); err != nil { return err } if f.statusProxy, err = f.createProxy(); err != nil { return err } f.sqlPort = f.sqlProxy.port() f.statusPort = f.statusProxy.port() go f.pollingForTiDB() go f.sqlProxy.run(ctx) go f.statusProxy.run(ctx) return nil } func (f *Forwarder) createProxy() (*proxy, error) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, err } proxy := newProxy(l, nil, f.config.ProxyCheckInterval, f.config.ProxyTimeout) return proxy, nil } func (f *Forwarder) pollingForTiDB() { ebo := backoff.NewExponentialBackOff() ebo.MaxInterval = f.config.TiDBPollInterval bo := backoff.WithContext(ebo, f.lifecycleCtx) for { var allTiDB []topology.TiDBInfo err := backoff.Retry(func() error { var err error allTiDB, err = topology.FetchTiDBTopology(bo.Context(), f.etcdClient) return err }, bo) if err == nil { statusEndpoints := make(map[string]struct{}, len(allTiDB)) tidbEndpoints := make(map[string]struct{}, len(allTiDB)) for _, server := range allTiDB { if server.Status == topology.ComponentStatusUp { tidbEndpoints[net.JoinHostPort(server.IP, strconv.Itoa(int(server.Port)))] = struct{}{} statusEndpoints[net.JoinHostPort(server.IP, strconv.Itoa(int(server.StatusPort)))] = struct{}{} } } f.sqlProxy.updateRemotes(tidbEndpoints) f.statusProxy.updateRemotes(statusEndpoints) } select { case <-f.lifecycleCtx.Done(): return case <-time.After(f.config.TiDBPollInterval): } } } func (f *Forwarder) getEndpointAddr(port int) (string, error) { if f.statusProxy.noAliveRemote.Load() { log.Warn(fmt.Sprintf("Unable to resolve connection address since no alive %s instance", distro.R().TiDB)) return "", ErrNoAliveTiDB.NewWithNoMessage() } return fmt.Sprintf("127.0.0.1:%d", port), nil } func newForwarder(lc fx.Lifecycle, etcdClient *clientv3.Client) *Forwarder { f := &Forwarder{ config: &forwarderConfig{ TiDBRetrieveTimeout: time.Second, TiDBPollInterval: 5 * time.Second, ProxyTimeout: 3 * time.Second, ProxyCheckInterval: 2 * time.Second, }, etcdClient: etcdClient, } lc.Append(fx.Hook{ OnStart: f.Start, }) return f } ================================================ FILE: pkg/tidb/model/codec.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package model import ( "bytes" "encoding/binary" "github.com/pingcap/errors" ) var ( tablePrefix = []byte{'t'} metaPrefix = []byte{'m'} recordPrefix = []byte{'r'} ) const ( signMask uint64 = 0x8000000000000000 encGroupSize = 8 encMarker = byte(0xFF) encPad = byte(0x0) ) // Key represents high-level TiDB Key type. type Key []byte // KeyInfoBuffer can obtain the meta information of the TiDB Key. // It can be reused, thereby reducing memory applications. type KeyInfoBuffer []byte // DecodeKey obtains the KeyInfoBuffer from a TiDB Key. func (buf *KeyInfoBuffer) DecodeKey(key Key) (KeyInfoBuffer, error) { _, result, err := decodeBytes(key, *buf) if err != nil { *buf = (*buf)[:0] return nil, err } *buf = result return result, nil } // MetaOrTable checks if the key is a meta key or table key. // If the key is a meta key, it returns true and 0. // If the key is a table key, it returns false and table ID. // Otherwise, it returns false and 0. func (buf KeyInfoBuffer) MetaOrTable() (isMeta bool, tableID int64) { if bytes.HasPrefix(buf, metaPrefix) { return true, 0 } if bytes.HasPrefix(buf, tablePrefix) { _, tableID, _ := decodeInt(buf[len(tablePrefix):]) return false, tableID } return false, 0 } // RowInfo returns the row ID of the key, if the key is not table key, returns 0. func (buf KeyInfoBuffer) RowInfo() (isCommonHandle bool, rowID int64) { if !bytes.HasPrefix(buf, tablePrefix) || len(buf) < 19 || (buf[9] != '_' || buf[10] != 'r') { return } isCommonHandle = len(buf) != 19 if !isCommonHandle { _, rowID, _ = decodeInt(buf[11:19]) } return } // IndexInfo returns the row ID of the key, if the key is not table key, returns 0. func (buf KeyInfoBuffer) IndexInfo() (indexID int64) { if !bytes.HasPrefix(buf, tablePrefix) || len(buf) < 19 || (buf[9] != '_' || buf[10] != 'i') { return } _, indexID, _ = decodeInt(buf[11:19]) return } // GenerateTableKey generates a table split key. func (buf *KeyInfoBuffer) GenerateKey(tableID, rowID int64) Key { if tableID == 0 { return nil } data := *buf if data == nil { length := len(tablePrefix) + 8 if rowID != 0 { length = len(tablePrefix) + len(recordPrefix) + 8*2 } data = make([]byte, 0, length) } else { data = data[:0] } data = append(data, tablePrefix...) data = encodeInt(data, tableID) if rowID != 0 { data = append(data, recordPrefix...) data = encodeInt(data, rowID) } *buf = data return encodeBytes(data) } var pads = make([]byte, encGroupSize) // decodeBytes decodes bytes which is encoded by encodeBytes before, // returns the leftover bytes and decoded value if no error. func decodeBytes(b []byte, buf []byte) (rest []byte, result []byte, err error) { if buf == nil { buf = make([]byte, 0, len(b)) } buf = buf[:0] for { if len(b) < encGroupSize+1 { return nil, nil, errors.New("insufficient bytes to decode value") } groupBytes := b[:encGroupSize+1] group := groupBytes[:encGroupSize] marker := groupBytes[encGroupSize] padCount := encMarker - marker if padCount > encGroupSize { return nil, nil, errors.Errorf("invalid marker byte, group bytes %q", groupBytes) } realGroupSize := encGroupSize - padCount buf = append(buf, group[:realGroupSize]...) b = b[encGroupSize+1:] if padCount != 0 { // Check validity of padding bytes. for _, v := range group[realGroupSize:] { if v != encPad { return nil, nil, errors.Errorf("invalid padding byte, group bytes %q", groupBytes) } } break } } return b, buf, nil } // encodeBytes guarantees the encoded value is in ascending order for comparison, // encoding with the following rule: // // [group1][marker1]...[groupN][markerN] // group is 8 bytes slice which is padding with 0. // marker is `0xFF - padding 0 count` // // For example: // // [] -> [0, 0, 0, 0, 0, 0, 0, 0, 247] // [1, 2, 3] -> [1, 2, 3, 0, 0, 0, 0, 0, 250] // [1, 2, 3, 0] -> [1, 2, 3, 0, 0, 0, 0, 0, 251] // [1, 2, 3, 4, 5, 6, 7, 8] -> [1, 2, 3, 4, 5, 6, 7, 8, 255, 0, 0, 0, 0, 0, 0, 0, 0, 247] // // Refer: https://github.com/facebook/mysql-5.6/wiki/MyRocks-record-format#memcomparable-format func encodeBytes(data []byte) []byte { // Allocate more space to avoid unnecessary slice growing. // Assume that the byte slice size is about `(len(data) / encGroupSize + 1) * (encGroupSize + 1)` bytes, // that is `(len(data) / 8 + 1) * 9` in our implement. dLen := len(data) result := make([]byte, 0, (dLen/encGroupSize+1)*(encGroupSize+1)) for idx := 0; idx <= dLen; idx += encGroupSize { remain := dLen - idx padCount := 0 if remain >= encGroupSize { result = append(result, data[idx:idx+encGroupSize]...) } else { padCount = encGroupSize - remain result = append(result, data[idx:]...) result = append(result, pads[:padCount]...) } marker := encMarker - byte(padCount) result = append(result, marker) } return result } // decodeInt decodes value encoded by EncodeInt before. // It returns the leftover un-decoded slice, decoded value if no error. func decodeInt(b []byte) ([]byte, int64, error) { if len(b) < 8 { return nil, 0, errors.New("insufficient bytes to decode value") } u := binary.BigEndian.Uint64(b[:8]) v := decodeCmpUintToInt(u) b = b[8:] return b, v, nil } // encodeInt appends the encoded value to slice b and returns the appended slice. // encodeInt guarantees that the encoded value is in ascending order for comparison. func encodeInt(b []byte, v int64) []byte { var data [8]byte u := encodeIntToCmpUint(v) binary.BigEndian.PutUint64(data[:], u) return append(b, data[:]...) } func decodeCmpUintToInt(u uint64) int64 { return int64(u ^ signMask) } func encodeIntToCmpUint(v int64) uint64 { return uint64(v) ^ signMask } ================================================ FILE: pkg/tidb/model/codec_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package model import ( "testing" "github.com/pingcap/check" ) func TestTable(t *testing.T) { check.TestingT(t) } var _ = check.Suite(&testCodecSuite{}) type testCodecSuite struct{} func (s *testCodecSuite) TestDecodeBytes(c *check.C) { key := "abcdefghijklmnopqrstuvwxyz" for i := 0; i < len(key); i++ { _, k, err := decodeBytes(encodeBytes([]byte(key[:i])), nil) c.Assert(err, check.IsNil) c.Assert(string(k), check.Equals, key[:i]) } } func (s *testCodecSuite) TestTiDBInfo(c *check.C) { buf := new(KeyInfoBuffer) // no encode _, err := buf.DecodeKey([]byte("t\x80\x00\x00\x00\x00\x00\x00\xff")) c.Assert(err, check.NotNil) testcases := []struct { Key string IsMeta bool TableID int64 IsCommonHandle bool RowID int64 IndexID int64 }{ { "T\x00\x00\x00\x00\x00\x00\x00\xff", false, 0, false, 0, 0, }, { "t\x80\x00\x00\x00\x00\x00\xff", false, 0, false, 0, 0, }, { "t\x80\x00\x00\x00\x00\x00\x00\xff", false, 0xff, false, 0, 0, }, { "t\x80\x00\x00\x00\x00\x00\x00\xff_i\x01\x02", false, 0xff, false, 0, 0, }, { "t\x80\x00\x00\x00\x00\x00\x00\xff_i\x80\x00\x00\x00\x00\x00\x00\x02", false, 0xff, false, 0, 2, }, { "t\x80\x00\x00\x00\x00\x00\x00\xff_r\x80\x00\x00\x00\x00\x00\x00\x02", false, 0xff, false, 2, 0, }, { "t\x80\x00\x00\x00\x00\x00\x00\xff_r\x03\x80\x00\x00\x00\x00\x02\r\xaf\x03\x80\x00\x00\x00\x00\x00\x00\x03\x03\x80\x00\x00\x00\x00\x00\b%", false, 0xff, true, 0, 0, }, } for _, t := range testcases { key := encodeBytes([]byte(t.Key)) _, err := buf.DecodeKey(key) c.Assert(err, check.IsNil) isMeta, tableID := buf.MetaOrTable() c.Assert(isMeta, check.Equals, t.IsMeta) c.Assert(tableID, check.Equals, t.TableID) isCommonHandle, rowID := buf.RowInfo() c.Assert(isCommonHandle, check.Equals, t.IsCommonHandle) c.Assert(rowID, check.Equals, t.RowID) indexID := buf.IndexInfo() c.Assert(indexID, check.Equals, t.IndexID) } } ================================================ FILE: pkg/tidb/model/model.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package model // SchemaState is the state for schema elements. type SchemaState byte const ( // StateNone means this schema element is absent and can't be used. StateNone SchemaState = iota // StateDeleteOnly means we can only delete items for this schema element. StateDeleteOnly // StateWriteOnly means we can use any write operation on this schema element, // but outer can't read the changed data. StateWriteOnly // StateWriteReorganization means we are re-organizing whole data after write only state. StateWriteReorganization // StateDeleteReorganization means we are re-organizing whole data after delete only state. StateDeleteReorganization // StatePublic means this schema element is ok for all write and read operations. StatePublic ) // CIStr is case insensitive string. type CIStr struct { O string `json:"O"` // Original string. L string `json:"L"` // Lower case string. } // DBInfo provides meta data describing a DB. type DBInfo struct { ID int64 `json:"id"` Name CIStr `json:"db_name"` State SchemaState `json:"state"` } // IndexInfo provides meta data describing a DB index. // It corresponds to the statement `CREATE INDEX Name ON Table (Column);` // See https://dev.mysql.com/doc/refman/5.7/en/create-index.html type IndexInfo struct { ID int64 `json:"id"` Name CIStr `json:"idx_name"` } // PartitionDefinition defines a single partition. type PartitionDefinition struct { ID int64 `json:"id"` Name CIStr `json:"name"` } // PartitionInfo provides table partition info. type PartitionInfo struct { // User may already creates table with partition but table partition is not // yet supported back then. When Enable is true, write/read need use tid // rather than pid. Enable bool `json:"enable"` Definitions []*PartitionDefinition `json:"definitions"` } // TableInfo provides meta data describing a DB table. type TableInfo struct { ID int64 `json:"id"` Name CIStr `json:"name"` Indices []*IndexInfo `json:"index_info"` Partition *PartitionInfo `json:"partition"` Version *int64 `json:"version"` } // GetPartitionInfo returns the partition information. func (t *TableInfo) GetPartitionInfo() *PartitionInfo { if t.Partition != nil && t.Partition.Enable { return t.Partition } return nil } ================================================ FILE: pkg/tidb/proxy.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tidb import ( "context" "io" "net" "sync" "time" "github.com/pingcap/log" "go.uber.org/atomic" "go.uber.org/zap" ) type remote struct { addr string inactive *atomic.Bool } func (r *remote) isActive() bool { return !r.inactive.Load() } func (r *remote) becomeInactive() { r.inactive.Store(true) log.Debug("remote become inactive", zap.String("remote", r.addr)) } func (r *remote) checkAlive(timeout time.Duration) error { conn, err := net.DialTimeout("tcp", r.addr, timeout) if err != nil { return err } _ = conn.Close() r.inactive.Store(false) return nil } type proxy struct { listener net.Listener checkInterval time.Duration dialTimeout time.Duration noAliveRemote *atomic.Bool remotes sync.Map current *atomic.String } func newProxy(l net.Listener, endpoints map[string]string, checkInterval time.Duration, timeout time.Duration) *proxy { if checkInterval <= 0 { checkInterval = 2 * time.Second } if timeout <= 0 { timeout = 3 * time.Second } p := &proxy{ listener: l, remotes: sync.Map{}, dialTimeout: timeout, checkInterval: checkInterval, noAliveRemote: atomic.NewBool(len(endpoints) == 0), current: atomic.NewString(""), } for key, e := range endpoints { p.remotes.Store(key, &remote{addr: e, inactive: atomic.NewBool(true)}) } return p } func (p *proxy) port() int { return p.listener.Addr().(*net.TCPAddr).Port } func (p *proxy) updateRemotes(remotes map[string]struct{}) { if len(remotes) == 0 { p.remotes.Range(func(key, _ interface{}) bool { p.remotes.Delete(key) return true }) p.noAliveRemote.Store(true) return } // update or create new remote for addr := range remotes { if _, ok := p.remotes.Load(addr); !ok { log.Debug("proxy adds new remote", zap.String("remote", addr)) p.remotes.Store(addr, &remote{ addr: addr, inactive: atomic.NewBool(true), }) } } // remove old remote p.remotes.Range(func(key, _ interface{}) bool { addr := key.(string) if _, ok := remotes[addr]; !ok { log.Debug("proxy discards remote", zap.String("remote", addr)) p.remotes.Delete(key) } return true }) p.noAliveRemote.Store(false) } func (p *proxy) serve(in net.Conn) { out := p.pickActiveConn() if out == nil { log.Warn("no alive remote, drop incoming conn") _ = in.Close() return } deadline := time.Now().Add(10 * time.Minute) if err := in.SetDeadline(deadline); err != nil { log.Warn("input set deadline failed", zap.Error(err)) _ = in.Close() return } if err := out.SetReadDeadline(deadline); err != nil { log.Warn("output set deadline failed", zap.Error(err)) _ = in.Close() return } done := make(chan struct{}) // bidirectional copy go func() { defer func() { done <- struct{}{} }() // nolint io.Copy(in, out) }() // nolint io.Copy(out, in) <-done _ = out.Close() _ = in.Close() } func (p *proxy) pickActiveConn() (out net.Conn) { var ( err error picked *remote ) for { picked = p.pick() if picked == nil { break } out, err = net.DialTimeout("tcp", picked.addr, p.dialTimeout) if err == nil { break } p.current.Store("") picked.becomeInactive() log.Warn("remote become inactive", zap.String("remote", picked.addr)) } p.noAliveRemote.Store(out == nil) return } // pick returns an active remote if there is any. func (p *proxy) pick() *remote { var picked *remote if p.current.Load() == "" { p.remotes.Range(func(key, value interface{}) bool { id := key.(string) r := value.(*remote) if r.isActive() { p.current.Store(id) picked = r return false } return true }) } if picked != nil { return picked } curRemote := p.current.Load() if curRemote != "" { r, ok := p.remotes.Load(curRemote) if ok { picked = r.(*remote) } else { p.current.Store("") } } return picked } func (p *proxy) doCheck(ctx context.Context) { for { select { case <-ctx.Done(): return case <-time.After(p.checkInterval): p.remotes.Range(func(_, value interface{}) bool { rmt := value.(*remote) if rmt.isActive() { return true } go func(r *remote) { log.Debug("run remote check", zap.String("remote", r.addr)) if err := r.checkAlive(p.dialTimeout); err != nil { log.Warn("fail to recv activity from remote, stay inactive and wait to next checking round", zap.String("remote", r.addr), zap.Duration("interval", p.checkInterval), zap.Error(err)) } else { log.Debug("remote become active", zap.String("remote", r.addr)) } }(rmt) return true }) } } } func (p *proxy) run(ctx context.Context) { endpoints := make([]string, 0) p.remotes.Range(func(_, value interface{}) bool { r := value.(*remote) endpoints = append(endpoints, r.addr) return true }) log.Info("start serve requests to remotes", zap.String("endpoint", p.listener.Addr().String()), zap.Strings("remotes", endpoints)) go p.doCheck(ctx) defer p.listener.Close() // wait a check round before serve connections select { case <-ctx.Done(): return case <-time.After(p.checkInterval + time.Second): } // serve for { select { case <-ctx.Done(): return default: incoming, err := p.listener.Accept() if err != nil { log.Warn("got err from listener", zap.Error(err), zap.String("from", p.listener.Addr().String())) } else { go p.serve(incoming) } } } } ================================================ FILE: pkg/tidb/proxy_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tidb import ( "context" "fmt" "io" "net" "net/http" "net/http/httptest" "net/url" "strconv" "testing" "time" "github.com/stretchr/testify/assert" ) func TestProxy(t *testing.T) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() want := "hello proxy" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, err := w.Write([]byte(want)) if err != nil { t.Fatal(err) } })) defer server.Close() u, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } p := newProxy(l, map[string]string{"test": fmt.Sprintf("%s:%s", u.Hostname(), u.Port())}, 0, 0) ctx, cancel := context.WithCancel(context.Background()) go p.run(ctx) defer cancel() u.Host = l.Addr().String() res, err := http.Get(u.String()) if err != nil { t.Fatal(err) } got, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } assert.Equal(t, want, string(got)) } func TestProxyPick(t *testing.T) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() n := 3 responseData := "test" endpoints := make(map[string]string) picked := make(map[int]bool) servers := make(map[int]*httptest.Server) var currentPicked int for i := range n { idx := i server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { picked[idx] = true currentPicked = idx _, err := w.Write([]byte(responseData)) if err != nil { t.Fatal(err) } })) defer server.Close() u, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } key := strconv.Itoa(i) endpoint := fmt.Sprintf("%s:%s", u.Hostname(), u.Port()) endpoints[key] = endpoint servers[idx] = server } p := newProxy(l, endpoints, 0, 0) ctx, cancel := context.WithCancel(context.Background()) go p.run(ctx) defer cancel() for range n { client := &http.Client{} res, err := client.Get("http://" + l.Addr().String()) if err != nil { t.Fatal(err) } _, err = io.ReadAll(res.Body) if err != nil { t.Fatal(err) } // close conn manually to force proxy re-pick remote client.CloseIdleConnections() time.Sleep(time.Second) } // Always pick the same active remote assert.Equal(t, 1, len(picked)) ps := servers[currentPicked] if ps == nil { t.Fatal("Fail to get current picked server") } // Shutdown current server to see if we can pick a new one ps.Close() client := &http.Client{} target := "http://" + l.Addr().String() assertRespData(t, client, responseData, target) // Remove current picked from remotes and test out picking p.remotes.Delete(strconv.Itoa(currentPicked)) ps = servers[currentPicked] if ps == nil { t.Fatal("Fail to get current picked server") } ps.Close() // First conn will be dropped as the current picked remote is deleted _, err = client.Get(target) assert.NotNil(t, err) // Then pick a new remote assertRespData(t, client, responseData, target) } func assertRespData(t *testing.T, client *http.Client, expect string, target string) { res, err := client.Get(target) if err != nil { t.Fatal(err) } data, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } assert.Equal(t, expect, string(data)) } ================================================ FILE: pkg/tidb/tidb.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tidb import ( "github.com/joomcode/errorx" ) var ErrNS = errorx.NewNamespace("error.tidb") ================================================ FILE: pkg/tiflash/client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tiflash import ( "context" "fmt" "io" "net" "net/http" "strconv" "time" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/httpc" "github.com/pingcap/tidb-dashboard/util/distro" ) var ErrFlashClientRequestFailed = ErrNS.NewType("client_request_failed") const ( defaultTiFlashStatusAPITimeout = time.Second * 10 ) type Client struct { httpClient *httpc.Client httpScheme string lifecycleCtx context.Context timeout time.Duration } func NewTiFlashClient(lc fx.Lifecycle, httpClient *httpc.Client, config *config.Config) *Client { client := &Client{ httpClient: httpClient, httpScheme: config.GetClusterHTTPScheme(), lifecycleCtx: nil, timeout: defaultTiFlashStatusAPITimeout, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { client.lifecycleCtx = ctx return nil }, }) return client } func (c Client) GetHTTPScheme() string { return c.httpScheme } func (c Client) WithTimeout(timeout time.Duration) *Client { c.timeout = timeout return &c } func (c Client) AddRequestHeader(key, value string) *Client { c.httpClient = c.httpClient.CloneAndAddRequestHeader(key, value) return &c } func (c *Client) Get(host string, statusPort int, relativeURI string) (*httpc.Response, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(statusPort)), relativeURI) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrFlashClientRequestFailed, distro.R().TiFlash) } func (c *Client) SendGetRequest(host string, statusPort int, relativeURI string) ([]byte, error) { res, err := c.Get(host, statusPort, relativeURI) if err != nil { return nil, err } return res.Body() } func (c *Client) SendPostRequest(host string, statusPort int, relativeURI string, body io.Reader) ([]byte, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(statusPort)), relativeURI) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrFlashClientRequestFailed, distro.R().TiFlash) } ================================================ FILE: pkg/tiflash/tiflash.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tiflash import ( "github.com/joomcode/errorx" ) var ErrNS = errorx.NewNamespace("error.tiflash") ================================================ FILE: pkg/tikv/client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tikv import ( "context" "fmt" "io" "net" "net/http" "strconv" "time" "go.etcd.io/etcd/client/pkg/v3/transport" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/httpc" "github.com/pingcap/tidb-dashboard/util/distro" ) var ErrTiKVClientRequestFailed = ErrNS.NewType("client_request_failed") const ( defaultTiKVStatusAPITimeout = time.Second * 10 ) type Client struct { httpClient *httpc.Client httpScheme string lifecycleCtx context.Context timeout time.Duration tlsInfo *transport.TLSInfo } func NewTiKVClient(lc fx.Lifecycle, httpClient *httpc.Client, config *config.Config) *Client { client := &Client{ httpClient: httpClient, httpScheme: config.GetClusterHTTPScheme(), lifecycleCtx: nil, timeout: defaultTiKVStatusAPITimeout, tlsInfo: config.ClusterTLSInfo, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { client.lifecycleCtx = ctx return nil }, }) return client } func (c Client) WithTimeout(timeout time.Duration) *Client { c.timeout = timeout return &c } func (c Client) AddRequestHeader(key, value string) *Client { c.httpClient = c.httpClient.CloneAndAddRequestHeader(key, value) return &c } func (c *Client) GetHTTPScheme() string { return c.httpScheme } func (c *Client) GetTLSInfo() *transport.TLSInfo { return c.tlsInfo } func (c *Client) Get(host string, statusPort int, relativeURI string) (*httpc.Response, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(statusPort)), relativeURI) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrTiKVClientRequestFailed, distro.R().TiKV) } func (c *Client) SendGetRequest(host string, statusPort int, relativeURI string) ([]byte, error) { res, err := c.Get(host, statusPort, relativeURI) if err != nil { return nil, err } return res.Body() } func (c *Client) SendPostRequest(host string, statusPort int, relativeURI string, body io.Reader) ([]byte, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(statusPort)), relativeURI) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrTiKVClientRequestFailed, distro.R().TiKV) } ================================================ FILE: pkg/tikv/tikv.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tikv import ( "github.com/joomcode/errorx" ) var ErrNS = errorx.NewNamespace("error.tikv") ================================================ FILE: pkg/tiproxy/client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tiproxy import ( "context" "fmt" "io" "net" "net/http" "strconv" "time" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/httpc" "github.com/pingcap/tidb-dashboard/util/distro" ) var ErrTiProxyClientRequestFailed = ErrNS.NewType("client_request_failed") const ( defaultTiProxyStatusAPITimeout = time.Second * 10 ) type Client struct { httpClient *httpc.Client httpScheme string lifecycleCtx context.Context timeout time.Duration } func NewTiProxyClient(lc fx.Lifecycle, httpClient *httpc.Client, config *config.Config) *Client { client := &Client{ httpClient: httpClient, httpScheme: config.GetClusterHTTPScheme(), lifecycleCtx: nil, timeout: defaultTiProxyStatusAPITimeout, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { client.lifecycleCtx = ctx return nil }, }) return client } func (c Client) WithTimeout(timeout time.Duration) *Client { c.timeout = timeout return &c } func (c Client) AddRequestHeader(key, value string) *Client { c.httpClient = c.httpClient.CloneAndAddRequestHeader(key, value) return &c } func (c *Client) Get(host string, statusPort int, relativeURI string) (*httpc.Response, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(statusPort)), relativeURI) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrTiProxyClientRequestFailed, distro.R().TiProxy) } func (c *Client) SendGetRequest(host string, statusPort int, relativeURI string) ([]byte, error) { res, err := c.Get(host, statusPort, relativeURI) if err != nil { return nil, err } return res.Body() } func (c *Client) SendPostRequest(host string, statusPort int, relativeURI string, body io.Reader) ([]byte, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(statusPort)), relativeURI) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrTiProxyClientRequestFailed, distro.R().TiProxy) } ================================================ FILE: pkg/tiproxy/tiproxy.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tiproxy import ( "github.com/joomcode/errorx" ) var ErrNS = errorx.NewNamespace("error.tiproxy") ================================================ FILE: pkg/tso/client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tso import ( "context" "fmt" "io" "net" "net/http" "strconv" "time" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/httpc" "github.com/pingcap/tidb-dashboard/util/distro" ) var ErrTSOClientRequestFailed = ErrNS.NewType("client_request_failed") const ( defaultTSOStatusAPITimeout = time.Second * 10 ) type Client struct { httpClient *httpc.Client httpScheme string lifecycleCtx context.Context timeout time.Duration } func NewTSOClient(lc fx.Lifecycle, httpClient *httpc.Client, config *config.Config) *Client { client := &Client{ httpClient: httpClient, httpScheme: config.GetClusterHTTPScheme(), lifecycleCtx: nil, timeout: defaultTSOStatusAPITimeout, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { client.lifecycleCtx = ctx return nil }, }) return client } func (c Client) WithTimeout(timeout time.Duration) *Client { c.timeout = timeout return &c } func (c Client) AddRequestHeader(key, value string) *Client { c.httpClient = c.httpClient.CloneAndAddRequestHeader(key, value) return &c } func (c *Client) Get(host string, port int, relativeURI string) (*httpc.Response, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(port)), relativeURI) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrTSOClientRequestFailed, distro.R().TSO) } func (c *Client) SendGetRequest(host string, port int, relativeURI string) ([]byte, error) { res, err := c.Get(host, port, relativeURI) if err != nil { return nil, err } return res.Body() } func (c *Client) SendPostRequest(host string, port int, relativeURI string, body io.Reader) ([]byte, error) { uri := fmt.Sprintf("%s://%s%s", c.httpScheme, net.JoinHostPort(host, strconv.Itoa(port)), relativeURI) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrTSOClientRequestFailed, distro.R().TSO) } ================================================ FILE: pkg/tso/tso.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tso import ( "github.com/joomcode/errorx" ) var ErrNS = errorx.NewNamespace("error.tso") ================================================ FILE: pkg/uiserver/.gitignore ================================================ /embedded_assets_handler.go ================================================ FILE: pkg/uiserver/embedded_assets_rewriter.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. //go:build ui_server package uiserver import ( "net/http" "os" "path" "sync" "time" "go.uber.org/zap" "github.com/pingcap/log" "github.com/pingcap/tidb-dashboard/pkg/config" ) var once sync.Once func Assets(cfg *config.Config) http.FileSystem { once.Do(func() { exePath, err := os.Executable() if err != nil { log.Fatal("Failed to get executable path", zap.Error(err)) } distroResFolderPath := path.Join(path.Dir(exePath), distroResFolderName) RewriteAssets(assets, cfg, distroResFolderPath, func(fs http.FileSystem, f http.File, path, newContent string, bs []byte) { m := fs.(vfsgen۰FS) fi := f.(os.FileInfo) m[path] = &vfsgen۰CompressedFileInfo{ name: fi.Name(), modTime: time.Now(), uncompressedSize: int64(len(newContent)), compressedContent: bs, } }) }) return assets } ================================================ FILE: pkg/uiserver/empty_assets_handler.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. //go:build !ui_server package uiserver import ( "net/http" "github.com/pingcap/tidb-dashboard/pkg/config" ) func Assets(*config.Config) http.FileSystem { return nil } ================================================ FILE: pkg/uiserver/uiserver.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. package uiserver import ( "bytes" "compress/gzip" "encoding/base64" "encoding/json" "errors" "fmt" "html" "io" "io/ioutil" "net/http" "os" "path" "strings" "time" "github.com/pingcap/log" "github.com/shurcooL/httpgzip" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/util/distro" ) const ( distroResFolderName = "distro-res" ) type UpdateContentFunc func(fs http.FileSystem, oldFile http.File, path, newContent string, zippedBytes []byte) func RewriteAssets(fs http.FileSystem, cfg *config.Config, distroResFolderPath string, updater UpdateContentFunc) { if fs == nil { return } rewrite := func(assetPath string) { f, err := fs.Open(assetPath) if err != nil { log.Fatal("Asset not found", zap.String("path", assetPath), zap.Error(err)) } defer f.Close() bs, err := ioutil.ReadAll(f) if err != nil { log.Fatal("Failed to read asset", zap.String("path", assetPath), zap.Error(err)) } tmplText := string(bs) updated := strings.ReplaceAll(tmplText, "__PUBLIC_PATH_PREFIX__", html.EscapeString(cfg.PublicPathPrefix)) distroStrings, _ := json.Marshal(distro.R()) // this will never fail updated = strings.ReplaceAll(updated, "__DISTRO_STRINGS_RES__", base64.StdEncoding.EncodeToString(distroStrings)) updated = strings.ReplaceAll(updated, "__DISTRO_ASSETS_RES_TIMESTAMP__", fmt.Sprintf("%d", time.Now().Unix())) var b bytes.Buffer w := gzip.NewWriter(&b) if _, err := w.Write([]byte(updated)); err != nil { log.Fatal("Failed to zip asset", zap.Error(err)) } if err := w.Close(); err != nil { log.Fatal("Failed to zip asset", zap.Error(err)) } updater(fs, f, assetPath, updated, b.Bytes()) } rewrite("/index.html") rewrite("/diagnoseReport.html") if err := overrideDistroAssetsRes(fs, distroResFolderPath, updater); err != nil { log.Fatal("Failed to load distro assets res", zap.Error(err)) } } func overrideDistroAssetsRes(fs http.FileSystem, distroResFolderPath string, updater UpdateContentFunc) error { info, err := os.Stat(distroResFolderPath) if errors.Is(err, os.ErrNotExist) || !info.IsDir() { // just ignore if the folder doesn't exist or it's not a folder return nil } if err != nil { return err } // traverse files, err := ioutil.ReadDir(distroResFolderPath) if err != nil { return err } for _, file := range files { if err := overrideSingleDistroAsset(fs, distroResFolderPath, file.Name(), updater); err != nil { return err } } return nil } func overrideSingleDistroAsset(fs http.FileSystem, distroResFolderPath, assetName string, updater UpdateContentFunc) error { assetPath := path.Join("/", distroResFolderName, assetName) targetFile, err := fs.Open(assetPath) if err != nil { // has no target asset to be overried, skip return nil } defer targetFile.Close() assetFullPath := path.Join(distroResFolderPath, assetName) sourceFile, err := os.Open(assetFullPath) if err != nil { return err } defer sourceFile.Close() data, err := ioutil.ReadAll(sourceFile) if err != nil { return err } var b bytes.Buffer w := gzip.NewWriter(&b) if _, err := w.Write(data); err != nil { return err } if err := w.Close(); err != nil { return err } updater(fs, targetFile, assetPath, string(data), b.Bytes()) return nil } func Handler(root http.FileSystem) http.Handler { if root != nil { return httpgzip.FileServer(root, httpgzip.FileServerOptions{IndexHTML: true}) } return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, "Dashboard UI is not built. Use `UI=1 make`.\n") }) } ================================================ FILE: pkg/utils/fx.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "github.com/pingcap/log" "go.uber.org/fx" ) type FxPrinter func(string, ...interface{}) func (p FxPrinter) Printf(format string, args ...interface{}) { p(format, args...) } func NewFxPrinter() fx.Printer { return FxPrinter(log.S().Debugf) } ================================================ FILE: pkg/utils/grpc.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "time" "google.golang.org/grpc" "google.golang.org/grpc/backoff" "google.golang.org/grpc/keepalive" ) var ( DefaultGRPCConnectParams = grpc.ConnectParams{ Backoff: backoff.Config{ BaseDelay: 100 * time.Millisecond, // Default was 1 second Multiplier: 1.6, // Default Jitter: 0.2, // Default MaxDelay: 3 * time.Second, // Default was 120 seconds }, MinConnectTimeout: 5 * time.Second, // Default was 20 seconds } DefaultGRPCKeepaliveParams = keepalive.ClientParameters{ Time: 10 * time.Second, Timeout: 3 * time.Second, PermitWithoutStream: false, } ) var DefaultGRPCDialOptions = []grpc.DialOption{ grpc.WithConnectParams(DefaultGRPCConnectParams), } ================================================ FILE: pkg/utils/service_status.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "context" "net/http" "sync/atomic" "github.com/gin-gonic/gin" "go.uber.org/fx" ) type ServiceStatus int32 func NewServiceStatus() *ServiceStatus { return new(ServiceStatus) } func (s *ServiceStatus) IsRunning() bool { return atomic.LoadInt32((*int32)(s)) != 0 } func (s *ServiceStatus) Start() { atomic.StoreInt32((*int32)(s), 1) } func (s *ServiceStatus) Stop() { atomic.StoreInt32((*int32)(s), 0) } func (s *ServiceStatus) Register(lc fx.Lifecycle) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { select { case <-ctx.Done(): return ctx.Err() default: s.Start() return nil } }, OnStop: func(context.Context) error { s.Stop() return nil }, }) } func (s *ServiceStatus) MWHandleStopped(stoppedHandler gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { if !s.IsRunning() { stoppedHandler(c) c.Abort() return } c.Next() } } func (s *ServiceStatus) NewStatusAwareHandler(handler http.Handler, stoppedHandler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !s.IsRunning() { stoppedHandler.ServeHTTP(w, r) return } handler.ServeHTTP(w, r) }) } ================================================ FILE: pkg/utils/sys_schema.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package utils import ( "context" "fmt" "time" "github.com/ReneKroon/ttlcache/v2" "go.uber.org/fx" "gorm.io/gorm" ) const ( cacheTTL = 1 * time.Minute ) type SysSchema struct { cache *ttlcache.Cache } func ProvideSysSchema(lc fx.Lifecycle) *SysSchema { s := NewSysSchema() lc.Append(fx.Hook{ OnStop: func(_ context.Context) error { return s.Close() }, }) return s } func NewSysSchema() *SysSchema { c := ttlcache.NewCache() c.SkipTTLExtensionOnHit(true) return &SysSchema{ cache: c, } } func (c *SysSchema) Close() error { return c.cache.Close() } func (c *SysSchema) GetTableColumnNames(db *gorm.DB, tableName string) ([]string, error) { cnsCache, _ := c.cache.Get(tableName) if cnsCache != nil { return cnsCache.([]string), nil } cns := []string{} cs, err := fetchTableSchema(db, tableName) if err != nil { return nil, err } for _, c := range cs { cns = append(cns, c.Field) } err = c.cache.SetWithTTL(tableName, cns, cacheTTL) if err != nil { return nil, err } return cns, nil } type columnInfo struct { Field string `gorm:"column:Field" json:"field"` } func fetchTableSchema(db *gorm.DB, table string) ([]columnInfo, error) { var cs []columnInfo err := db.Raw(fmt.Sprintf("DESC %s", table)).Scan(&cs).Error if err != nil { return nil, err } return cs, nil } ================================================ FILE: pkg/utils/topology/models.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topology type ComponentStatus uint const ( ComponentStatusUnreachable ComponentStatus = 0 ComponentStatusUp ComponentStatus = 1 ComponentStatusTombstone ComponentStatus = 2 ComponentStatusOffline ComponentStatus = 3 ComponentStatusDown ComponentStatus = 4 ) type PDInfo struct { GitHash string `json:"git_hash"` Version string `json:"version"` IP string `json:"ip"` Port uint `json:"port"` DeployPath string `json:"deploy_path"` Status ComponentStatus `json:"status"` StartTimestamp int64 `json:"start_timestamp"` // Ts = 0 means unknown } type TiDBInfo struct { GitHash string `json:"git_hash"` Version string `json:"version"` IP string `json:"ip"` Port uint `json:"port"` DeployPath string `json:"deploy_path"` Status ComponentStatus `json:"status"` StatusPort uint `json:"status_port"` StartTimestamp int64 `json:"start_timestamp"` } type TiCDCInfo struct { ClusterName string `json:"cluster_name"` GitHash string `json:"git_hash"` Version string `json:"version"` IP string `json:"ip"` Port uint `json:"port"` DeployPath string `json:"deploy_path"` Status ComponentStatus `json:"status"` StatusPort uint `json:"status_port"` StartTimestamp int64 `json:"start_timestamp"` } type TiProxyInfo struct { GitHash string `json:"git_hash"` Version string `json:"version"` IP string `json:"ip"` Port uint `json:"port"` DeployPath string `json:"deploy_path"` Status ComponentStatus `json:"status"` StatusPort uint `json:"status_port"` StartTimestamp int64 `json:"start_timestamp"` } type TSOInfo struct { GitHash string `json:"git_hash"` Version string `json:"version"` IP string `json:"ip"` Port uint `json:"port"` DeployPath string `json:"deploy_path"` Status ComponentStatus `json:"status"` StartTimestamp int64 `json:"start_timestamp"` } type SchedulingInfo struct { GitHash string `json:"git_hash"` Version string `json:"version"` IP string `json:"ip"` Port uint `json:"port"` DeployPath string `json:"deploy_path"` Status ComponentStatus `json:"status"` StartTimestamp int64 `json:"start_timestamp"` } // Store may be a TiKV store or TiFlash store. type StoreInfo struct { GitHash string `json:"git_hash"` Version string `json:"version"` IP string `json:"ip"` Port uint `json:"port"` DeployPath string `json:"deploy_path"` Status ComponentStatus `json:"status"` StatusPort uint `json:"status_port"` Labels map[string]string `json:"labels"` StartTimestamp int64 `json:"start_timestamp"` } type StoreLabels struct { Address string `json:"address"` Labels map[string]string `json:"labels"` } type StoreLocation struct { LocationLabels []string `json:"location_labels"` Stores []StoreLabels `json:"stores"` } type StandardComponentInfo struct { IP string `json:"ip"` Port uint `json:"port"` } type AlertManagerInfo struct { StandardComponentInfo } type GrafanaInfo struct { StandardComponentInfo } type PrometheusInfo struct { StandardComponentInfo } ================================================ FILE: pkg/utils/topology/monitor.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topology import ( "context" "strconv" "strings" "time" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/distro" ) func FetchAlertManagerTopology(ctx context.Context, etcdClient *clientv3.Client) (*AlertManagerInfo, error) { i, err := fetchStandardComponentTopology(ctx, "alertmanager", etcdClient) if err != nil { return nil, err } if i == nil { return nil, nil } return &AlertManagerInfo{StandardComponentInfo: *i}, nil } func FetchGrafanaTopology(ctx context.Context, etcdClient *clientv3.Client) (*GrafanaInfo, error) { i, err := fetchStandardComponentTopology(ctx, "grafana", etcdClient) if err != nil { return nil, err } if i == nil { return nil, nil } return &GrafanaInfo{StandardComponentInfo: *i}, nil } func FetchPrometheusTopology(ctx context.Context, etcdClient *clientv3.Client) (*PrometheusInfo, error) { i, err := fetchStandardComponentTopology(ctx, "prometheus", etcdClient) if err != nil { return nil, err } if i == nil { return nil, nil } return &PrometheusInfo{StandardComponentInfo: *i}, nil } const ngMonitoringKeyPrefix = "/topology/ng-monitoring/" func FetchNgMonitoringTopology(ctx context.Context, etcdClient *clientv3.Client) (string, error) { ctx2, cancel := context.WithTimeout(ctx, defaultFetchTimeout) defer cancel() resp, err := etcdClient.Get(ctx2, ngMonitoringKeyPrefix, clientv3.WithPrefix()) if err != nil { return "", ErrEtcdRequestFailed.Wrap(err, "failed to get key %s from %s etcd", ngMonitoringKeyPrefix, distro.R().PD) } for _, kv := range resp.Kvs { key := string(kv.Key) if !strings.HasPrefix(key, ngMonitoringKeyPrefix) { continue } // RemainingKey looks like `ip:port/info` or `ip:port/ttl`. // Currently it only has `ip:port/ttl`. remainingKey := key[len(ngMonitoringKeyPrefix):] keyParts := strings.Split(remainingKey, "/") if len(keyParts) != 2 { log.Warn("Ignored invalid topology key", zap.String("component", "ng-monitoring"), zap.String("key", key)) continue } if keyParts[1] == "ttl" { _, err := parseNgMontioringAliveness(kv.Value) if err != nil { log.Warn("Ignored invalid NgMonitoring topology TTL entry", zap.String("key", key), zap.String("value", string(kv.Value)), zap.Error(err)) return "", err } // Currently ttl value is not refreshed periodically in the NgMonitoring side // if !alive { // log.Warn("Alive of NgMonitoring has expired, maybe local time in different hosts are not synchronized", // zap.String("key", key), // zap.String("value", string(kv.Value))) // return "", ErrInstanceNotAlive.NewWithNoMessage() // } return keyParts[0], nil } } return "", nil } func parseNgMontioringAliveness(value []byte) (bool, error) { unixTimestampNano, err := strconv.ParseUint(string(value), 10, 64) if err != nil { return false, ErrInvalidTopologyData.Wrap(err, "NgMonitoring TTL info parse failed") } t := time.Unix(0, int64(unixTimestampNano)) if time.Since(t) > time.Second*90 { return false, nil } return true, nil } ================================================ FILE: pkg/utils/topology/pd.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topology import ( "encoding/json" "fmt" "sort" "strings" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/pd" "github.com/pingcap/tidb-dashboard/util/distro" "github.com/pingcap/tidb-dashboard/util/netutil" ) func FetchPDTopology(pdClient *pd.Client) ([]PDInfo, error) { nodes := make([]PDInfo, 0) healthMap, err := fetchPDHealth(pdClient) if err != nil { return nil, err } data, err := pdClient.SendGetRequest("/members") if err != nil { return nil, err } ds := struct { Count int `json:"count"` Members []struct { GitHash string `json:"git_hash"` ClientUrls []string `json:"client_urls"` DeployPath string `json:"deploy_path"` BinaryVersion string `json:"binary_version"` MemberID uint64 `json:"member_id"` } `json:"members"` }{} err = json.Unmarshal(data, &ds) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s members API unmarshal failed", distro.R().PD) } for _, ds := range ds.Members { u := ds.ClientUrls[0] hostname, port, err := netutil.ParseHostAndPortFromAddressURL(u) if err != nil { continue } ts, err := fetchPDStartTimestamp(pdClient) if err != nil { log.Warn(fmt.Sprintf("Failed to fetch %s start timestamp", distro.R().PD), zap.String("targetPdNode", u), zap.Error(err)) ts = 0 } var storeStatus ComponentStatus if _, ok := healthMap[ds.MemberID]; ok { storeStatus = ComponentStatusUp } else { storeStatus = ComponentStatusUnreachable } nodes = append(nodes, PDInfo{ GitHash: ds.GitHash, Version: ds.BinaryVersion, IP: hostname, Port: port, DeployPath: ds.DeployPath, Status: storeStatus, StartTimestamp: ts, }) } sort.Slice(nodes, func(i, j int) bool { if nodes[i].IP < nodes[j].IP { return true } if nodes[i].IP > nodes[j].IP { return false } return nodes[i].Port < nodes[j].Port }) return nodes, nil } func fetchPDStartTimestamp(pdClient *pd.Client) (int64, error) { data, err := pdClient.SendGetRequest("/status") if err != nil { return 0, err } ds := struct { StartTimestamp int64 `json:"start_timestamp"` }{} err = json.Unmarshal(data, &ds) if err != nil { return 0, ErrInvalidTopologyData.Wrap(err, "%s status API unmarshal failed", distro.R().PD) } return ds.StartTimestamp, nil } func fetchPDHealth(pdClient *pd.Client) (map[uint64]struct{}, error) { data, err := pdClient.SendGetRequest("/health") if err != nil { return nil, err } var healths []struct { MemberID uint64 `json:"member_id"` Health bool `json:"health"` } err = json.Unmarshal(data, &healths) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s health API unmarshal failed", distro.R().PD) } memberHealth := map[uint64]struct{}{} for _, v := range healths { if v.Health { memberHealth[v.MemberID] = struct{}{} } } return memberHealth, nil } func fetchLocationLabels(pdClient *pd.Client) ([]string, error) { data, err := pdClient.SendGetRequest("/config/replicate") if err != nil { return nil, err } var replicateConfig struct { LocationLabels string `json:"location-labels"` } err = json.Unmarshal(data, &replicateConfig) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s config/replicate API unmarshal failed", distro.R().PD) } labels := strings.Split(replicateConfig.LocationLabels, ",") return labels, nil } ================================================ FILE: pkg/utils/topology/scheduling.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topology import ( "context" "encoding/json" "sort" "github.com/pingcap/tidb-dashboard/pkg/pd" "github.com/pingcap/tidb-dashboard/util/distro" "github.com/pingcap/tidb-dashboard/util/netutil" ) func FetchSchedulingTopology(_ context.Context, pdClient *pd.Client) ([]SchedulingInfo, error) { nodes := make([]SchedulingInfo, 0) data, err := pdClient.WithoutPrefix().SendGetRequest("/pd/api/v2/ms/members/scheduling") if err != nil { return nil, err } ds := []struct { ServiceAddr string `json:"service-addr"` Version string `json:"version"` GitHash string `json:"git-hash"` DeployPath string `json:"deploy-path"` StartTimestamp int64 `json:"start-timestamp"` }{} err = json.Unmarshal(data, &ds) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s members API unmarshal failed", distro.R().Scheduling) } for _, ds := range ds { u := ds.ServiceAddr hostname, port, err := netutil.ParseHostAndPortFromAddressURL(u) if err != nil { continue } nodes = append(nodes, SchedulingInfo{ GitHash: ds.GitHash, Version: ds.Version, IP: hostname, Port: port, DeployPath: ds.DeployPath, Status: ComponentStatusUp, StartTimestamp: ds.StartTimestamp, }) } sort.Slice(nodes, func(i, j int) bool { if nodes[i].IP < nodes[j].IP { return true } if nodes[i].IP > nodes[j].IP { return false } return nodes[i].Port < nodes[j].Port }) return nodes, nil } ================================================ FILE: pkg/utils/topology/store.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topology import ( "encoding/json" "sort" "strings" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/pkg/pd" "github.com/pingcap/tidb-dashboard/util/distro" "github.com/pingcap/tidb-dashboard/util/netutil" ) // FetchStoreTopology returns TiKV info and TiFlash info. func FetchStoreTopology(pdClient *pd.Client) ([]StoreInfo, []StoreInfo, error) { stores, err := fetchStores(pdClient) if err != nil { return nil, nil, err } tiKVStores := make([]store, 0, len(stores)) tiFlashStores := make([]store, 0, len(stores)) for _, store := range stores { isTiFlash := false for _, label := range store.Labels { if label.Key == "engine" && (label.Value == "tiflash" || label.Value == "tiflash_compute") { isTiFlash = true } } if isTiFlash { tiFlashStores = append(tiFlashStores, store) } else { tiKVStores = append(tiKVStores, store) } } return buildStoreTopology(tiKVStores), buildStoreTopology(tiFlashStores), nil } func FetchStoreLocation(pdClient *pd.Client) (*StoreLocation, error) { locationLabels, err := fetchLocationLabels(pdClient) if err != nil { return nil, err } stores, err := fetchStores(pdClient) if err != nil { return nil, err } nodes := make([]StoreLabels, 0, len(stores)) for _, s := range stores { node := StoreLabels{ Address: s.Address, Labels: map[string]string{}, } for _, l := range s.Labels { node.Labels[l.Key] = l.Value } nodes = append(nodes, node) } storeLocation := StoreLocation{ LocationLabels: locationLabels, Stores: nodes, } return &storeLocation, nil } func buildStoreTopology(stores []store) []StoreInfo { nodes := make([]StoreInfo, 0, len(stores)) for _, v := range stores { hostname, port, err := netutil.ParseHostAndPortFromAddress(v.Address) if err != nil { log.Warn("Failed to parse store address", zap.Any("store", v)) continue } _, statusPort, err := netutil.ParseHostAndPortFromAddress(v.StatusAddress) if err != nil { log.Warn("Failed to parse store status address", zap.Any("store", v)) continue } // In current TiKV, it's version may not start with 'v', // so we may need to add a prefix 'v' for it. version := strings.Trim(v.Version, "\n ") if !strings.HasPrefix(version, "v") { version = "v" + version } node := StoreInfo{ Version: version, IP: hostname, Port: port, GitHash: v.GitHash, DeployPath: v.DeployPath, Status: parseStoreState(v.StateName), StatusPort: statusPort, Labels: map[string]string{}, StartTimestamp: v.StartTimestamp, } for _, v := range v.Labels { node.Labels[v.Key] = v.Value } nodes = append(nodes, node) } return nodes } type store struct { Address string `json:"address"` ID int `json:"id"` Labels []struct { Key string `json:"key"` Value string `json:"value"` } `json:"labels"` StateName string `json:"state_name"` Version string `json:"version"` StatusAddress string `json:"status_address"` GitHash string `json:"git_hash"` DeployPath string `json:"deploy_path"` StartTimestamp int64 `json:"start_timestamp"` } func fetchStores(pdClient *pd.Client) ([]store, error) { data, err := pdClient.SendGetRequest("/stores") if err != nil { return nil, err } storeResp := struct { Count int `json:"count"` Stores []struct { Store store } `json:"stores"` }{} err = json.Unmarshal(data, &storeResp) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s stores API unmarshal failed", distro.R().PD) } ret := make([]store, 0, storeResp.Count) for _, s := range storeResp.Stores { ret = append(ret, s.Store) } sort.Slice(ret, func(i, j int) bool { return ret[i].Address < ret[j].Address }) return ret, nil } func parseStoreState(state string) ComponentStatus { state = strings.Trim(strings.ToLower(state), "\n ") switch state { case "up": return ComponentStatusUp case "tombstone": return ComponentStatusTombstone case "offline": return ComponentStatusOffline case "down": return ComponentStatusDown case "disconnected": return ComponentStatusUnreachable default: return ComponentStatusUnreachable } } ================================================ FILE: pkg/utils/topology/ticdc.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topology import ( "context" "encoding/json" "fmt" "sort" "strings" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/distro" "github.com/pingcap/tidb-dashboard/util/netutil" ) // TODO: refactor this with topology prefix since it is compatible with other components. const ( ticdcTopologyKeyPrefix = "/topology/ticdc/" ) func FetchTiCDCTopology(ctx context.Context, etcdClient *clientv3.Client) ([]TiCDCInfo, error) { ctx2, cancel := context.WithTimeout(ctx, defaultFetchTimeout) defer cancel() resp, err := etcdClient.Get(ctx2, ticdcTopologyKeyPrefix, clientv3.WithPrefix()) if err != nil { return nil, ErrEtcdRequestFailed.Wrap(err, "failed to get key %s from %s etcd", ticdcTopologyKeyPrefix, distro.R().PD) } nodes := make([]TiCDCInfo, 0) for _, kv := range resp.Kvs { key := string(kv.Key) if !strings.HasPrefix(key, ticdcTopologyKeyPrefix) { continue } // parse default keys := strings.TrimPrefix(key, ticdcTopologyKeyPrefix) clusterName := strings.Split(keys, "/")[0] nodeInfo, err := parseTiCDCInfo(clusterName, kv.Value) if err != nil { log.Warn(fmt.Sprintf("Ignored invalid %s topology info entry", distro.R().TiCDC), zap.String("key", key), zap.String("value", string(kv.Value)), zap.Error(err)) continue } nodes = append(nodes, *nodeInfo) } sort.Slice(nodes, func(i, j int) bool { if nodes[i].IP < nodes[j].IP { return true } if nodes[i].IP > nodes[j].IP { return false } return nodes[i].Port < nodes[j].Port }) return nodes, nil } func parseTiCDCInfo(clusterName string, value []byte) (*TiCDCInfo, error) { ds := struct { ID string `json:"id"` Address string `json:"address"` Version string `json:"version"` GitHash string `json:"git-hash"` DeployPath string `json:"deploy-path"` StartTimestamp int64 `json:"start-timestamp"` }{} err := json.Unmarshal(value, &ds) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s info unmarshal failed", distro.R().TiCDC) } hostname, port, err := netutil.ParseHostAndPortFromAddress(ds.Address) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s info address parse failed", distro.R().TiCDC) } return &TiCDCInfo{ ClusterName: clusterName, GitHash: ds.GitHash, Version: ds.Version, IP: hostname, Port: port, DeployPath: ds.DeployPath, Status: ComponentStatusUp, StatusPort: port, StartTimestamp: ds.StartTimestamp, }, nil } ================================================ FILE: pkg/utils/topology/tidb.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topology import ( "context" "encoding/json" "fmt" "sort" "strconv" "strings" "time" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/distro" "github.com/pingcap/tidb-dashboard/util/netutil" ) const ( tidbTopologyKeyPrefix = "/topology/tidb/" keyspaceNameKeyPrefix = "/keyspaces/tidb" ) func getAliveNodesAndInfos(ctx context.Context, etcdClient *clientv3.Client, keyPrefix string) (map[string]struct{}, map[string]*TiDBInfo, error) { ctx2, cancel := context.WithTimeout(ctx, defaultFetchTimeout) defer cancel() resp, err := etcdClient.Get(ctx2, keyPrefix, clientv3.WithPrefix()) if err != nil { return nil, nil, ErrEtcdRequestFailed.Wrap(err, "failed to get key %s from %s etcd", keyPrefix, distro.R().PD) } nodesAlive := make(map[string]struct{}, len(resp.Kvs)) nodesInfo := make(map[string]*TiDBInfo, len(resp.Kvs)) for _, kv := range resp.Kvs { key := string(kv.Key) if !strings.HasPrefix(key, keyPrefix) { continue } // remainingKey looks like `ip:port/info` or `ip:port/ttl`. remainingKey := key[len(keyPrefix):] keyParts := strings.Split(remainingKey, "/") if len(keyParts) != 2 { log.Warn("Ignored invalid topology key", zap.String("component", distro.R().TiDB), zap.String("key", key)) continue } switch keyParts[1] { case "info": node, err := parseTiDBInfo(keyParts[0], kv.Value) if err == nil { nodesInfo[keyParts[0]] = node } else { log.Warn(fmt.Sprintf("Ignored invalid %s topology info entry", distro.R().TiDB), zap.String("key", key), zap.String("value", string(kv.Value)), zap.Error(err)) } case "ttl": alive, err := parseTiDBAliveness(kv.Value) if err == nil { nodesAlive[keyParts[0]] = struct{}{} if !alive { log.Warn(fmt.Sprintf("Alive of %s has expired, maybe local time in different hosts are not synchronized", distro.R().TiDB), zap.String("key", key), zap.String("value", string(kv.Value))) } } else { log.Warn(fmt.Sprintf("Ignored invalid %s topology TTL entry", distro.R().TiDB), zap.String("key", key), zap.String("value", string(kv.Value)), zap.Error(err)) } } } return nodesAlive, nodesInfo, nil } func getAliveNodesAndInfoWithPrefix(ctx context.Context, etcdClient *clientv3.Client) (map[string]struct{}, map[string]*TiDBInfo, error) { childCtx, cancel := context.WithTimeout(ctx, defaultFetchTimeout) defer cancel() resp, err := etcdClient.Get(childCtx, keyspaceNameKeyPrefix, clientv3.WithPrefix()) if err != nil { return nil, nil, ErrEtcdRequestFailed.Wrap(err, "failed to get key %s from %s etcd", keyspaceNameKeyPrefix, distro.R().PD) } idMap := make(map[string]struct{}) for _, kv := range resp.Kvs { // layout: /keyspaces/tidb//topology/tidb/... rest := strings.TrimPrefix(string(kv.Key), keyspaceNameKeyPrefix) // rest: /topology/tidb/... parts := strings.SplitN(rest, "/", 3) if len(parts) >= 2 && strings.HasPrefix(parts[2], "topology/tidb/") { id := parts[1] idMap[id] = struct{}{} } } retryTimes := 3 nodesAlive := make(map[string]struct{}) nodesInfo := make(map[string]*TiDBInfo) for id := range idMap { keyPrefix := fmt.Sprintf("%s/%s/topology/tidb/", keyspaceNameKeyPrefix, id) var ( nodesAlive0 map[string]struct{} nodesInfo0 map[string]*TiDBInfo err error ) for i := 1; i <= retryTimes; i++ { nodesAlive0, nodesInfo0, err = getAliveNodesAndInfos(childCtx, etcdClient, keyPrefix) if err != nil { log.Warn("Failed to get TiDB topology nodes", zap.String("keyPrefix", keyPrefix), zap.Int("retry", i), zap.Error(err)) if i == retryTimes { return nil, nil, err } continue } break } for addr := range nodesAlive0 { nodesAlive[addr] = struct{}{} } for addr, info := range nodesInfo0 { if _, exists := nodesInfo[addr]; exists { // If the same address appears, we keep the first one. continue } nodesInfo[addr] = info } } return nodesAlive, nodesInfo, nil } func FetchTiDBTopology(ctx context.Context, etcdClient *clientv3.Client) ([]TiDBInfo, error) { nodesAlive, nodesInfo, err := getAliveNodesAndInfos(ctx, etcdClient, tidbTopologyKeyPrefix) if err != nil { return nil, err } if len(nodesAlive) == 0 { nodesAlive, nodesInfo, err = getAliveNodesAndInfoWithPrefix(ctx, etcdClient) if err != nil { return nil, err } } nodes := make([]TiDBInfo, 0) for addr, info := range nodesInfo { if _, ok := nodesAlive[addr]; ok { info.Status = ComponentStatusUp } nodes = append(nodes, *info) } sort.Slice(nodes, func(i, j int) bool { if nodes[i].IP < nodes[j].IP { return true } if nodes[i].IP > nodes[j].IP { return false } return nodes[i].Port < nodes[j].Port }) return nodes, nil } func parseTiDBInfo(address string, value []byte) (*TiDBInfo, error) { ds := struct { Version string `json:"version"` GitHash string `json:"git_hash"` StatusPort uint `json:"status_port"` DeployPath string `json:"deploy_path"` StartTimestamp int64 `json:"start_timestamp"` }{} err := json.Unmarshal(value, &ds) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s info unmarshal failed", distro.R().TiDB) } hostname, port, err := netutil.ParseHostAndPortFromAddress(address) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s info address parse failed", distro.R().TiDB) } return &TiDBInfo{ GitHash: ds.GitHash, Version: ds.Version, IP: hostname, Port: port, DeployPath: ds.DeployPath, Status: ComponentStatusUnreachable, StatusPort: ds.StatusPort, StartTimestamp: ds.StartTimestamp, }, nil } func parseTiDBAliveness(value []byte) (bool, error) { unixTimestampNano, err := strconv.ParseUint(string(value), 10, 64) if err != nil { return false, ErrInvalidTopologyData.Wrap(err, "%s TTL info parse failed", distro.R().TiDB) } t := time.Unix(0, int64(unixTimestampNano)) if time.Since(t) > time.Second*45 { return false, nil } return true, nil } ================================================ FILE: pkg/utils/topology/tiproxy.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topology import ( "context" "encoding/json" "fmt" "sort" "strconv" "strings" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/distro" ) const tiproxyTopologyKeyPrefix = "/topology/tiproxy/" func FetchTiProxyTopology(ctx context.Context, etcdClient *clientv3.Client) ([]TiProxyInfo, error) { ctx2, cancel := context.WithTimeout(ctx, defaultFetchTimeout) defer cancel() resp, err := etcdClient.Get(ctx2, tiproxyTopologyKeyPrefix, clientv3.WithPrefix()) if err != nil { return nil, ErrEtcdRequestFailed.Wrap(err, "failed to get key %s from %s etcd", tiproxyTopologyKeyPrefix, distro.R().PD) } nodesAlive := make(map[string]struct{}, len(resp.Kvs)) nodesInfo := make(map[string]*TiProxyInfo, len(resp.Kvs)) for _, kv := range resp.Kvs { key := string(kv.Key) if !strings.HasPrefix(key, tiproxyTopologyKeyPrefix) { continue } // remainingKey looks like `ip:port/info` or `ip:port/ttl`. remainingKey := key[len(tiproxyTopologyKeyPrefix):] keyParts := strings.Split(remainingKey, "/") if len(keyParts) != 2 { log.Warn("Ignored invalid topology key", zap.String("component", distro.R().TiProxy), zap.String("key", key)) continue } switch keyParts[1] { case "info": node, err := parseTiProxyInfo(keyParts[0], kv.Value) if err == nil { nodesInfo[keyParts[0]] = node } else { log.Warn(fmt.Sprintf("Ignored invalid %s topology info entry", distro.R().TiProxy), zap.String("key", key), zap.String("value", string(kv.Value)), zap.Error(err)) } case "ttl": alive, err := parseTiDBAliveness(kv.Value) if err == nil { nodesAlive[keyParts[0]] = struct{}{} if !alive { log.Warn(fmt.Sprintf("Alive of %s has expired, maybe local time in different hosts are not synchronized", distro.R().TiProxy), zap.String("key", key), zap.String("value", string(kv.Value))) } } else { log.Warn(fmt.Sprintf("Ignored invalid %s topology TTL entry", distro.R().TiProxy), zap.String("key", key), zap.String("value", string(kv.Value)), zap.Error(err)) } } } nodes := make([]TiProxyInfo, 0) for addr, info := range nodesInfo { if _, ok := nodesAlive[addr]; ok { info.Status = ComponentStatusUp } nodes = append(nodes, *info) } sort.Slice(nodes, func(i, j int) bool { if nodes[i].IP < nodes[j].IP { return true } if nodes[i].IP > nodes[j].IP { return false } return nodes[i].Port < nodes[j].Port }) return nodes, nil } func parseTiProxyInfo(_ string, value []byte) (*TiProxyInfo, error) { ds := struct { GitHash string `json:"git_hash"` Version string `json:"version"` IP string `json:"ip"` Port string `json:"port"` DeployPath string `json:"deploy_path"` StatusPort string `json:"status_port"` StartTimestamp int64 `json:"start_timestamp"` }{} err := json.Unmarshal(value, &ds) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s info unmarshal failed", distro.R().TiProxy) } port, err := strconv.ParseUint(ds.Port, 10, 64) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s port parse failed", distro.R().TiProxy) } statusPort, err := strconv.ParseUint(ds.StatusPort, 10, 64) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s port parse failed", distro.R().TiProxy) } return &TiProxyInfo{ GitHash: ds.GitHash, Version: ds.Version, IP: ds.IP, Port: uint(port), DeployPath: ds.DeployPath, Status: ComponentStatusUnreachable, StatusPort: uint(statusPort), StartTimestamp: ds.StartTimestamp, }, nil } ================================================ FILE: pkg/utils/topology/topology.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topology import ( "context" "encoding/json" "time" "github.com/joomcode/errorx" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/distro" ) var ( ErrNS = errorx.NewNamespace("error.topology") ErrEtcdRequestFailed = ErrNS.NewType("pd_etcd_request_failed") ErrInvalidTopologyData = ErrNS.NewType("invalid_topology_data") ErrInstanceNotAlive = ErrNS.NewType("instance_not_alive") ) const defaultFetchTimeout = 2 * time.Second func fetchStandardComponentTopology(ctx context.Context, componentName string, etcdClient *clientv3.Client) (*StandardComponentInfo, error) { ctx2, cancel := context.WithTimeout(ctx, defaultFetchTimeout) defer cancel() key := "/topology/" + componentName resp, err := etcdClient.Get(ctx2, key, clientv3.WithPrefix()) if err != nil { return nil, ErrEtcdRequestFailed.Wrap(err, "failed to get key %s from %s etcd", key, distro.R().PD) } if resp.Count == 0 { return nil, nil } info := StandardComponentInfo{} kv := resp.Kvs[0] if err = json.Unmarshal(kv.Value, &info); err != nil { log.Warn("Failed to unmarshal topology value", zap.String("key", string(kv.Key)), zap.String("value", string(kv.Value))) return nil, nil } return &info, nil } ================================================ FILE: pkg/utils/topology/tso.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topology import ( "context" "encoding/json" "sort" "github.com/pingcap/tidb-dashboard/pkg/pd" "github.com/pingcap/tidb-dashboard/util/distro" "github.com/pingcap/tidb-dashboard/util/netutil" ) func FetchTSOTopology(_ context.Context, pdClient *pd.Client) ([]TSOInfo, error) { nodes := make([]TSOInfo, 0) data, err := pdClient.WithoutPrefix().SendGetRequest("/pd/api/v2/ms/members/tso") if err != nil { return nil, err } ds := []struct { ServiceAddr string `json:"service-addr"` Version string `json:"version"` GitHash string `json:"git-hash"` DeployPath string `json:"deploy-path"` StartTimestamp int64 `json:"start-timestamp"` }{} err = json.Unmarshal(data, &ds) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "%s members API unmarshal failed", distro.R().TSO) } for _, ds := range ds { u := ds.ServiceAddr hostname, port, err := netutil.ParseHostAndPortFromAddressURL(u) if err != nil { continue } nodes = append(nodes, TSOInfo{ GitHash: ds.GitHash, Version: ds.Version, IP: hostname, Port: port, DeployPath: ds.DeployPath, Status: ComponentStatusUp, StartTimestamp: ds.StartTimestamp, }) } sort.Slice(nodes, func(i, j int) bool { if nodes[i].IP < nodes[j].IP { return true } if nodes[i].IP > nodes[j].IP { return false } return nodes[i].Port < nodes[j].Port }) return nodes, nil } ================================================ FILE: pkg/utils/version/fips.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. //go:build boringcrypto package version import _ "crypto/tls/fipsonly" ================================================ FILE: pkg/utils/version/version.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package version import ( "fmt" "strings" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/distro" ) type Info struct { InternalVersion string `json:"internal_version"` Standalone string `json:"standalone"` PDVersion string `json:"pd_version"` BuildTime string `json:"build_time"` BuildGitHash string `json:"build_git_hash"` } // Zero-value version information. It will be overwritten by LDFLAGS. var ( InternalVersion = "0.0.0" Standalone = "Yes" // Unknown, Yes or No PDVersion = "0.0.0" BuildTime = "1970-01-01 00:00:00" BuildGitHash = "Unknown" ) func Print() { log.Info(fmt.Sprintf("%s Dashboard started", distro.R().TiDB), zap.String("internal-version", InternalVersion), zap.String("standalone", Standalone), zap.String(fmt.Sprintf("%s-version", strings.ToLower(distro.R().PD)), PDVersion), zap.String("build-time", BuildTime), zap.String("build-git-hash", BuildGitHash)) } func GetInfo() *Info { return &Info{ InternalVersion: InternalVersion, Standalone: Standalone, PDVersion: PDVersion, BuildTime: BuildTime, BuildGitHash: BuildGitHash, } } func PrintStandaloneModeInfo() { fmt.Println("Internal Version: ", InternalVersion) fmt.Println("Git Commit Hash: ", BuildGitHash) fmt.Println("UTC Build Time: ", BuildTime) } ================================================ FILE: release-version ================================================ # This file specifies the TiDB Dashboard internal version, which will be printed in `--version` # and UI. In release branch, changing this file will result in publishing a new version and tag. nightly ================================================ FILE: scripts/_inc/download_tools.sh ================================================ #!/usr/bin/env bash # Download tools for running the integration test set -euo pipefail PROJECT_DIR="$(dirname "$0")/.." BIN="${PROJECT_DIR}/bin" download_tools() { echo "+ Download tools" download_tiup mkdir -p $BIN if [ ! -e "$BIN/toolkit.tar.gz" ]; then echo " - Downloading toolkit..." curl -L -f -o "$BIN/toolkit.tar.gz" "https://download.pingcap.org/tidb-toolkit-v6.0.0-linux-amd64.tar.gz" fi if [ ! -e "$BIN/dumpling" ]; then tar -x -f "$BIN/toolkit.tar.gz" -C "$BIN/" tidb-toolkit-v6.0.0-linux-amd64/bin/dumpling mv "$BIN"/tidb-toolkit-v6.0.0-linux-amd64/bin/dumpling "$BIN/dumpling" fi echo "+ All binaries are now available." } download_tiup() { if ! command -v tiup >/dev/null 2>&1; then echo " - Downloading tiup..." curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh fi } ================================================ FILE: scripts/_inc/run_services.sh ================================================ #!/usr/bin/env bash set -euo pipefail INTEGRATION_LOG_PATH=/tmp/dashboard-integration-test.log INTEGRATION_PID_LOG_PATH=/tmp/dashboard-integration-test-pid.log TIUP_BIN_DIR=$HOME/.tiup/bin PROJECT_DIR="$(dirname "$0")/.." BIN="${PROJECT_DIR}/bin" start_tidb() { echo "+ Waiting for TiDB start, for at most 15 min..." rm -rf $INTEGRATION_LOG_PATH TIDB_VERSION=${1:-latest} $TIUP_BIN_DIR/tiup playground $TIDB_VERSION > $INTEGRATION_LOG_PATH & echo $! > $INTEGRATION_PID_LOG_PATH ensure_tidb echo " - Start TiDB@$TIDB_VERSION Success!" } stop_tidb() { echo "+ Waiting for TiDB teardown..." kill `cat $INTEGRATION_PID_LOG_PATH` } ensure_tidb() { i=1 while ! grep "TiDB Playground Cluster is started" $INTEGRATION_LOG_PATH; do i=$((i+1)) if [ "$i" -gt 90 ]; then echo 'Failed to start TiDB' return 1 fi sleep 10 done } dump_schema() { if [ ${1:-""} = "" ]; then echo "Please specify the 'database-name.table-name' to dump" echo "Usage: tests/dump.sh database-name.table-name" return 1 fi if [ -e "$BIN/dumpling" ]; then echo "+ Start dump schema..." $BIN/dumpling -u root -P 4000 -h 127.0.0.1 --filetype sql --no-data -o "${PROJECT_DIR}/tests/schema" -T $1 echo " - Dump success!" else echo "Tool $BIN/dumpling not exist" return 1 fi } ================================================ FILE: scripts/create_release_tag.js ================================================ // This scripts is used to create a new release tag for the current release branch // when we need to submit a PR to PD for updating the tidb-dashboard. // After this tag is pushed, it will trigger the CI to build a new tidb-dashboard release version. // Then we can run the manual-create-pd-pr.yaml workflow in the GitHub to create a PR to PD. const { execSync } = require('child_process'); const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // rl.on('close', () => { // console.log('exit') // process.exit(0); // }); function getGitBranch() { // master, release-7.6 return execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); } function getGitShortSha() { // eb69e4fd return execSync('git rev-parse --short HEAD').toString().trim(); } function getGitLatestTag() { // v7.6.0-alpha, v7.6.0-, v7.6.0, v7.6.1-, v7.6.1 // v7.6.0-alpha-3-g383cf602, v7.6.0--3-g383cf602, v7.6.0-3-g383cf602, v7.6.1--3-g383cf602, v7.6.1-3-g383cf602 return execSync('git describe --tags --dirty --always').toString().trim(); } function question(nextTag) { rl.question(`Do you want to create tag ${nextTag}? (y or enter continue, others exit): `, (answer) => { if (answer.toLowerCase() === 'y' || answer.toLowerCase() === '') { execSync(`git tag ${nextTag}`); console.log(`Created tag ${nextTag}`) process.exit(0); } else { console.log('Cancel create tag') process.exit(0); } }) } function createReleaseTag() { const branch = getGitBranch(); const branchVer = branch.replace('release-', ''); const latestTag = getGitLatestTag().replace('-fips', ''); if (!latestTag.startsWith(`v${branchVer}.`)) { console.error(`Err: latest tag ${latestTag} doesn't match the branch ${branch}, you need to add the new tag manually`); process.exit(1) } const shortSha = getGitShortSha(); const splitPos = latestTag.indexOf('-'); let nextTag = '' if (splitPos === -1) { // the latest tag likes v7.6.0, v7.6.1 // then the next tag should be v7.6.1- for v7.6.0, v7.6.2- for v7.6.1 const suffix = latestTag.replace(`v${branchVer}.`, ''); nextTag = `v${branchVer}.${parseInt(suffix) + 1}-${shortSha}`; } else if (latestTag.match(/^v\d+\.\d+\.\d+-\d+-[0-9a-z]{9}/)) { // the latest tag likes v7.6.0-3-g383cf602, v7.6.1-3-g383cf602 // then the next tag should be v7.6.1- for v7.6.0, v7.6.2- for v7.6.1 const suffix = latestTag.substring(0, splitPos).replace(`v${branchVer}.`, ''); nextTag = `v${branchVer}.${parseInt(suffix) + 1}-${shortSha}`; } else { // the latest tag likes v7.6.0-, v7.6.1- // then the next tag should be v7.6.0- for v7.6.0-, v7.6.1- for v7.6.1- const prefix = latestTag.substring(0, splitPos); nextTag = `${prefix}-${shortSha}`; } question(nextTag) } function createMasterTag() { const latestTag = getGitLatestTag(); // the latest tag must like vX.Y.0-alpha-3-g383cf602, likes `v8.4.0-alpha-3-g383cf602` // or like vX.Y.0--3-g383cf602 const shortSha = getGitShortSha(); if (!latestTag.match(/^v\d+\.\d+.0-alpha/) && !latestTag.match(/^v\d+\.\d+.0-[0-9a-z]{8}/)) { console.error(`Err: latest tag ${latestTag} is not a valid tag, please add the tag manually, currently sha is ${shortSha}, the tag should be vX.Y.Z-${shortSha}`) process.exit(1) } const splitPos = latestTag.indexOf('-'); const prefix = latestTag.substring(0, splitPos); const nextTag = `${prefix}-${shortSha}` question(nextTag) } function createTag() { const branch = getGitBranch(); if (branch.match(/^release-\d+\.\d+$/)) { createReleaseTag() } else if (branch === 'master') { createMasterTag() } else { console.error('Err: this is not a valid branch that can be tagged'); process.exit(1) } } createTag() ================================================ FILE: scripts/distro/write_strings.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. package main import ( "encoding/json" "flag" "io/ioutil" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/distro" ) func main() { outputPath := flag.String("o", "", "Output json file path") flag.Parse() d, _ := json.Marshal(distro.R()) if err := ioutil.WriteFile(*outputPath, d, 0o600); err != nil { log.Fatal("Write output failed", zap.Error(err)) } } ================================================ FILE: scripts/distro/write_strings.sh ================================================ #!/usr/bin/env bash # This script outputs distribution strings to json which is required for the frontend. set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" PROJECT_DIR=$(cd "$DIR/../.."; pwd) TARGET="${PROJECT_DIR}/ui/packages/tidb-dashboard-for-op/src/utils/distro/strings_res.json" echo "+ Write distro strings" cd "$PROJECT_DIR" go run scripts/distro/write_strings.go -o="${TARGET}" echo " - Success! Distro strings:" cat "${TARGET}" echo ================================================ FILE: scripts/embed_ui_assets.sh ================================================ #!/usr/bin/env bash # This script embeds UI assets into Golang source file. UI assets must be already built # before calling this script. # # Available flags: # NO_ASSET_BUILD_TAG=1 # No build tags will be included in the generated source code. # ASSET_BUILD_TAG=X # Customize the build tag of the generated source code. If unspecified, build tag will be "ui_server". set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" PROJECT_DIR=$(cd "$DIR/.."; pwd) UI_ASSETS_DIR=$PROJECT_DIR/ui/packages/tidb-dashboard-for-op/dist export GOBIN=$PROJECT_DIR/bin export PATH=$GOBIN:$PATH echo "+ Preflight check" if [ ! -d "$UI_ASSETS_DIR" ]; then echo " - Error: UI assets must be built first" exit 1 fi if [ "${NO_ASSET_BUILD_TAG:-}" = "1" ]; then BUILD_TAG_PARAMETER="" else BUILD_TAG_PARAMETER=${ASSET_BUILD_TAG:-ui_server} fi echo "+ Embed UI assets" cd "$PROJECT_DIR/scripts" go run generate_assets.go "$UI_ASSETS_DIR" "$BUILD_TAG_PARAMETER" HANDLER_PATH=$PROJECT_DIR/pkg/uiserver/embedded_assets_handler.go mv assets_vfsdata.go $HANDLER_PATH echo " - Assets handler written to $HANDLER_PATH" ================================================ FILE: scripts/generate_assets.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. package main import ( "net/http" "os" "github.com/pingcap/log" "github.com/shurcooL/vfsgen" "go.uber.org/zap" ) func main() { if len(os.Args) < 3 { log.Fatal("Require 2 args") } directory := os.Args[1] buildTag := os.Args[2] var fs http.FileSystem = http.Dir(directory) err := vfsgen.Generate(fs, vfsgen.Options{ BuildTags: buildTag, PackageName: "uiserver", VariableName: "assets", }) if err != nil { log.Fatal("Generate vfs failed", zap.Error(err)) } } ================================================ FILE: scripts/generate_swagger_spec.sh ================================================ #!/usr/bin/env bash # This script generates API client from the swagger annotation in the Golang server code. set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" PROJECT_DIR=$(cd "$DIR/.."; pwd) cd $PROJECT_DIR echo "+ Generate swagger spec" bin/swag init --parseDependency --parseDepth 1 --generalInfo cmd/tidb-dashboard/main.go --propertyStrategy snakecase \ --exclude ui --output swaggerspec ================================================ FILE: scripts/go.mod ================================================ module scripts go 1.25.7 require ( github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe github.com/olekukonko/tablewriter v0.0.5 github.com/pingcap/log v0.0.0-20210906054005-afc726e70354 github.com/pingcap/tidb-dashboard v0.0.0-00010101000000-000000000000 github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 github.com/swaggo/swag v1.7.9 github.com/vektra/mockery/v2 v2.40.3 go.uber.org/zap v1.21.0 golang.org/x/mod v0.29.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/benbjohnson/clock v1.1.0 // indirect github.com/chigopher/pathlib v0.19.1 // indirect github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/ghodss/yaml v1.0.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/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/iancoleman/strcase v0.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/copier v0.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rs/zerolog v1.29.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.6.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.15.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/urfave/cli/v2 v2.3.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.8.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/tools v0.38.0 // indirect gopkg.in/ini.v1 v1.67.0 // 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 ) replace github.com/pingcap/tidb-dashboard => ../ ================================================ FILE: scripts/go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 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/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs= github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chigopher/pathlib v0.19.1 h1:RoLlUJc0CqBGwq239cilyhxPNLXTK+HXoASGyGznx5A= github.com/chigopher/pathlib v0.19.1/go.mod h1:tzC1dZLW8o33UQpWkNkhvPwL5n4yyFRFm/jL1YGWFvY= 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/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe h1:69JI97HlzP+PH5Mi1thcGlDoBr6PS2Oe+l3mNmAkbs4= github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 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/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 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.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 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/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/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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/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.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 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/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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/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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 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/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/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/log v0.0.0-20210906054005-afc726e70354 h1:SvWCbCPh1YeHd9yQLksvJYAgft6wLTY1aNG81tpyscQ= github.com/pingcap/log v0.0.0-20210906054005-afc726e70354/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= 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/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvqWEUH6SjNiu7VhSjuVFTFiTcphaLU= github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 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/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 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/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 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 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/vektra/mockery/v2 v2.40.3 h1:IZ2lydSDFsY0khnEsbSu13VLcqSsa6UYSS/8F+uOJmo= github.com/vektra/mockery/v2 v2.40.3/go.mod h1:KYBZF/7sqOa86BaOZPYsoCZWEWLS90a5oBLg2pVudxY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 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/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= 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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/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-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/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.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/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-20190108225652-1e06a53dbb7e/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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.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.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 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-20190227155943-e225da77a7e6/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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20201207232520-09787c993a3a/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/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-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/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/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 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-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= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 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 h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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.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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/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= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: scripts/install_go_tools.sh ================================================ #!/usr/bin/env bash set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" PROJECT_DIR=$(cd "$DIR/.."; pwd) # See https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module cd $PROJECT_DIR/scripts export GOBIN=$PROJECT_DIR/bin export PATH=$GOBIN:$PATH echo "+ Install go tools" grep '_' tools.go | sed 's/"//g' | awk '{print $2}' | xargs -t -L 1 go install echo "+ Clean up go mod" go mod tidy ================================================ FILE: scripts/lint.sh ================================================ #!/usr/bin/env bash # This script run lints. set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" PROJECT_DIR="$(dirname "$DIR")" cd $PROJECT_DIR LINT_BIN=./bin/golangci-lint REQUIRED_VERSION=2.11.3 NEED_DOWNLOAD=true echo "+ Check golangci-lint binary" if [[ -f "${LINT_BIN}" ]]; then if ${LINT_BIN} --version | grep -qF ${REQUIRED_VERSION}; then NEED_DOWNLOAD=false fi fi if [ "${NEED_DOWNLOAD}" = true ]; then echo " - Download golangci-lint binary" curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v${REQUIRED_VERSION} fi echo "+ Run lints for Go source code" ${LINT_BIN} run --fix --timeout=10m echo "+ Clean up go mod" go mod tidy ================================================ FILE: scripts/pd_version_matrix.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. // This proram can be used to generate a version relationship matrix for TiDB Dashboard and PD. package main import ( "flag" "fmt" "os" "sort" "strings" "time" "github.com/codeskyblue/go-sh" "github.com/olekukonko/tablewriter" "github.com/pingcap/log" "go.uber.org/zap" "golang.org/x/mod/modfile" "golang.org/x/mod/semver" ) var pdGitDir string var dashboardGitDir = flag.String("dashboard", "", "TiDB Dashboard git path") var outputFormat = flag.String("format", "mdtable", "Output format, accept values: text, mdtable, mdtable-link") func mustSuccess(err error) { if err != nil { log.Fatal("Execute failed", zap.Error(err)) } } func listPDTags() []string { output, err := sh.Command("git", "tag", sh.Dir(pdGitDir)).Output() mustSuccess(err) validTags := make([]string, 0) pdTags := strings.Split(string(output), "\n") for _, pdTag := range pdTags { if !semver.IsValid(pdTag) { continue } validTags = append(validTags, pdTag) } sort.SliceStable(validTags, func(i, j int) bool { return semver.Compare(validTags[i], validTags[j]) < 0 }) return validTags } func lookupDashboardCommit(pdTag string) string { output, err := sh.Command( "git", "show", fmt.Sprintf("%s:go.mod", pdTag), sh.Dir(pdGitDir)).Output() mustSuccess(err) f, err := modfile.Parse("go.mod", output, nil) mustSuccess(err) for _, r := range f.Require { if r.Mod.Path != "github.com/pingcap-incubator/tidb-dashboard" && r.Mod.Path != "github.com/pingcap/tidb-dashboard" { continue } if !semver.IsValid(r.Mod.Version) { continue } versionSegments := strings.Split(r.Mod.Version, "-") gitHash := versionSegments[2] return gitHash } return "" } func lookupDashboardRelease(gitCommit string) string { sess := sh.NewSession() sess.Stderr = nil output, err := sess. Command("git", "show", fmt.Sprintf("%s:release-version", gitCommit), sh.Dir(*dashboardGitDir)). Output() if err != nil { // It might be possible that the TiDB Dashboard is not using a calver. return "" } lines := strings.Split(string(output), "\n") for _, line := range lines { if len(line) > 0 && !strings.HasPrefix(line, "#") { return line } } return "" } func lookupPDTagUpdateTime(pdTag string) string { output, err := sh.Command( "git", "log", "-1", "--format=%cI", pdTag, sh.Dir(pdGitDir)).Output() mustSuccess(err) t, err := time.Parse(time.RFC3339, strings.TrimSpace(string(output))) mustSuccess(err) return t.UTC().Format("2006-01-02") } func main() { flag.Parse() if flag.NArg() < 1 { fmt.Println("Usage: go run pd_version_matrix.go ") os.Exit(1) return } pdGitDir = flag.Arg(0) output := make([][]string, 0) pdTags := listPDTags() for _, pdTag := range pdTags { if strings.Count(pdTag, "-") > 0 { continue } if semver.Compare(pdTag, "v4.0.0") < 0 { continue } tagAt := lookupPDTagUpdateTime(pdTag) dashboardCommit := lookupDashboardCommit(pdTag) if dashboardCommit == "" { continue } dashboardRelease := lookupDashboardRelease(dashboardCommit) if dashboardRelease == "" { continue } output = append(output, []string{pdTag, tagAt, dashboardRelease}) } switch *outputFormat { case "mdtable", "mdtable-link": if *outputFormat == "mdtable-link" { for _, row := range output { row[2] = fmt.Sprintf("[%s](https://github.com/pingcap/tidb-dashboard/releases/tag/v%s)", row[2], row[2]) } } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"PD Version", "PD Commit At", "TiDB Dashboard Version"}) table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) table.SetCenterSeparator("|") table.AppendBulk(output) table.Render() default: for _, row := range output { fmt.Printf("%s: %s\n", row[0], row[1], row[2]) } } } ================================================ FILE: scripts/start_tiup.sh ================================================ #!/usr/bin/env bash set -ex tidb_version=$1 without_ngm=${2:-false} mode=${3:-"start"} # TIUP_BIN_DIR=$TIUP_HOME/bin/tiup TIUP_BIN_DIR=$HOME/.tiup/bin/tiup DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" if [ $mode = "restart" ]; then # get process id pid=$(ps -ef | grep -v start_tiup | grep tiup | grep -v grep | awk '{print $2}') for id in $pid do # kill tiup-playground echo "killing $id" kill -9 $id; done # Run Tiup $TIUP_BIN_DIR playground ${tidb_version} --db.config=$DIR/tiup.config.toml --tiflash=0 &> start_tiup.log & else echo "install tiup" # Install TiUP curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh $TIUP_BIN_DIR update playground # Run Tiup $TIUP_BIN_DIR playground ${tidb_version} --without-monitor=${without_ngm} --tiflash=0 &> ~/start_tiup.log & fi ================================================ FILE: scripts/tiup.config.toml ================================================ [log] slow-threshold = 800 ================================================ FILE: scripts/tools.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. package scripts // This file lists tools that need to be installed via `go install`. import ( _ "github.com/swaggo/swag/cmd/swag" _ "github.com/vektra/mockery/v2" ) ================================================ FILE: scripts/wait_tiup_playground.sh ================================================ #!/usr/bin/env bash # Wait unitl `tiup playground` command runs success INTERVAL=$1 MAX_TIMES=$2 if ([ -z "${INTERVAL}" ] || [ -z "${MAX_TIMES}" ]); then echo "Usage: command " exit 1 fi source /home/runner/.profile for ((i=0; i<${MAX_TIMES}; i++)); do tiup playground display if [ $? -eq 0 ]; then exit 0 fi cat ~/start_tiup.log sleep ${INTERVAL} done exit 1 ================================================ FILE: swaggerspec/.gitignore ================================================ * !.gitignore !placeholder.go ================================================ FILE: swaggerspec/placeholder.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. // This file only ensures `swaggerspec` package exist even if swagger is not enabled. This is required for `go mod tidy`. package swaggerspec import ( // Make sure that go mod tidy won't clean up necessary dependencies. _ "github.com/alecthomas/template" _ "github.com/swaggo/swag" ) ================================================ FILE: tests/create_table.sh ================================================ #!/usr/bin/env bash set -euo pipefail echo "+ Create test tables" cat tests/schema/*.sql | mysql --host 127.0.0.1 --port 4000 -u root test || true echo " - Success!" ================================================ FILE: tests/dump.sh ================================================ #!/usr/bin/env bash set -euo pipefail PROJECT_DIR="$(dirname "$0")/.." source scripts/_inc/download_tools.sh >/dev/null source scripts/_inc/run_services.sh >/dev/null download_tools dump_schema $@ go run $PROJECT_DIR/tests/util/dump/dump.go $@ ================================================ FILE: tests/fixtures/CLUSTER_SLOW_QUERY.yml ================================================ - INSTANCE: "1" Warnings: null Time: 2021-12-19T23:45:30.786521+08:00 Txn_start_ts: 1 User: "1" Host: "1" Conn_ID: 1 Exec_retry_count: 1 Exec_retry_time: 1 Query_time: 0.462883101 Parse_time: 1 Compile_time: 3.2932e-05 Rewrite_time: 1.943e-06 Preproc_subqueries: 1 Preproc_subqueries_time: 1 Optimize_time: 1 Wait_TS: 1 Prewrite_time: 1 Wait_prewrite_binlog_time: 1 Commit_time: 1 Get_commit_ts_time: 1 Commit_backoff_time: 1 Backoff_types: "1" Resolve_lock_time: 1 Local_latch_wait_time: 1 Write_keys: 1 Write_size: 1 Prewrite_region: 1 Txn_retry: 1 Cop_time: 1 Process_time: 1 Wait_time: 1 Backoff_time: 1 LockKeys_time: 1 Request_count: 1 Total_keys: 1 Process_keys: 1 Rocksdb_delete_skipped_count: 1 Rocksdb_key_skipped_count: 1 Rocksdb_block_cache_hit_count: 1 Rocksdb_block_read_count: 1 Rocksdb_block_read_byte: 1 DB: "1" Index_names: "1" Is_internal: 1 Digest: TEST_ALL_FIELDS Stats: "1" Cop_proc_avg: 1 Cop_proc_p90: 1 Cop_proc_max: 1 Cop_proc_addr: "1" Cop_wait_avg: 1 Cop_wait_p90: 1 Cop_wait_max: 1 Cop_wait_addr: "1" Mem_max: 1 Disk_max: 1 KV_total: 1 PD_total: 1 Backoff_total: 1 Write_sql_response_total: 1 Result_rows: 1 Backoff_Detail: "1" Prepared: 1 Succ: 1 IsExplicitTxn: 1 IsWriteCacheTable: 1 Plan_from_cache: 1 Plan_from_binding: 1 Plan: "1" Plan_digest: "1" Prev_stmt: "test prev stmt" Query: CREATE DATABASE IF NOT EXISTS `mysql`; - INSTANCE: "" Warnings: null Time: 2021-12-19T23:49:47.79658+08:00 Txn_start_ts: 0 User: root Host: 127.0.0.1 Conn_ID: 7 Exec_retry_count: 0 Exec_retry_time: 0 Query_time: 6.3039e-05 Parse_time: 1.5559e-05 Compile_time: 3.4865e-05 Rewrite_time: 5.851e-06 Preproc_subqueries: 0 Preproc_subqueries_time: 0 Optimize_time: 2.18e-05 Wait_TS: 0 Prewrite_time: 0 Wait_prewrite_binlog_time: 0 Commit_time: 0 Get_commit_ts_time: 0 Commit_backoff_time: 0 Backoff_types: "" Resolve_lock_time: 0 Local_latch_wait_time: 0 Write_keys: 0 Write_size: 0 Prewrite_region: 0 Txn_retry: 0 Cop_time: 0 Process_time: 0 Wait_time: 0 Backoff_time: 0 LockKeys_time: 0 Request_count: 0 Total_keys: 0 Process_keys: 0 Rocksdb_delete_skipped_count: 1 Rocksdb_key_skipped_count: 1 Rocksdb_block_cache_hit_count: 1 Rocksdb_block_read_count: 1 Rocksdb_block_read_byte: 1 DB: test Index_names: "" Is_internal: 0 Digest: 911916530273cbddb6cee664f22d8f9d1ddf457ca0b6baeb28d3fad06b376a07 Stats: "" Cop_proc_avg: 0 Cop_proc_p90: 0 Cop_proc_max: 0 Cop_proc_addr: "" Cop_wait_avg: 0 Cop_wait_p90: 0 Cop_wait_max: 0 Cop_wait_addr: "" Mem_max: 0 Disk_max: 0 KV_total: 0 PD_total: 8.677e-06 Backoff_total: 0 Write_sql_response_total: 0 Result_rows: 0 Backoff_Detail: "" Prepared: 0 Succ: 1 IsExplicitTxn: 0 IsWriteCacheTable: 0 Plan_from_cache: 0 Plan_from_binding: 0 Plan: "" Plan_digest: "" Prev_stmt: "" Query: SET tidb_slow_log_threshold = 0; - INSTANCE: "" Warnings: null Time: 2021-12-19T23:49:47.797894+08:00 Txn_start_ts: 0 User: root Host: 127.0.0.1 Conn_ID: 9 Exec_retry_count: 0 Exec_retry_time: 0 Query_time: 0.000101329 Parse_time: 9.748e-06 Compile_time: 1.7342e-05 Rewrite_time: 4.298e-06 Preproc_subqueries: 0 Preproc_subqueries_time: 0 Optimize_time: 0 Wait_TS: 0 Prewrite_time: 0 Wait_prewrite_binlog_time: 0 Commit_time: 0 Get_commit_ts_time: 0 Commit_backoff_time: 0 Backoff_types: "" Resolve_lock_time: 0 Local_latch_wait_time: 0 Write_keys: 0 Write_size: 0 Prewrite_region: 0 Txn_retry: 0 Cop_time: 0 Process_time: 0 Wait_time: 0 Backoff_time: 0 LockKeys_time: 0 Request_count: 0 Total_keys: 0 Process_keys: 0 Rocksdb_delete_skipped_count: 1 Rocksdb_key_skipped_count: 1 Rocksdb_block_cache_hit_count: 1 Rocksdb_block_read_count: 1 Rocksdb_block_read_byte: 1 DB: test Index_names: "" Is_internal: 0 Digest: a78efa0dfe9c9aeeace75326c12fa30cca3bdec7ae9449de21e1eefc7a46720b Stats: "" Cop_proc_avg: 0 Cop_proc_p90: 0 Cop_proc_max: 0 Cop_proc_addr: "" Cop_wait_avg: 0 Cop_wait_p90: 0 Cop_wait_max: 0 Cop_wait_addr: "" Mem_max: 0 Disk_max: 0 KV_total: 0 PD_total: 1.502e-06 Backoff_total: 0 Write_sql_response_total: 0 Result_rows: 0 Backoff_Detail: "" Prepared: 0 Succ: 1 IsExplicitTxn: 0 IsWriteCacheTable: 0 Plan_from_cache: 0 Plan_from_binding: 0 Plan: "" Plan_digest: "" Prev_stmt: "" Query: SET time_zone='+00:00'; - INSTANCE: "" Warnings: null Time: 2021-12-19T23:49:47.798101+08:00 Txn_start_ts: 0 User: root Host: 127.0.0.1 Conn_ID: 13 Exec_retry_count: 0 Exec_retry_time: 0 Query_time: 9.7543e-05 Parse_time: 1.1542e-05 Compile_time: 1.628e-05 Rewrite_time: 3.166e-06 Preproc_subqueries: 0 Preproc_subqueries_time: 0 Optimize_time: 0 Wait_TS: 0 Prewrite_time: 0 Wait_prewrite_binlog_time: 0 Commit_time: 0 Get_commit_ts_time: 0 Commit_backoff_time: 0 Backoff_types: "" Resolve_lock_time: 0 Local_latch_wait_time: 0 Write_keys: 0 Write_size: 0 Prewrite_region: 0 Txn_retry: 0 Cop_time: 0 Process_time: 0 Wait_time: 0 Backoff_time: 0 LockKeys_time: 0 Request_count: 0 Total_keys: 0 Process_keys: 0 Rocksdb_delete_skipped_count: 1 Rocksdb_key_skipped_count: 1 Rocksdb_block_cache_hit_count: 1 Rocksdb_block_read_count: 1 Rocksdb_block_read_byte: 1 DB: test Index_names: "" Is_internal: 0 Digest: a78efa0dfe9c9aeeace75326c12fa30cca3bdec7ae9449de21e1eefc7a46720b Stats: "" Cop_proc_avg: 0 Cop_proc_p90: 0 Cop_proc_max: 0 Cop_proc_addr: "" Cop_wait_avg: 0 Cop_wait_p90: 0 Cop_wait_max: 0 Cop_wait_addr: "" Mem_max: 0 Disk_max: 0 KV_total: 0 PD_total: 1.533e-06 Backoff_total: 0 Write_sql_response_total: 0 Result_rows: 0 Backoff_Detail: "" Prepared: 0 Succ: 1 IsExplicitTxn: 0 IsWriteCacheTable: 0 Plan_from_cache: 0 Plan_from_binding: 0 Plan: "" Plan_digest: "" Prev_stmt: "" Query: SET time_zone='+00:00'; - INSTANCE: "" Warnings: null Time: 2021-12-19T23:49:47.799045+08:00 Txn_start_ts: 0 User: root Host: 127.0.0.1 Conn_ID: 11 Exec_retry_count: 0 Exec_retry_time: 0 Query_time: 0.000119424 Parse_time: 1.1973e-05 Compile_time: 2.1259e-05 Rewrite_time: 4.338e-06 Preproc_subqueries: 0 Preproc_subqueries_time: 0 Optimize_time: 0 Wait_TS: 0 Prewrite_time: 0 Wait_prewrite_binlog_time: 0 Commit_time: 0 Get_commit_ts_time: 0 Commit_backoff_time: 0 Backoff_types: "" Resolve_lock_time: 0 Local_latch_wait_time: 0 Write_keys: 0 Write_size: 0 Prewrite_region: 0 Txn_retry: 0 Cop_time: 0 Process_time: 0 Wait_time: 0 Backoff_time: 0 LockKeys_time: 0 Request_count: 0 Total_keys: 0 Process_keys: 0 Rocksdb_delete_skipped_count: 1 Rocksdb_key_skipped_count: 1 Rocksdb_block_cache_hit_count: 1 Rocksdb_block_read_count: 1 Rocksdb_block_read_byte: 1 DB: test Index_names: "" Is_internal: 0 Digest: a78efa0dfe9c9aeeace75326c12fa30cca3bdec7ae9449de21e1eefc7a46720b Stats: "" Cop_proc_avg: 0 Cop_proc_p90: 0 Cop_proc_max: 0 Cop_proc_addr: "" Cop_wait_avg: 0 Cop_wait_p90: 0 Cop_wait_max: 0 Cop_wait_addr: "" Mem_max: 0 Disk_max: 0 KV_total: 0 PD_total: 1.282e-06 Backoff_total: 0 Write_sql_response_total: 0 Result_rows: 0 Backoff_Detail: "" Prepared: 0 Succ: 1 IsExplicitTxn: 0 IsWriteCacheTable: 0 Plan_from_cache: 0 Plan_from_binding: 0 Plan: "" Plan_digest: "" Prev_stmt: "" Query: SET time_zone='+00:00'; - INSTANCE: "" Warnings: null Time: 2021-12-19T23:49:47.802016+08:00 Txn_start_ts: 429897544566046725 User: root Host: 127.0.0.1 Conn_ID: 7 Exec_retry_count: 0 Exec_retry_time: 0 Query_time: 0.005136275 Parse_time: 4.252e-05 Compile_time: 0.001705726 Rewrite_time: 0.000206389 Preproc_subqueries: 0 Preproc_subqueries_time: 0 Optimize_time: 0.001440608 Wait_TS: 1.4577e-05 Prewrite_time: 0 Wait_prewrite_binlog_time: 0 Commit_time: 0 Get_commit_ts_time: 0 Commit_backoff_time: 0 Backoff_types: "" Resolve_lock_time: 0 Local_latch_wait_time: 0 Write_keys: 0 Write_size: 0 Prewrite_region: 0 Txn_retry: 0 Cop_time: 0.002414267 Process_time: 0 Wait_time: 0 Backoff_time: 0 LockKeys_time: 0 Request_count: 1 Total_keys: 0 Process_keys: 0 Rocksdb_delete_skipped_count: 1 Rocksdb_key_skipped_count: 1 Rocksdb_block_cache_hit_count: 1 Rocksdb_block_read_count: 1 Rocksdb_block_read_byte: 1 DB: test Index_names: "" Is_internal: 0 Digest: 2375da6810d9c5a0d1c84875b1376bfd469ad952c1884f5dc1d6f36fc953b5df Stats: CLUSTER_SLOW_QUERY:pseudo Cop_proc_avg: 0 Cop_proc_p90: 0 Cop_proc_max: 0 Cop_proc_addr: 127.0.0.1:10080 Cop_wait_avg: 0 Cop_wait_p90: 0 Cop_wait_max: 0 Cop_wait_addr: 127.0.0.1:10080 Mem_max: 10670 Disk_max: 0 KV_total: 0.002446018 PD_total: 1.3976e-05 Backoff_total: 0 Write_sql_response_total: 5.11e-07 Result_rows: 1 Backoff_Detail: "" Prepared: 0 Succ: 1 IsExplicitTxn: 0 IsWriteCacheTable: 0 Plan_from_cache: 0 Plan_from_binding: 0 Plan: "\tid \ttask \testRows\toperator info \tactRows\texecution info \tmemory \ \tdisk\n\tHashAgg_5 \troot \t1 \tfuncs:count(1)->Column#73 \ \t1 \ttime:2.74ms, loops:2, partial_worker:{wall_time:2.714742ms, concurrency:5, task_num:1, tot_wait:12.22074ms, tot_exec:2.325µs, tot_time:12.225638ms, max:2.448372ms, p95:2.448372ms}, final_worker:{wall_time:0s, concurrency:5, task_num:1, tot_wait:12.288149ms, tot_exec:14.748µs, tot_time:12.305432ms, max:2.469603ms, p95:2.469603ms}\t10.2 KB \tN/A\n\t└─TableReader_8 \troot \t10000 \tdata:TableFullScan_7 \ \t5 \ttime:2.44ms, loops:2, cop_task: {num: 1, max: 2.64ms, proc_keys: 0, rpc_num: 1, rpc_time: 2.63ms, copr_cache_hit_ratio: 0.00} \t190 Bytes\tN/A\n\t └─TableFullScan_7\tcop[tidb]\t10000 \ttable:CLUSTER_SLOW_QUERY, keep order:false, stats:pseudo\t0 \t \tN/A \ \tN/A" Plan_digest: a5e33155313418557311d13039dbf20aa54df3b825d062bdca92f1a271e5778a Prev_stmt: "" Query: SELECT count(*) FROM INFORMATION_SCHEMA.CLUSTER_SLOW_QUERY; - INSTANCE: "" Warnings: null Time: 2021-12-19T23:49:47.802039+08:00 Txn_start_ts: 429897544566046730 User: root Host: 127.0.0.1 Conn_ID: 9 Exec_retry_count: 0 Exec_retry_time: 0 Query_time: 0.003925498 Parse_time: 2.1069e-05 Compile_time: 0.000539934 Rewrite_time: 0.000118383 Preproc_subqueries: 0 Preproc_subqueries_time: 0 Optimize_time: 0.000369585 Wait_TS: 0.000127581 Prewrite_time: 0 Wait_prewrite_binlog_time: 0 Commit_time: 0 Get_commit_ts_time: 0 Commit_backoff_time: 0 Backoff_types: "" Resolve_lock_time: 0 Local_latch_wait_time: 0 Write_keys: 0 Write_size: 0 Prewrite_region: 0 Txn_retry: 0 Cop_time: 0.002487686 Process_time: 0 Wait_time: 0 Backoff_time: 0 LockKeys_time: 0 Request_count: 1 Total_keys: 0 Process_keys: 0 Rocksdb_delete_skipped_count: 1 Rocksdb_key_skipped_count: 1 Rocksdb_block_cache_hit_count: 1 Rocksdb_block_read_count: 1 Rocksdb_block_read_byte: 1 DB: test Index_names: "" Is_internal: 0 Digest: 2375da6810d9c5a0d1c84875b1376bfd469ad952c1884f5dc1d6f36fc953b5df Stats: CLUSTER_SLOW_QUERY:pseudo Cop_proc_avg: 0 Cop_proc_p90: 0 Cop_proc_max: 0 Cop_proc_addr: 127.0.0.1:10080 Cop_wait_avg: 0 Cop_wait_p90: 0 Cop_wait_max: 0 Cop_wait_addr: 127.0.0.1:10080 Mem_max: 10670 Disk_max: 0 KV_total: 0.002366699 PD_total: 0.000122611 Backoff_total: 0 Write_sql_response_total: 1.002e-06 Result_rows: 1 Backoff_Detail: "" Prepared: 0 Succ: 1 IsExplicitTxn: 0 IsWriteCacheTable: 0 Plan_from_cache: 0 Plan_from_binding: 0 Plan: "\tid \ttask \testRows\toperator info \tactRows\texecution info \tmemory \ \tdisk\n\tHashAgg_5 \troot \t1 \tfuncs:count(1)->Column#73 \ \t1 \ttime:2.65ms, loops:2, partial_worker:{wall_time:2.62302ms, concurrency:5, task_num:1, tot_wait:12.675366ms, tot_exec:3.887µs, tot_time:12.682497ms, max:2.539994ms, p95:2.539994ms}, final_worker:{wall_time:0s, concurrency:5, task_num:1, tot_wait:12.769694ms, tot_exec:17.533µs, tot_time:12.79023ms, max:2.568939ms, p95:2.568939ms}\t10.2 KB \tN/A\n\t└─TableReader_8 \troot \t10000 \tdata:TableFullScan_7 \ \t5 \ttime:2.52ms, loops:2, cop_task: {num: 1, max: 2.49ms, proc_keys: 0, rpc_num: 1, rpc_time: 2.48ms, copr_cache_hit_ratio: 0.00} \t190 Bytes\tN/A\n\t └─TableFullScan_7\tcop[tidb]\t10000 \ttable:CLUSTER_SLOW_QUERY, keep order:false, stats:pseudo\t0 \t \tN/A \ \tN/A" Plan_digest: a5e33155313418557311d13039dbf20aa54df3b825d062bdca92f1a271e5778a Prev_stmt: "" Query: SELECT count(*) FROM INFORMATION_SCHEMA.CLUSTER_SLOW_QUERY; - INSTANCE: "" Warnings: null Time: 2021-12-19T23:49:47.802044+08:00 Txn_start_ts: 429897544566046731 User: root Host: 127.0.0.1 Conn_ID: 13 Exec_retry_count: 0 Exec_retry_time: 0 Query_time: 0.003768796 Parse_time: 1.7675e-05 Compile_time: 0.00056407 Rewrite_time: 0.000102121 Preproc_subqueries: 0 Preproc_subqueries_time: 0 Optimize_time: 0.000430239 Wait_TS: 2.0288e-05 Prewrite_time: 0 Wait_prewrite_binlog_time: 0 Commit_time: 0 Get_commit_ts_time: 0 Commit_backoff_time: 0 Backoff_types: "" Resolve_lock_time: 0 Local_latch_wait_time: 0 Write_keys: 0 Write_size: 0 Prewrite_region: 0 Txn_retry: 0 Cop_time: 0.002568317 Process_time: 0 Wait_time: 0 Backoff_time: 0 LockKeys_time: 0 Request_count: 1 Total_keys: 0 Process_keys: 0 Rocksdb_delete_skipped_count: 1 Rocksdb_key_skipped_count: 1 Rocksdb_block_cache_hit_count: 1 Rocksdb_block_read_count: 1 Rocksdb_block_read_byte: 1 DB: test Index_names: "" Is_internal: 0 Digest: 2375da6810d9c5a0d1c84875b1376bfd469ad952c1884f5dc1d6f36fc953b5df Stats: CLUSTER_SLOW_QUERY:pseudo Cop_proc_avg: 0 Cop_proc_p90: 0 Cop_proc_max: 0 Cop_proc_addr: 127.0.0.1:10080 Cop_wait_avg: 0 Cop_wait_p90: 0 Cop_wait_max: 0 Cop_wait_addr: 127.0.0.1:10080 Mem_max: 10670 Disk_max: 0 KV_total: 0.00214943 PD_total: 2.886e-06 Backoff_total: 0 Write_sql_response_total: 3.41e-07 Result_rows: 1 Backoff_Detail: "" Prepared: 0 Succ: 1 IsExplicitTxn: 0 IsWriteCacheTable: 0 Plan_from_cache: 0 Plan_from_binding: 0 Plan: "\tid \ttask \testRows\toperator info \tactRows\texecution info \tmemory \ \tdisk\n\tHashAgg_5 \troot \t1 \tfuncs:count(1)->Column#73 \ \t1 \ttime:2.77ms, loops:2, partial_worker:{wall_time:2.730291ms, concurrency:5, task_num:1, tot_wait:12.990317ms, tot_exec:2.415µs, tot_time:12.996318ms, max:2.601721ms, p95:2.601721ms}, final_worker:{wall_time:0s, concurrency:5, task_num:1, tot_wait:13.109715ms, tot_exec:14.026µs, tot_time:13.126607ms, max:2.635896ms, p95:2.635896ms}\t10.2 KB \tN/A\n\t└─TableReader_8 \troot \t10000 \tdata:TableFullScan_7 \ \t5 \ttime:2.59ms, loops:2, cop_task: {num: 1, max: 2.58ms, proc_keys: 0, rpc_num: 1, rpc_time: 2.57ms, copr_cache_hit_ratio: 0.00} \t190 Bytes\tN/A\n\t └─TableFullScan_7\tcop[tidb]\t10000 \ttable:CLUSTER_SLOW_QUERY, keep order:false, stats:pseudo\t0 \t \tN/A \ \tN/A" Plan_digest: a5e33155313418557311d13039dbf20aa54df3b825d062bdca92f1a271e5778a Prev_stmt: "" Query: SELECT count(*) FROM INFORMATION_SCHEMA.CLUSTER_SLOW_QUERY; - INSTANCE: "" Warnings: null Time: 2021-12-19T23:49:48.802373+08:00 Txn_start_ts: 429897544566046734 User: root Host: 127.0.0.1 Conn_ID: 11 Exec_retry_count: 0 Exec_retry_time: 0 Query_time: 0.00292619 Parse_time: 2.2352e-05 Compile_time: 0.000229712 Rewrite_time: 0.000125655 Preproc_subqueries: 0 Preproc_subqueries_time: 0 Optimize_time: 6.339e-05 Wait_TS: 0.000181109 Prewrite_time: 0 Wait_prewrite_binlog_time: 0 Commit_time: 0 Get_commit_ts_time: 0 Commit_backoff_time: 0 Backoff_types: "" Resolve_lock_time: 0 Local_latch_wait_time: 0 Write_keys: 0 Write_size: 0 Prewrite_region: 0 Txn_retry: 0 Cop_time: 0.001871728 Process_time: 0 Wait_time: 0 Backoff_time: 0 LockKeys_time: 0 Request_count: 1 Total_keys: 0 Process_keys: 0 Rocksdb_delete_skipped_count: 1 Rocksdb_key_skipped_count: 1 Rocksdb_block_cache_hit_count: 1 Rocksdb_block_read_count: 1 Rocksdb_block_read_byte: 1 DB: test Index_names: "" Is_internal: 0 Digest: 2375da6810d9c5a0d1c84875b1376bfd469ad952c1884f5dc1d6f36fc953b5df Stats: CLUSTER_SLOW_QUERY:pseudo Cop_proc_avg: 0 Cop_proc_p90: 0 Cop_proc_max: 0 Cop_proc_addr: 127.0.0.1:10080 Cop_wait_avg: 0 Cop_wait_p90: 0 Cop_wait_max: 0 Cop_wait_addr: 127.0.0.1:10080 Mem_max: 10670 Disk_max: 0 KV_total: 0.001912876 PD_total: 0.000176561 Backoff_total: 0 Write_sql_response_total: 7.01e-07 Result_rows: 1 Backoff_Detail: "" Prepared: 0 Succ: 1 IsExplicitTxn: 0 IsWriteCacheTable: 0 Plan_from_cache: 0 Plan_from_binding: 0 Plan: "\tid \ttask \testRows\toperator info \tactRows\texecution info \tmemory \ \tdisk\n\tHashAgg_5 \troot \t1 \tfuncs:count(1)->Column#73 \ \t1 \ttime:2.03ms, loops:2, partial_worker:{wall_time:2.014637ms, concurrency:5, task_num:1, tot_wait:9.451746ms, tot_exec:2.094µs, tot_time:9.456433ms, max:1.900011ms, p95:1.900011ms}, final_worker:{wall_time:2.044704ms, concurrency:5, task_num:1, tot_wait:9.422482ms, tot_exec:11.863µs, tot_time:9.437038ms, max:1.903117ms, p95:1.903117ms}\t10.2 KB \tN/A\n\t└─TableReader_8 \troot \t10000 \tdata:TableFullScan_7 \ \t5 \ttime:1.89ms, loops:2, cop_task: {num: 1, max: 1.92ms, proc_keys: 0, rpc_num: 1, rpc_time: 1.92ms, copr_cache_hit_ratio: 0.00} \t190 Bytes\tN/A\n\t └─TableFullScan_7\tcop[tidb]\t10000 \ttable:CLUSTER_SLOW_QUERY, keep order:false, stats:pseudo\t0 \t \tN/A \ \tN/A" Plan_digest: a5e33155313418557311d13039dbf20aa54df3b825d062bdca92f1a271e5778a Prev_stmt: "" Query: SELECT count(*) FROM INFORMATION_SCHEMA.CLUSTER_SLOW_QUERY; ================================================ FILE: tests/integration/diagnose_report_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package integration 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) TestReport(c *C) { // cli, err := gorm.Open("mysql", "root:@tcp(172.16.5.40:4009)/test?charset=utf8&parseTime=True&loc=Local") // c.Assert(err, IsNil) // defer cli.Close() // // startTime := "2020-03-03 17:18:00" // endTime := "2020-03-03 17:21:00" // // tables := GetReportTablesForDisplay(startTime, endTime, cli) // for _, tbl := range tables { // printRows(tbl) // } // } // func (t *testReportSuite) TestGetTable(c *C) { // cli, err := gorm.Open(mysql.Open("root:@tcp(172.16.5.40:4009)/test?charset=utf8&parseTime=True&loc=Local")) // c.Assert(err, IsNil) // startTime := "2020-03-25 23:00:00" // endTime := "2020-03-25 23:05:00" // var table diagnose.TableDef // table, err = diagnose.GetLoadTable(startTime, endTime, cli) // c.Assert(err, IsNil) // printRows(&table) // } // func (t *testReportSuite) TestGetCompareTable(c *C) { // cli, err := gorm.Open(mysql.Open("root:@tcp(172.16.5.40:4009)/test?charset=utf8&parseTime=True&loc=Local")) // c.Assert(err, IsNil) // //startTime1 := "2020-03-12 20:17:00" // //endTime1 := "2020-03-12 20:39:00" // // // //startTime2 := "2020-03-12 20:17:00" // //endTime2 := "2020-03-12 20:39:00" // startTime1 := "2020-04-02 12:13:00" // endTime1 := "2020-04-02 12:15:00" // startTime2 := "2020-04-02 12:15:00" // endTime2 := "2020-04-02 12:17:00" // tables := diagnose.GetCompareReportTablesForDisplay(startTime1, endTime1, startTime2, endTime2, cli, nil, "ID") // for _, tbl := range tables { // printRows(tbl) // } // } // func (t *testReportSuite) TestInspection(c *C) { // cli, err := gorm.Open(mysql.Open("root:@tcp(172.16.5.40:4009)/test?charset=utf8&parseTime=True&loc=Local")) // c.Assert(err, IsNil) // // affect by big query join // startTime1 := "2020-03-08 01:36:00" // endTime1 := "2020-03-08 01:41:00" // startTime2 := "2020-03-08 01:46:30" // endTime2 := "2020-03-08 01:51:30" // // affect by big write with conflict // //startTime1 := "2020-03-10 12:35:00" // //endTime1 := "2020-03-10 12:39:00" // // // //startTime2 := "2020-03-10 12:41:00" // //endTime2 := "2020-03-10 12:45:00" // // affect by big write without conflict // //startTime1 := " 2020-03-10 13:20:00" // //endTime1 := " 2020-03-10 13:23:00" // // // //startTime2 := "2020-03-10 13:24:00" // //endTime2 := "2020-03-10 13:27:00" // // diagnose for server down // // startTime1 := "2020-03-09 20:35:00" // // endTime1 := "2020-03-09 21:20:00" // // startTime2 := "2020-03-08 20:35:00" // // endTime2 := "2020-03-09 21:20:00" // // diagnose for disk slow , need more disk metric. // //startTime1 := "2020-03-10 12:48:00" // //endTime1 := "2020-03-10 12:50:00" // // // //startTime2 := "2020-03-10 12:54:30" // //endTime2 := "2020-03-10 12:56:30" // table, errRow := diagnose.CompareDiagnose(startTime1, endTime1, startTime2, endTime2, cli) // c.Assert(errRow, IsNil) // printRows(&table) // } // func printRows(t *diagnose.TableDef) { // if t == nil { // fmt.Println("table is nil") // return // } // fmt.Println(strings.Join(t.Category, " - ")) // fmt.Println(t.Title) // fmt.Println(t.Comment) // if len(t.Rows) == 0 { // fmt.Println("table rows is 0") // return // } // fieldLen := t.ColumnWidth() // // fmt.Println(fieldLen) // printLine := func(values []string, comment string) { // line := "" // for i, s := range values { // for k := len(s); k < fieldLen[i]; k++ { // s += " " // } // if i > 0 { // line += " | " // } // line += s // } // if len(comment) != 0 { // line = line + " | " + comment // } // fmt.Println(line) // } // printLine(t.Column, "") // for _, row := range t.Rows { // printLine(row.Values, row.Comment) // for i := range row.SubValues { // printLine(row.SubValues[i], "") // } // } // fmt.Println("") // } ================================================ FILE: tests/integration/info/info_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package info import ( "bytes" "encoding/json" "net/http" "testing" "github.com/stretchr/testify/suite" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/apiserver/info" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user/code" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/tests/util" "github.com/pingcap/tidb-dashboard/util/testutil" ) type testInfoSuite struct { suite.Suite db *testutil.TestDB authService *user.AuthService infoService *info.Service codeService *code.Service } func TestInfoSuite(t *testing.T) { db := testutil.OpenTestDB(t) tidbVersion := util.GetTiDBVersion(t, db) authService := &user.AuthService{} infoService := &info.Service{} codeService := &code.Service{} app := util.NewMockApp(t, tidbVersion, config.Default(), fx.Populate(&authService), fx.Populate(&infoService), fx.Populate(&codeService), ) app.RequireStart() suite.Run(t, &testInfoSuite{ db: db, authService: authService, infoService: infoService, codeService: codeService, }) app.RequireStop() } func (s *testInfoSuite) TestWithNotLoginUser() { req, _ := http.NewRequest(http.MethodGet, "/info/whoami", nil) c, w := util.TestReqWithHandlers(req, s.authService.MWAuthRequired(), s.infoService.WhoamiHandler) s.Require().Contains(c.Errors.Last().Err.Error(), "common.unauthenticated") s.Require().Equal(401, w.Code) } func (s *testInfoSuite) TestWithSQLLoginUser() { token := s.getTokenBySQLRoot() res := s.requestWhoami(token) s.Require().Equal(res.DisplayName, "root") s.Require().Equal(res.IsWriteable, true) s.Require().Equal(res.IsShareable, true) } func (s *testInfoSuite) TestWithShareCodeLoginUser() { rootUserToken := s.getTokenBySQLRoot() shareCode := s.shareCode(rootUserToken, false) shareCodeUserToken := s.getTokenByShareCode(shareCode) res := s.requestWhoami(shareCodeUserToken) s.Require().Equal(res.DisplayName, "Shared from root") s.Require().Equal(res.IsWriteable, false) s.Require().Equal(res.IsShareable, false) } func (s *testInfoSuite) TestWithShareCodeAndWritePrivLoginUser() { rootUserToken := s.getTokenBySQLRoot() shareCode := s.shareCode(rootUserToken, true) shareCodeUserToken := s.getTokenByShareCode(shareCode) res := s.requestWhoami(shareCodeUserToken) s.Require().Equal(res.DisplayName, "Shared from root") s.Require().Equal(res.IsWriteable, true) s.Require().Equal(res.IsShareable, false) } func (s *testInfoSuite) getTokenBySQLRoot() string { param := make(map[string]interface{}) param["type"] = 0 param["username"] = "root" pwd, _ := user.Encrypt("", s.authService.RsaPublicKey) param["password"] = pwd jsonByte, _ := json.Marshal(param) req, _ := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(jsonByte)) c, w := util.TestReqWithHandlers(req, s.authService.LoginHandler) s.Require().Len(c.Errors, 0) s.Require().Equal(200, w.Code) res := struct { Token string }{} err := json.Unmarshal(w.Body.Bytes(), &res) s.Require().Nil(err) return res.Token } func (s *testInfoSuite) getTokenByShareCode(shareCode string) string { param := make(map[string]interface{}) param["type"] = 1 param["password"] = shareCode jsonByte, _ := json.Marshal(param) req, _ := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(jsonByte)) c, w := util.TestReqWithHandlers(req, s.authService.LoginHandler) s.Require().Len(c.Errors, 0) s.Require().Equal(200, w.Code) res := struct { Token string }{} err := json.Unmarshal(w.Body.Bytes(), &res) s.Require().Nil(err) return res.Token } func (s *testInfoSuite) shareCode(token string, grantWritePriv bool) string { // request /user/share/code param := make(map[string]interface{}) param["expire_in_sec"] = 10800 param["revoke_write_priv"] = !grantWritePriv jsonByte, _ := json.Marshal(param) req, _ := http.NewRequest(http.MethodPost, "/user/share/code", bytes.NewReader(jsonByte)) req.Header.Add("Authorization", "Bearer "+token) c, w := util.TestReqWithHandlers(req, s.authService.MWAuthRequired(), s.codeService.ShareHandler) s.Require().Len(c.Errors, 0) s.Require().Equal(200, w.Code) res := struct { Code string }{} err := json.Unmarshal(w.Body.Bytes(), &res) s.Require().Nil(err) return res.Code } func (s *testInfoSuite) requestWhoami(token string) info.WhoAmIResponse { req, _ := http.NewRequest(http.MethodPost, "/info/whoami", nil) req.Header.Add("Authorization", "Bearer "+token) _, w := util.TestReqWithHandlers(req, s.authService.MWAuthRequired(), s.infoService.WhoamiHandler) s.Require().Equal(200, w.Code) res := info.WhoAmIResponse{} err := json.Unmarshal(w.Body.Bytes(), &res) s.Require().Nil(err) return res } ================================================ FILE: tests/integration/slowquery/compatibility_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package slowquery import ( "fmt" "sync" "testing" "github.com/stretchr/testify/suite" "gorm.io/gorm" "github.com/pingcap/tidb-dashboard/pkg/apiserver/slowquery" "github.com/pingcap/tidb-dashboard/pkg/utils" "github.com/pingcap/tidb-dashboard/tests/util" "github.com/pingcap/tidb-dashboard/util/testutil" ) type testCompatibilitySuite struct { suite.Suite db *testutil.TestDB sysSchema *utils.SysSchema } func TestCompatibilitySuite(t *testing.T) { db := testutil.OpenTestDB(t) sysSchema := utils.NewSysSchema() suite.Run(t, &testCompatibilitySuite{ db: db, sysSchema: sysSchema, }) } func (s *testCompatibilitySuite) SetupSuite() { s.db.MustExec("SET tidb_slow_log_threshold = 0") var wg sync.WaitGroup for i := 1; i < 5; i++ { wg.Go(func() { s.db.MustExec(fmt.Sprintf("SELECT count(*) FROM %s", slowquery.SlowQueryTable)) }) } wg.Wait() s.db.MustExec("SET tidb_slow_log_threshold = 300") util.LoadFixtures(s.T(), s.db, "../../fixtures") } func (s *testCompatibilitySuite) TearDownSuite() { s.db.MustClose() _ = s.sysSchema.Close() } func (s *testCompatibilitySuite) dbSession() *gorm.DB { return s.db.Gorm().Debug().Table(slowquery.SlowQueryTable) } func (s *testCompatibilitySuite) mockDBSession() *gorm.DB { return s.db.Gorm().Debug().Table(TestSlowQueryTableName) } func (s *testCompatibilitySuite) mustQuerySlowLogListWithMockDB(req *slowquery.GetListRequest) []slowquery.Model { d, err := slowquery.QuerySlowLogList(req, s.sysSchema, s.mockDBSession()) s.Require().NoError(err) return d } func (s *testCompatibilitySuite) TestFieldsCompatibility() { if util.CheckTiDBVersion(s.Require(), "< 5.0.0") { ds := s.mustQuerySlowLogListWithMockDB(&slowquery.GetListRequest{Digest: "TEST_ALL_FIELDS", Fields: "*"}) s.Require().Len(ds, 1) d := ds[0] s.Require().Empty(d.DiskMax) s.Require().Empty(d.ExecRetryTime) s.Require().Empty(d.OptimizeTime) s.Require().Empty(d.PreprocSubqueriesTime) s.Require().Empty(d.RewriteTime) s.Require().Empty(d.WaitTSTime) s.Require().Empty(d.WriteRespTime) s.Require().Empty(d.RocksdbBlockCacheHitCount) s.Require().Empty(d.RocksdbBlockReadByte) s.Require().Empty(d.RocksdbBlockReadCount) s.Require().Empty(d.RocksdbDeleteSkippedCount) s.Require().Empty(d.RocksdbKeySkippedCount) } if util.CheckTiDBVersion(s.Require(), ">= 5.0.0") { ds := s.mustQuerySlowLogListWithMockDB(&slowquery.GetListRequest{Digest: "TEST_ALL_FIELDS", Fields: "*"}) s.Require().Len(ds, 1) d := ds[0] s.Require().NotEmpty(d.DiskMax) s.Require().NotEmpty(d.ExecRetryTime) s.Require().NotEmpty(d.OptimizeTime) s.Require().NotEmpty(d.PreprocSubqueriesTime) s.Require().NotEmpty(d.RewriteTime) s.Require().NotEmpty(d.WaitTSTime) s.Require().NotEmpty(d.WriteRespTime) s.Require().NotEmpty(d.RocksdbBlockCacheHitCount) s.Require().NotEmpty(d.RocksdbBlockReadByte) s.Require().NotEmpty(d.RocksdbBlockReadCount) s.Require().NotEmpty(d.RocksdbDeleteSkippedCount) s.Require().NotEmpty(d.RocksdbKeySkippedCount) } } func (s *testCompatibilitySuite) TestQueryTableColumns() { if util.CheckTiDBVersion(s.Require(), "< 5.0.0") { cls, err := slowquery.GetAvailableFields(s.sysSchema, s.dbSession()) s.Require().NoError(err) s.Require().NotContains(cls, "rocksdb_delete_skipped_count") s.Require().NotContains(cls, "rocksdb_key_skipped_count") s.Require().NotContains(cls, "rocksdb_block_cache_hit_count") s.Require().NotContains(cls, "rocksdb_block_read_count") s.Require().NotContains(cls, "rocksdb_block_read_byte") } if util.CheckTiDBVersion(s.Require(), ">= 5.0.0") { cls, err := slowquery.GetAvailableFields(s.sysSchema, s.dbSession()) s.Require().NoError(err) s.Require().Contains(cls, "rocksdb_delete_skipped_count") s.Require().Contains(cls, "rocksdb_key_skipped_count") s.Require().Contains(cls, "rocksdb_block_cache_hit_count") s.Require().Contains(cls, "rocksdb_block_read_count") s.Require().Contains(cls, "rocksdb_block_read_byte") } } func (s *testCompatibilitySuite) TestAllAvailableFields() { // TODO: use latest instead of specific versions if util.CheckTiDBVersion(s.Require(), ">= 5.0.0") { cls, err := slowquery.GetAvailableFields(s.sysSchema, s.dbSession()) s.Require().NoError(err) s.Require().Contains(cls, "digest") s.Require().Contains(cls, "query") s.Require().Contains(cls, "instance") s.Require().Contains(cls, "db") s.Require().Contains(cls, "connection_id") s.Require().Contains(cls, "success") s.Require().Contains(cls, "timestamp") s.Require().Contains(cls, "query_time") s.Require().Contains(cls, "parse_time") s.Require().Contains(cls, "compile_time") s.Require().Contains(cls, "rewrite_time") s.Require().Contains(cls, "preproc_subqueries_time") s.Require().Contains(cls, "optimize_time") s.Require().Contains(cls, "wait_ts") s.Require().Contains(cls, "cop_time") s.Require().Contains(cls, "lock_keys_time") s.Require().Contains(cls, "write_sql_response_total") s.Require().Contains(cls, "exec_retry_time") s.Require().Contains(cls, "memory_max") s.Require().Contains(cls, "disk_max") s.Require().Contains(cls, "txn_start_ts") s.Require().Contains(cls, "prev_stmt") s.Require().Contains(cls, "plan") s.Require().Contains(cls, "is_internal") s.Require().Contains(cls, "index_names") s.Require().Contains(cls, "stats") s.Require().Contains(cls, "backoff_types") s.Require().Contains(cls, "user") s.Require().Contains(cls, "host") s.Require().Contains(cls, "process_time") s.Require().Contains(cls, "wait_time") s.Require().Contains(cls, "backoff_time") s.Require().Contains(cls, "get_commit_ts_time") s.Require().Contains(cls, "local_latch_wait_time") s.Require().Contains(cls, "resolve_lock_time") s.Require().Contains(cls, "prewrite_time") s.Require().Contains(cls, "wait_prewrite_binlog_time") s.Require().Contains(cls, "commit_time") s.Require().Contains(cls, "commit_backoff_time") s.Require().Contains(cls, "cop_proc_avg") s.Require().Contains(cls, "cop_proc_p90") s.Require().Contains(cls, "cop_proc_max") s.Require().Contains(cls, "cop_wait_avg") s.Require().Contains(cls, "cop_wait_p90") s.Require().Contains(cls, "cop_wait_max") s.Require().Contains(cls, "write_keys") s.Require().Contains(cls, "write_size") s.Require().Contains(cls, "prewrite_region") s.Require().Contains(cls, "txn_retry") s.Require().Contains(cls, "request_count") s.Require().Contains(cls, "process_keys") s.Require().Contains(cls, "total_keys") s.Require().Contains(cls, "cop_proc_addr") s.Require().Contains(cls, "cop_wait_addr") s.Require().Contains(cls, "rocksdb_delete_skipped_count") s.Require().Contains(cls, "rocksdb_key_skipped_count") s.Require().Contains(cls, "rocksdb_block_cache_hit_count") s.Require().Contains(cls, "rocksdb_block_read_count") s.Require().Contains(cls, "rocksdb_block_read_byte") } } ================================================ FILE: tests/integration/slowquery/mock_db_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package slowquery import ( "fmt" "testing" "github.com/stretchr/testify/suite" "gorm.io/gorm" "github.com/pingcap/tidb-dashboard/pkg/apiserver/slowquery" "github.com/pingcap/tidb-dashboard/pkg/utils" "github.com/pingcap/tidb-dashboard/tests/util" "github.com/pingcap/tidb-dashboard/util/testutil" ) const ( TestSlowQueryTableName = "test.CLUSTER_SLOW_QUERY" ) type testMockDBSuite struct { suite.Suite db *testutil.TestDB sysSchema *utils.SysSchema } func TestMockDBSuite(t *testing.T) { db := testutil.OpenTestDB(t) sysSchema := utils.NewSysSchema() suite.Run(t, &testMockDBSuite{ db: db, sysSchema: sysSchema, }) } func (s *testMockDBSuite) SetupSuite() { util.LoadFixtures(s.T(), s.db, "../../fixtures") } func (s *testMockDBSuite) TearDownSuite() { s.db.MustExec(fmt.Sprintf("DROP TABLE IF EXISTS `%s`", TestSlowQueryTableName)) s.db.MustClose() _ = s.sysSchema.Close() } func (s *testMockDBSuite) mustQuerySlowLogList(req *slowquery.GetListRequest) []slowquery.Model { d, err := slowquery.QuerySlowLogList(req, s.sysSchema, s.mockDBSession()) s.Require().NoError(err) return d } func (s *testMockDBSuite) mustQuerySlowLogDetail(req *slowquery.GetDetailRequest) (*slowquery.Model, error) { d, err := slowquery.QuerySlowLogDetail(req, s.sysSchema, s.mockDBSession()) return d, err } func (s *testMockDBSuite) mockDBSession() *gorm.DB { return s.db.Gorm().Debug().Table(TestSlowQueryTableName) } func (s *testMockDBSuite) TestGetListDefaultRequest() { ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{}) s.Require().Len(ds, 9) for i, d := range ds { s.Require().NotEmpty(d.Digest) s.Require().NotEmpty(d.ConnectionID) s.Require().NotEmpty(d.Timestamp) // order by timestamp if i == 0 { continue } s.Require().GreaterOrEqual(d.Timestamp, ds[i-1].Timestamp) } } func (s *testMockDBSuite) TestGetListSpecificFieldsRequest() { ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{Fields: "digest,query"}) for _, d := range ds { s.Require().NotEmpty(d.Digest) s.Require().NotEmpty(d.ConnectionID) s.Require().NotEmpty(d.Timestamp) s.Require().NotEmpty(d.Query) } } // Contains fields available for all tidb versions, other fields will be tested in compatibility_test.go. func (s *testMockDBSuite) TestGetListAllFieldsRequest() { ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{Digest: "TEST_ALL_FIELDS", Fields: "*"}) s.Require().Len(ds, 1) d := ds[0] s.Require().NotEmpty(d.BackoffTime) s.Require().NotEmpty(d.BackoffTypes) s.Require().NotEmpty(d.CommitBackoffTime) s.Require().NotEmpty(d.CommitTime) s.Require().NotEmpty(d.CompileTime) s.Require().NotEmpty(d.ConnectionID) s.Require().NotEmpty(d.CopProcAddr) s.Require().NotEmpty(d.CopProcAvg) s.Require().NotEmpty(d.CopProcMax) s.Require().NotEmpty(d.CopProcP90) s.Require().NotEmpty(d.CopTime) s.Require().NotEmpty(d.CopWaitAddr) s.Require().NotEmpty(d.CopWaitAvg) s.Require().NotEmpty(d.CopWaitMax) s.Require().NotEmpty(d.CopWaitP90) s.Require().NotEmpty(d.DB) s.Require().NotEmpty(d.Digest) s.Require().NotEmpty(d.GetCommitTSTime) s.Require().NotEmpty(d.Host) s.Require().NotEmpty(d.IndexNames) s.Require().NotEmpty(d.Instance) s.Require().NotEmpty(d.IsInternal) s.Require().NotEmpty(d.LocalLatchWaitTime) s.Require().NotEmpty(d.LockKeysTime) s.Require().NotEmpty(d.MemoryMax) s.Require().NotEmpty(d.ParseTime) s.Require().NotEmpty(d.Plan) s.Require().NotEmpty(d.PrevStmt) s.Require().NotEmpty(d.PrewriteRegion) s.Require().NotEmpty(d.PrewriteTime) s.Require().NotEmpty(d.ProcessKeys) s.Require().NotEmpty(d.ProcessTime) s.Require().NotEmpty(d.Query) s.Require().NotEmpty(d.QueryTime) s.Require().NotEmpty(d.RequestCount) s.Require().NotEmpty(d.Stats) s.Require().NotEmpty(d.Success) s.Require().NotEmpty(d.Timestamp) s.Require().NotEmpty(d.TotalKeys) s.Require().NotEmpty(d.TxnRetry) s.Require().NotEmpty(d.User) s.Require().NotEmpty(d.WaitPreWriteBinlogTime) s.Require().NotEmpty(d.WaitTime) s.Require().NotEmpty(d.WriteKeys) s.Require().NotEmpty(d.WriteSize) } func (s *testMockDBSuite) TestGetListTimeRangeRequest() { // 1639928730 - 2021-12-19T23:45:30+08:00 // 1639928987 - 2021-12-19T23:49:48+08:00 ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{BeginTime: 1639928730, EndTime: 1639928988}) s.Require().Len(ds, 8) } func (s *testMockDBSuite) TestGetListLimitRequest() { ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{Limit: 5}) s.Require().Len(ds, 5) } func (s *testMockDBSuite) TestGetListSearchRequest() { digest := "2375da6810d9c5a0d1c84875b1376bfd469ad952c1884f5dc1d6f36fc953b5df" ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{Fields: "digest", Text: digest}) s.Require().Len(ds, 4) for _, d := range ds { s.Require().Contains(d.Digest, digest) } txnStartTS := "429897544566046725" ds2 := s.mustQuerySlowLogList(&slowquery.GetListRequest{Fields: "txn_start_ts", Text: txnStartTS}) s.Require().Len(ds2, 1) s.Require().Contains(ds2[0].TxnStartTS, txnStartTS) query := "INFORMATION_SCHEMA.CLUSTER_SLOW_QUERY" ds3 := s.mustQuerySlowLogList(&slowquery.GetListRequest{Fields: "query", Text: query}) s.Require().Len(ds3, 4) for _, d := range ds3 { s.Require().Contains(d.Query, query) } prevStmt := "test prev stmt" ds4 := s.mustQuerySlowLogList(&slowquery.GetListRequest{Fields: "prev_stmt", Text: prevStmt}) s.Require().Len(ds4, 1) s.Require().Contains(ds4[0].PrevStmt, prevStmt) } func (s *testMockDBSuite) TestGetListMultiKeywordsSearchRequest() { digest := "2375da6810d9c5a0d1c84875b1376bfd469ad952c1884f5dc1d6f36fc953b5df" txnStartTS := "429897544566046725" ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{Fields: "digest,txn_start_ts", Text: fmt.Sprintf("%s %s", digest, txnStartTS)}) s.Require().Len(ds, 1) s.Require().Contains(ds[0].Digest, digest) s.Require().Contains(ds[0].TxnStartTS, txnStartTS) } func (s *testMockDBSuite) TestGetListUseDBRequest() { ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{DB: []string{"test"}}) s.Require().NotEmpty(ds) ds2 := s.mustQuerySlowLogList(&slowquery.GetListRequest{DB: []string{"not_exist_db"}}) s.Require().Empty(ds2) } func (s *testMockDBSuite) TestGetListOrderRequest() { ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{OrderBy: "txn_start_ts"}) for i, d := range ds { if i == 0 { continue } s.Require().GreaterOrEqual(d.TxnStartTS, ds[i-1].TxnStartTS) } ds2 := s.mustQuerySlowLogList(&slowquery.GetListRequest{IsDesc: true, OrderBy: "txn_start_ts"}) for i, d := range ds2 { if i == 0 { continue } s.Require().LessOrEqual(d.TxnStartTS, ds[i-1].TxnStartTS) } } func (s *testMockDBSuite) TestGetListPlansRequest() { ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{Plans: []string{"a5e33155313418557311d13039dbf20aa54df3b825d062bdca92f1a271e5778a"}}) s.Require().NotEmpty(ds) ds2 := s.mustQuerySlowLogList(&slowquery.GetListRequest{Plans: []string{"not_exist_plan"}}) s.Require().Empty(ds2) } func (s *testMockDBSuite) TestGetListAllRequest() { digest := "2375da6810d9c5a0d1c84875b1376bfd469ad952c1884f5dc1d6f36fc953b5df" txnStartTS := "429897544566046725" ds := s.mustQuerySlowLogList(&slowquery.GetListRequest{ BeginTime: 1639928730, EndTime: 1639928988, Limit: 5, IsDesc: true, OrderBy: "txn_start_ts", DB: []string{"test"}, Fields: "digest,txn_start_ts", Text: fmt.Sprintf("%s %s", digest, txnStartTS), }) s.Require().NotEmpty(ds) s.Require().LessOrEqual(len(ds), 5) s.Require().Contains(ds[0].Digest, digest) s.Require().Contains(ds[0].TxnStartTS, txnStartTS) ds2 := s.mustQuerySlowLogList(&slowquery.GetListRequest{ BeginTime: 1639928730, EndTime: 1639928988, Limit: 5, IsDesc: true, OrderBy: "timestamp", DB: []string{}, Fields: "query,timestamp,query_time,memory_max,digest", Digest: digest, Plans: []string{"a5e33155313418557311d13039dbf20aa54df3b825d062bdca92f1a271e5778a"}, }) s.Require().NotEmpty(ds2) s.Require().LessOrEqual(len(ds2), 5) s.Require().Contains(ds2[0].Digest, digest) } func (s *testMockDBSuite) TestGetDetailRequest() { ds, err := s.mustQuerySlowLogDetail(&slowquery.GetDetailRequest{ Digest: "2375da6810d9c5a0d1c84875b1376bfd469ad952c1884f5dc1d6f36fc953b5df", Timestamp: 1639928730, ConnectID: "0", }) s.Require().Error(err) s.Require().Nil(ds) s.Require().Contains(err.Error(), "record not found") ds2, err := s.mustQuerySlowLogDetail(&slowquery.GetDetailRequest{ Digest: "2375da6810d9c5a0d1c84875b1376bfd469ad952c1884f5dc1d6f36fc953b5df", Timestamp: 1639928987.802016, ConnectID: "7", }) s.Require().Nil(err) s.Require().NotNil(ds2) s.Require().Equal(ds2.Timestamp, 1639928987.802016) s.Require().Equal(ds2.Digest, "2375da6810d9c5a0d1c84875b1376bfd469ad952c1884f5dc1d6f36fc953b5df") s.Require().Equal(ds2.ConnectionID, "7") } ================================================ FILE: tests/integration/user/user_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package user import ( "bytes" "encoding/json" "net/http" "testing" "github.com/joomcode/errorx" "github.com/stretchr/testify/suite" "go.uber.org/fx" "github.com/pingcap/tidb-dashboard/pkg/apiserver/info" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/pkg/tidb" "github.com/pingcap/tidb-dashboard/tests/util" "github.com/pingcap/tidb-dashboard/util/rest" "github.com/pingcap/tidb-dashboard/util/testutil" ) type testUserSuite struct { suite.Suite db *testutil.TestDB authService *user.AuthService infoService *info.Service } func TestUserSuite(t *testing.T) { db := testutil.OpenTestDB(t) tidbVersion := util.GetTiDBVersion(t, db) authService := &user.AuthService{} infoService := &info.Service{} app := util.NewMockApp(t, tidbVersion, config.Default(), fx.Populate(&authService), fx.Populate(&infoService), ) app.RequireStart() suite.Run(t, &testUserSuite{ db: db, authService: authService, infoService: infoService, }) app.RequireStop() } func (s *testUserSuite) supportNonRootLogin() bool { return s.authService.FeatureFlagNonRootLogin.IsSupported() } func (s *testUserSuite) SetupSuite() { // drop user if exist s.db.MustExec("DROP USER IF EXISTS 'dashboardAdmin'@'%'") s.db.MustExec("DROP USER IF EXISTS 'dashboardAdmin-2'@'%'") // create user 1 with sufficient priviledges s.db.MustExec("CREATE USER 'dashboardAdmin'@'%' IDENTIFIED BY '12345678'") s.db.MustExec("GRANT PROCESS, CONFIG ON *.* TO 'dashboardAdmin'@'%'") s.db.MustExec("GRANT SHOW DATABASES ON *.* TO 'dashboardAdmin'@'%'") if s.supportNonRootLogin() { s.db.MustExec("GRANT DASHBOARD_CLIENT ON *.* TO 'dashboardAdmin'@'%'") } // create user 2 with insufficient priviledges s.db.MustExec("CREATE USER 'dashboardAdmin-2'@'%' IDENTIFIED BY '12345678'") s.db.MustExec("GRANT PROCESS, CONFIG ON *.* TO 'dashboardAdmin-2'@'%'") s.db.MustExec("GRANT SHOW DATABASES ON *.* TO 'dashboardAdmin-2'@'%'") } func (s *testUserSuite) TearDownSuite() { s.db.MustExec("DROP USER IF EXISTS 'dashboardAdmin'@'%'") s.db.MustExec("DROP USER IF EXISTS 'dashboardAdmin-2'@'%'") } func (s *testUserSuite) TestLoginWithEmpty() { req, _ := http.NewRequest(http.MethodPost, "/user/login", nil) c, w := util.TestReqWithHandlers(req, s.authService.LoginHandler) s.Require().True(errorx.IsOfType(c.Errors.Last().Err, rest.ErrBadRequest)) s.Require().Equal(401, c.Writer.Status()) s.Require().Equal(401, w.Code) } func (s *testUserSuite) TestLoginWithNotExistUser() { param := make(map[string]interface{}) param["type"] = 0 param["username"] = "not_exist" pwd, _ := user.Encrypt("aaa", s.authService.RsaPublicKey) param["password"] = pwd jsonByte, _ := json.Marshal(param) req, _ := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(jsonByte)) c, w := util.TestReqWithHandlers(req, s.authService.LoginHandler) s.Require().Contains(c.Errors.Last().Err.Error(), "authenticate failed") s.Require().True(errorx.IsOfType(c.Errors.Last().Err, tidb.ErrTiDBAuthFailed)) s.Require().Equal(401, c.Writer.Status()) s.Require().Equal(401, w.Code) } func (s *testUserSuite) TestLoginWithWrongPassword() { param := make(map[string]interface{}) param["type"] = 0 param["username"] = "dashboardAdmin" pwd, _ := user.Encrypt("123456789", s.authService.RsaPublicKey) param["password"] = pwd jsonByte, _ := json.Marshal(param) req, _ := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(jsonByte)) c, w := util.TestReqWithHandlers(req, s.authService.LoginHandler) s.Require().Contains(c.Errors.Last().Err.Error(), "authenticate failed") s.Require().True(errorx.IsOfType(c.Errors.Last().Err, tidb.ErrTiDBAuthFailed)) s.Require().Equal(401, c.Writer.Status()) s.Require().Equal(401, w.Code) } func (s *testUserSuite) TestLoginWithInsufficientPrivs() { param := make(map[string]interface{}) param["type"] = 0 param["username"] = "dashboardAdmin-2" pwd, _ := user.Encrypt("12345678", s.authService.RsaPublicKey) param["password"] = pwd jsonByte, _ := json.Marshal(param) req, _ := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(jsonByte)) c, w := util.TestReqWithHandlers(req, s.authService.LoginHandler) s.Require().Contains(c.Errors.Last().Err.Error(), "authenticate failed") s.Require().True(errorx.IsOfType(c.Errors.Last().Err, user.ErrInsufficientPrivs)) s.Require().Equal(401, c.Writer.Status()) s.Require().Equal(401, w.Code) } func (s *testUserSuite) TestLoginWithSufficientPrivs() { if s.supportNonRootLogin() { param := make(map[string]interface{}) param["type"] = 0 param["username"] = "dashboardAdmin" pwd, _ := user.Encrypt("12345678", s.authService.RsaPublicKey) param["password"] = pwd jsonByte, _ := json.Marshal(param) req, _ := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(jsonByte)) c, w := util.TestReqWithHandlers(req, s.authService.LoginHandler) s.Require().Len(c.Errors, 0) s.Require().Equal(200, c.Writer.Status()) s.Require().Equal(200, w.Code) res := struct { Token string }{} err := json.Unmarshal(w.Body.Bytes(), &res) s.Require().Nil(err) // request /info/whoami by the token req2, _ := http.NewRequest(http.MethodPost, "/info/whoami", nil) req2.Header.Add("Authorization", "Bearer "+res.Token) c2, w2 := util.TestReqWithHandlers(req2, s.authService.MWAuthRequired(), s.infoService.WhoamiHandler) s.Require().Equal(200, c2.Writer.Status()) s.Require().Equal(200, w2.Code) res2 := info.WhoAmIResponse{} err2 := json.Unmarshal(w2.Body.Bytes(), &res2) s.Require().Nil(err2) s.Require().Equal(res2.DisplayName, "dashboardAdmin") } } func (s *testUserSuite) TestLoginWithWrongPasswordForRoot() { param := make(map[string]interface{}) param["type"] = 0 param["username"] = "root" pwd, _ := user.Encrypt("aaa", s.authService.RsaPublicKey) param["password"] = pwd jsonByte, _ := json.Marshal(param) req, _ := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(jsonByte)) c, w := util.TestReqWithHandlers(req, s.authService.LoginHandler) s.Require().Contains(c.Errors.Last().Err.Error(), "authenticate failed") s.Require().True(errorx.IsOfType(c.Errors.Last().Err, tidb.ErrTiDBAuthFailed)) s.Require().Equal(401, c.Writer.Status()) s.Require().Equal(401, w.Code) } func (s *testUserSuite) TestLoginWithCorrectPasswordForRoot() { param := make(map[string]interface{}) param["type"] = 0 param["username"] = "root" pwd, _ := user.Encrypt("", s.authService.RsaPublicKey) param["password"] = pwd jsonByte, _ := json.Marshal(param) req, _ := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(jsonByte)) c, w := util.TestReqWithHandlers(req, s.authService.LoginHandler) s.Require().Len(c.Errors, 0) s.Require().Equal(200, c.Writer.Status()) s.Require().Equal(200, w.Code) res := struct { Token string }{} err := json.Unmarshal(w.Body.Bytes(), &res) s.Require().Nil(err) } // TODO: uncomment it after thinking clearly // func (s *testUserSuite) TestLoginWithSamePayloadTwice() { // param := make(map[string]interface{}) // param["type"] = 0 // param["username"] = "root" // pwd, _ := user.Encrypt("", s.authService.RsaPublicKey) // param["password"] = pwd // // success at the first time // jsonByte, _ := json.Marshal(param) // req, _ := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(jsonByte)) // c, w := util.TestReqWithHandlers(req, s.authService.LoginHandler) // s.Require().Len(c.Errors, 0) // s.Require().Equal(200, c.Writer.Status()) // s.Require().Equal(200, w.Code) // // fail at the second time // req, _ = http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(jsonByte)) // c, w = util.TestReqWithHandlers(req, s.authService.LoginHandler) // s.Require().Contains(c.Errors.Last().Err.Error(), "authenticate failed") // s.Require().Contains(c.Errors.Last().Err.Error(), "crypto/rsa: decryption error") // s.Require().Equal(401, c.Writer.Status()) // s.Require().Equal(401, w.Code) // } func (s *testUserSuite) TestLoginInfo() { req, _ := http.NewRequest(http.MethodGet, "/user/login_info", nil) c, w := util.TestReqWithHandlers(req, s.authService.GetLoginInfoHandler) s.Require().Len(c.Errors, 0) s.Require().Equal(200, c.Writer.Status()) s.Require().Equal(200, w.Code) res := user.GetLoginInfoResponse{} err := json.Unmarshal(w.Body.Bytes(), &res) s.Require().Nil(err) // SSO is not enabled default, so only returns []int{0, 1} s.Require().Equal([]int{0, 1}, res.SupportedAuthTypes) } ================================================ FILE: tests/run.sh ================================================ #!/usr/bin/env bash # Available flags: # COVER_PKG # Pass to coverpkg flag, apply coverage analysis in each test to packages matching the patterns. # default: ./pkg/... # TIDB_VERSION # Run tests with the specified tidb version # default: latest # See code coverage html # go tool cover -html ./coverage/integration_$TIDB_VERSION.txt set -euo pipefail PROJECT_DIR="$(dirname "$0")/.." source scripts/_inc/download_tools.sh >/dev/null source scripts/_inc/run_services.sh >/dev/null download_tools trap stop_tidb EXIT start_tidb ${TIDB_VERSION:=latest} $PROJECT_DIR/tests/create_table.sh PRECISE_TIDB_VERSION=$(mysql --host 127.0.0.1 --port 4000 -u root -se "SELECT VERSION()" | sed -r "s/.*TiDB-(v[0-9]+\.[0-9]+\.[0-9]+).*/\1/g") echo "+ Run integration tests on tidb $PRECISE_TIDB_VERSION" TIDB_VERSION=$PRECISE_TIDB_VERSION go test -race -v -cover \ -coverprofile=coverage/integration_${TIDB_VERSION}.txt \ -coverpkg=${COVER_PKG:-./pkg/...} \ ./tests/integration/... echo " - All tests passed!" ================================================ FILE: tests/schema/test.CLUSTER_SLOW_QUERY-schema.sql ================================================ /*!40101 SET NAMES binary*/; /*T![placement] SET PLACEMENT_CHECKS = 0*/; CREATE TABLE `CLUSTER_SLOW_QUERY` (`INSTANCE` varchar(64) DEFAULT NULL, `Time` timestamp(6) NOT NULL, `Txn_start_ts` bigint(20) unsigned DEFAULT NULL, `User` varchar(64) DEFAULT NULL, `Host` varchar(64) DEFAULT NULL, `Conn_ID` bigint(20) unsigned DEFAULT NULL, `Session_alias` varchar(64) DEFAULT NULL, `Exec_retry_count` bigint(20) unsigned DEFAULT NULL, `Exec_retry_time` double DEFAULT NULL, `Query_time` double DEFAULT NULL, `Parse_time` double DEFAULT NULL, `Compile_time` double DEFAULT NULL, `Rewrite_time` double DEFAULT NULL, `Preproc_subqueries` bigint(20) unsigned DEFAULT NULL, `Preproc_subqueries_time` double DEFAULT NULL, `Optimize_time` double DEFAULT NULL, `Wait_TS` double DEFAULT NULL, `Prewrite_time` double DEFAULT NULL, `Wait_prewrite_binlog_time` double DEFAULT NULL, `Commit_time` double DEFAULT NULL, `Get_commit_ts_time` double DEFAULT NULL, `Commit_backoff_time` double DEFAULT NULL, `Backoff_types` varchar(64) DEFAULT NULL, `Resolve_lock_time` double DEFAULT NULL, `Local_latch_wait_time` double DEFAULT NULL, `Write_keys` bigint(22) DEFAULT NULL, `Write_size` bigint(22) DEFAULT NULL, `Prewrite_region` bigint(22) DEFAULT NULL, `Txn_retry` bigint(22) DEFAULT NULL, `Cop_time` double DEFAULT NULL, `Process_time` double DEFAULT NULL, `Wait_time` double DEFAULT NULL, `Backoff_time` double DEFAULT NULL, `LockKeys_time` double DEFAULT NULL, `Request_count` bigint(20) unsigned DEFAULT NULL, `Total_keys` bigint(20) unsigned DEFAULT NULL, `Process_keys` bigint(20) unsigned DEFAULT NULL, `Rocksdb_delete_skipped_count` bigint(20) unsigned DEFAULT NULL, `Rocksdb_key_skipped_count` bigint(20) unsigned DEFAULT NULL, `Rocksdb_block_cache_hit_count` bigint(20) unsigned DEFAULT NULL, `Rocksdb_block_read_count` bigint(20) unsigned DEFAULT NULL, `Rocksdb_block_read_byte` bigint(20) unsigned DEFAULT NULL, `DB` varchar(64) DEFAULT NULL, `Index_names` varchar(100) DEFAULT NULL, `Is_internal` tinyint(1) DEFAULT NULL, `Digest` varchar(64) DEFAULT NULL, `Stats` varchar(512) DEFAULT NULL, `Cop_proc_avg` double DEFAULT NULL, `Cop_proc_p90` double DEFAULT NULL, `Cop_proc_max` double DEFAULT NULL, `Cop_proc_addr` varchar(64) DEFAULT NULL, `Cop_wait_avg` double DEFAULT NULL, `Cop_wait_p90` double DEFAULT NULL, `Cop_wait_max` double DEFAULT NULL, `Cop_wait_addr` varchar(64) DEFAULT NULL, `Mem_max` bigint(20) DEFAULT NULL, `Mem_arbitration` double DEFAULT NULL, `Disk_max` bigint(20) DEFAULT NULL, `KV_total` double DEFAULT NULL, `PD_total` double DEFAULT NULL, `Backoff_total` double DEFAULT NULL, `Write_sql_response_total` double DEFAULT NULL, `Result_rows` bigint(22) DEFAULT NULL, `Warnings` longtext DEFAULT NULL, `Backoff_Detail` varchar(4096) DEFAULT NULL, `Prepared` tinyint(1) DEFAULT NULL, `Succ` tinyint(1) DEFAULT NULL, `IsExplicitTxn` tinyint(1) DEFAULT NULL, `IsWriteCacheTable` tinyint(1) DEFAULT NULL, `Plan_from_cache` tinyint(1) DEFAULT NULL, `Plan_from_binding` tinyint(1) DEFAULT NULL, `Has_more_results` tinyint(1) DEFAULT NULL, `Resource_group` varchar(64) DEFAULT NULL, `Request_unit_read` double DEFAULT NULL, `Request_unit_write` double DEFAULT NULL, `Time_queued_by_rc` double DEFAULT NULL, `Unpacked_bytes_sent_tikv_total` bigint DEFAULT NULL, `Unpacked_bytes_received_tikv_total` bigint DEFAULT NULL, `Unpacked_bytes_sent_tikv_cross_zone` bigint DEFAULT NULL, `Unpacked_bytes_received_tikv_cross_zone` bigint DEFAULT NULL, `Unpacked_bytes_sent_tiflash_total` bigint DEFAULT NULL, `Unpacked_bytes_received_tiflash_total` bigint DEFAULT NULL, `Unpacked_bytes_sent_tiflash_cross_zone` bigint DEFAULT NULL, `Unpacked_bytes_received_tiflash_cross_zone` bigint DEFAULT NULL, `Plan` longtext DEFAULT NULL, `Plan_digest` varchar(128) DEFAULT NULL, `Binary_plan` longtext DEFAULT NULL, `Prev_stmt` longtext DEFAULT NULL, `Query` longtext DEFAULT NULL, PRIMARY KEY (`Time`) /*T![clustered_index] CLUSTERED */ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; ================================================ FILE: tests/util/compatibility.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package util import ( "os" "github.com/Masterminds/semver" "github.com/stretchr/testify/require" ) // CheckTiDBVersion tests if tidb version satisfies the constraints. // Constraint examples: "~5.2.2", ">= 5.3.0", see github.com/Masterminds/semver to get more information. func CheckTiDBVersion(r *require.Assertions, constraint string) bool { tidbVersion := os.Getenv("TIDB_VERSION") if tidbVersion == "" { return false } c, err := semver.NewConstraint(constraint) r.NoError(err) v, err := semver.NewVersion(tidbVersion) r.NoError(err) return c.Check(v) } ================================================ FILE: tests/util/dump/dump.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package main import ( "fmt" "log" "os" "strings" "github.com/shhdgit/testfixtures/v3" "gorm.io/driver/mysql" "gorm.io/gorm" ) func main() { if len(os.Args) < 1 { log.Fatal("Require 1 arg db.table") } dbTable := os.Args[1] s := strings.Split(dbTable, ".") dbName := s[0] tableName := s[1] gormDB, err := gorm.Open(mysql.Open(fmt.Sprintf("root:@tcp(127.0.0.1:4000)/%s?charset=utf8&parseTime=True&loc=Local", dbName))) if err != nil { panic(err) } db, err := gormDB.DB() if err != nil { panic(err) } dumper, err := testfixtures.NewDumper( testfixtures.DumpDatabase(db), testfixtures.DumpDialect("tidb"), testfixtures.DumpDirectory("tests/fixtures"), testfixtures.DumpTables( tableName, ), ) if err != nil { panic(err) } if err := dumper.Dump(); err != nil { panic(err) } } ================================================ FILE: tests/util/fixtures.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package util import ( "testing" "github.com/shhdgit/testfixtures/v3" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/testutil" ) func LoadFixtures(t *testing.T, testDB *testutil.TestDB, dir string) { r := require.New(t) db, err := testDB.Gorm().DB() r.NoError(err) fixtures, err := testfixtures.New( testfixtures.Database(db), testfixtures.Dialect("tidb"), testfixtures.Directory(dir), ) r.NoError(err) err = fixtures.Load() r.NoError(err) } ================================================ FILE: tests/util/gin_test_helper.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package util import ( "net/http" "net/http/httptest" "github.com/gin-gonic/gin" ) func TestReqWithHandlers(req *http.Request, handlers ...gin.HandlerFunc) (*gin.Context, *httptest.ResponseRecorder) { w := httptest.NewRecorder() c, e := gin.CreateTestContext(w) c.Request = req e.Handle(req.Method, req.URL.Path, handlers...) e.HandleContext(c) return c, w } ================================================ FILE: tests/util/mock_app.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package util import ( "context" "time" "go.uber.org/fx" "go.uber.org/fx/fxtest" "github.com/pingcap/tidb-dashboard/pkg/apiserver" "github.com/pingcap/tidb-dashboard/pkg/config" "github.com/pingcap/tidb-dashboard/util/featureflag" ) type App struct { *fxtest.App tb fxtest.TB } func NewMockApp(tb fxtest.TB, tidbVersion string, c *config.Config, opts ...fx.Option) *App { allOpts := make([]fx.Option, 0, len(opts)+1) allOpts = append(allOpts, apiserver.Modules, fx.Supply(featureflag.NewRegistry(tidbVersion)), fx.Supply(c), ) allOpts = append(allOpts, opts...) app := fxtest.New(tb, allOpts...) return &App{ App: app, tb: tb, } } // RequireStart calls Start, failing the test if an error is encountered. // It also sleep 5 seconds to wait for the server to start. func (app *App) RequireStart() *App { if err := app.Start(context.Background()); err != nil { app.tb.Errorf("application didn't start cleanly: %v", err) app.tb.FailNow() } time.Sleep(5 * time.Second) return app } // RequireStop calls Stop, failing the test if an error is encountered. func (app *App) RequireStop() { if err := app.Stop(context.Background()); err != nil { app.tb.Errorf("application didn't stop cleanly: %v", err) app.tb.FailNow() } } ================================================ FILE: tests/util/tidb_version.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package util import ( "strings" "testing" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/testutil" ) func GetTiDBVersion(t *testing.T, testDB *testutil.TestDB) string { r := require.New(t) // get tidb version type versionSchemas struct { Version string `gorm:"column:version"` } var result []versionSchemas err := testDB.Gorm().Raw("select version() as version").Scan(&result).Error // output example: // +--------------------+ // | version | // +--------------------+ // | 5.7.25-TiDB-v5.3.0 | // +--------------------+ r.Nil(err) r.Len(result, 1) ver := strings.Split(result[0].Version, "-TiDB-") r.Len(ver, 2) return ver[1] } ================================================ FILE: ui/.editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: ui/.eslintrc.js ================================================ module.exports = { extends: ['react-app'], rules: { 'react/react-in-jsx-scope': 'error', 'jsx-a11y/anchor-is-valid': 'off', 'jsx-a11y/alt-text': 'off', 'import/no-anonymous-default-export': 'off', 'react/jsx-no-target-blank': 'off' } } ================================================ FILE: ui/.gitignore ================================================ node_modules dist build .eslintcache # testing coverage cypress/downloads cypress/videos cypress/screeshots cypress/integration/1-getting-started cypress/integration/2-advanced-examples .nyc_output *.log .env.local ================================================ FILE: ui/.husky/pre-commit ================================================ #!/bin/sh cd ui pnpm lint-staged ================================================ FILE: ui/.prettierignore ================================================ packages/clinic-client/src packages/clinic-client/swagger/spec.json packages/clinic-client/dist packages/tidb-dashboard-client/src packages/tidb-dashboard-client/swagger/spec.json packages/tidb-dashboard-client/dist packages/tidb-dashboard-lib/src/client/models.ts packages/tidb-dashboard-lib/dist packages/tidb-dashboard-for-op/src/utils/distro/strings_res.json packages/tidb-dashboard-for-op/public/speedscope packages/tidb-dashboard-for-op/dist packages/tidb-dashboard-for-op/.nyc_output packages/tidb-dashboard-for-op/coverage pnpm-lock.yaml ================================================ FILE: ui/.prettierrc ================================================ { "semi": false, "trailingComma": "none", "singleQuote": true } ================================================ FILE: ui/OWNERS ================================================ # See the OWNERS docs at https://go.k8s.io/owners labels: - area/frontend ================================================ FILE: ui/README.md ================================================ # TiDB Dashboard UI ## Arch ![ui arch](./ui_arch.png) ## Requirements - Node >= 22.0.0 - [use corepack](https://www.totaltypescript.com/how-to-use-corepack): `corepack enable && corepack enable npm` ## Run ### Dev 1. `pnpm i` 1. `pnpm dev` > Note: > > You can run `pnpm dev:op`, `pnpm dev:clinic-op`, `pnpm dev:clinic-cloud` only to start a specific dashboard variant, while `pnpm dev` starts all of them. > > Copy `.env.development` to `.env.local` in the variant folder to override the environment variables, and add `TARGET_VARIANT_DASHBOARD_PATH` to the `.env.local` file. e.g. `cp .env.development .env.local` in `packages/tidb-dashboard-for-clinic-cloud` to override the environment variables for clinic-cloud. > > Before starting `pnpm dev:clinic-op` and `pnpm dev:clinic-cloud`, you need to start clinic ui. ### Build 1. `pnpm i` 1. `pnpm build` ================================================ FILE: ui/go.mod ================================================ module ignore_ui // a hack to ignore this directory in go commands go 1.13 ================================================ FILE: ui/less-vars.js ================================================ const lessModifyVars = { '@primary-color': '#0ca6f2', '@body-background': '#fff', '@tooltip-bg': 'rgba(0, 0, 0, 0.9)', '@tooltip-max-width': '500px' } const lessGlobalVars = { '@padding-page': '48px', '@gray-1': '#fff', '@gray-2': '#fafafa', '@gray-3': '#f5f5f5', '@gray-4': '#f0f0f0', '@gray-5': '#d9d9d9', '@gray-6': '#bfbfbf', '@gray-7': '#8c8c8c', '@gray-8': '#595959', '@gray-9': '#262626', '@gray-10': '#000' } module.exports = { lessModifyVars, lessGlobalVars } ================================================ FILE: ui/netlify.toml ================================================ [[headers]] # Define which paths this specific [[headers]] block will cover. for = "/*" [headers.values] Access-Control-Allow-Origin = "*" ================================================ FILE: ui/package.json ================================================ { "name": "tidb-dashboard-ui", "private": "true", "version": "1.0.0", "license": "MIT", "engines": { "node": ">=22.0.0" }, "packageManager": "pnpm@8.6.12", "scripts": { "fmt-check": "prettier --check .", "fmt-fix": "prettier --write .", "prepare": "cd .. && husky install ui/.husky", "build:lib": "pnpm -r --filter @pingcap/tidb-dashboard-lib build", "dev": "pnpm build:lib && pnpm -r --parallel dev", "dev:op": "pnpm build:lib && pnpm -r --parallel --filter @pingcap/tidb-dashboard-for-op... dev", "dev:clinic-op": "pnpm build:lib && pnpm -r --parallel --filter @pingcap/tidb-dashboard-for-clinic-op... dev", "dev:clinic-cloud": "pnpm build:lib && pnpm -r --parallel --filter @pingcap/tidb-dashboard-for-clinic-cloud... dev", "dev:watch_api": "WATCH_API=1 pnpm dev", "build": "pnpm -r build" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^4.31.0", "@typescript-eslint/parser": "^4.31.0", "eslint": "^8.9.0", "eslint-config-react-app": "^7.0.0", "husky": "^8.0.0", "lint-staged": "^11.1.2", "prettier": "^2.4.0" }, "pnpm": { "overrides": { "@babel/helpers": "7.26.10", "@babel/runtime": "7.26.10", "@babel/traverse": "7.23.2", "braces@3.0.2": "3.0.3", "cross-spawn@6.0.5": "6.0.6", "cross-spawn@7.0.3": "7.0.5", "decode-uri-component@0.2.0": "0.2.1", "luxon": "1.28.1", "@nestjs/common>axios": "0.30.2", "ua-parser-js": "0.7.33", "ws@6.2.2": "7.5.10", "ws@7.5.9": "7.5.10" } }, "lint-staged": { "*.+(ts|tsx|js)": [ "eslint --fix", "prettier --write" ], "*.+(json|css|md|html)": "prettier --write" }, "husky": { "hooks": { "pre-commit": "lint-staged" } } } ================================================ FILE: ui/packages/clinic-client/gulpfile.js ================================================ const { task, series } = require('gulp') const shell = require('gulp-shell') /////////////////////////// task('swagger:gen', shell.task('./swagger/gen_api.sh')) /////////////////////////// task('tsc:watch', shell.task('tsc -w')) task('tsc:build', shell.task('tsc')) /////////////////////////// if (process.env.SKIP_GEN_API === '1') { task('dev', series('tsc:build')) } else { task('dev', series('swagger:gen', 'tsc:build')) } // in netlify or vercel, we only build frontend, we don't need to generate api, else it will fail because we don't have go and java if (process.env.SKIP_GEN_API === '1') { task('build', shell.task('echo "skip gen api" & tsc')) } else { task('build', series('swagger:gen', 'tsc:build')) } ================================================ FILE: ui/packages/clinic-client/openapitools.json ================================================ { "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { "version": "5.4.0" } } ================================================ FILE: ui/packages/clinic-client/package.json ================================================ { "name": "@pingcap/clinic-client", "version": "1.0.0", "description": "", "private": true, "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "dev": "gulp dev", "build": "gulp build" }, "author": "", "license": "ISC", "devDependencies": { "@openapitools/openapi-generator-cli": "^2.5.1", "gulp": "^4.0.2", "gulp-shell": "^0.8.0", "typescript": "^4.7.3" }, "dependencies": { "axios": "^1.12.0" } } ================================================ FILE: ui/packages/clinic-client/src/client/api/api.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Clinic Example API * This is a Clinic server. * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { Configuration } from './configuration'; import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; // Some imports not used depending on template conditions // @ts-ignore import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base'; /** * DefaultApi - axios parameter creator * @export */ export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { return { /** * get slow log list in cluster * @summary get slow log list * @param {string} xCsrfToken get value from login.ValidationResp response * @param {string} oid organization id * @param {string} itemID package id * @param {string} cid cluster id * @param {number} [beginTime] start time * @param {number} [endTime] end time * @param {Array} [db] db list * @param {number} [limit] limit * @param {string} [text] text * @param {string} [orderBy] orderBy * @param {boolean} [desc] desc * @param {Array} [plans] plans * @param {string} [digest] digest * @param {*} [options] Override http request option. * @throws {RequiredError} */ orgsOidClustersCidSlowqueriesGet: async (xCsrfToken: string, oid: string, itemID: string, cid: string, beginTime?: number, endTime?: number, db?: Array, limit?: number, text?: string, orderBy?: string, desc?: boolean, plans?: Array, digest?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'xCsrfToken' is not null or undefined assertParamExists('orgsOidClustersCidSlowqueriesGet', 'xCsrfToken', xCsrfToken) // verify required parameter 'oid' is not null or undefined assertParamExists('orgsOidClustersCidSlowqueriesGet', 'oid', oid) // verify required parameter 'itemID' is not null or undefined assertParamExists('orgsOidClustersCidSlowqueriesGet', 'itemID', itemID) // verify required parameter 'cid' is not null or undefined assertParamExists('orgsOidClustersCidSlowqueriesGet', 'cid', cid) const localVarPath = `/orgs/{oid}/clusters/{cid}/slowqueries` .replace(`{${"oid"}}`, encodeURIComponent(String(oid))) .replace(`{${"cid"}}`, encodeURIComponent(String(cid))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; if (itemID !== undefined) { localVarQueryParameter['itemID'] = itemID; } if (beginTime !== undefined) { localVarQueryParameter['begin_time'] = beginTime; } if (endTime !== undefined) { localVarQueryParameter['end_time'] = endTime; } if (db) { localVarQueryParameter['db'] = db.join(COLLECTION_FORMATS.csv); } if (limit !== undefined) { localVarQueryParameter['limit'] = limit; } if (text !== undefined) { localVarQueryParameter['text'] = text; } if (orderBy !== undefined) { localVarQueryParameter['orderBy'] = orderBy; } if (desc !== undefined) { localVarQueryParameter['desc'] = desc; } if (plans) { localVarQueryParameter['plans'] = plans.join(COLLECTION_FORMATS.csv); } if (digest !== undefined) { localVarQueryParameter['digest'] = digest; } if (xCsrfToken !== undefined && xCsrfToken !== null) { localVarHeaderParameter['x-csrf-token'] = String(xCsrfToken); } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * get slow log detail in cluster * @summary get slow log detail * @param {string} xCsrfToken get value from login.ValidationResp response * @param {string} oid organization id * @param {string} itemID package id * @param {string} cid cluster id * @param {string} queryid log id * @param {*} [options] Override http request option. * @throws {RequiredError} */ orgsOidClustersCidSlowqueriesQueryidGet: async (xCsrfToken: string, oid: string, itemID: string, cid: string, queryid: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'xCsrfToken' is not null or undefined assertParamExists('orgsOidClustersCidSlowqueriesQueryidGet', 'xCsrfToken', xCsrfToken) // verify required parameter 'oid' is not null or undefined assertParamExists('orgsOidClustersCidSlowqueriesQueryidGet', 'oid', oid) // verify required parameter 'itemID' is not null or undefined assertParamExists('orgsOidClustersCidSlowqueriesQueryidGet', 'itemID', itemID) // verify required parameter 'cid' is not null or undefined assertParamExists('orgsOidClustersCidSlowqueriesQueryidGet', 'cid', cid) // verify required parameter 'queryid' is not null or undefined assertParamExists('orgsOidClustersCidSlowqueriesQueryidGet', 'queryid', queryid) const localVarPath = `/orgs/{oid}/clusters/{cid}/slowqueries/{queryid}` .replace(`{${"oid"}}`, encodeURIComponent(String(oid))) .replace(`{${"cid"}}`, encodeURIComponent(String(cid))) .replace(`{${"queryid"}}`, encodeURIComponent(String(queryid))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; if (itemID !== undefined) { localVarQueryParameter['itemID'] = itemID; } if (xCsrfToken !== undefined && xCsrfToken !== null) { localVarHeaderParameter['x-csrf-token'] = String(xCsrfToken); } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, } }; /** * DefaultApi - functional programming interface * @export */ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) return { /** * get slow log list in cluster * @summary get slow log list * @param {string} xCsrfToken get value from login.ValidationResp response * @param {string} oid organization id * @param {string} itemID package id * @param {string} cid cluster id * @param {number} [beginTime] start time * @param {number} [endTime] end time * @param {Array} [db] db list * @param {number} [limit] limit * @param {string} [text] text * @param {string} [orderBy] orderBy * @param {boolean} [desc] desc * @param {Array} [plans] plans * @param {string} [digest] digest * @param {*} [options] Override http request option. * @throws {RequiredError} */ async orgsOidClustersCidSlowqueriesGet(xCsrfToken: string, oid: string, itemID: string, cid: string, beginTime?: number, endTime?: number, db?: Array, limit?: number, text?: string, orderBy?: string, desc?: boolean, plans?: Array, digest?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.orgsOidClustersCidSlowqueriesGet(xCsrfToken, oid, itemID, cid, beginTime, endTime, db, limit, text, orderBy, desc, plans, digest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * get slow log detail in cluster * @summary get slow log detail * @param {string} xCsrfToken get value from login.ValidationResp response * @param {string} oid organization id * @param {string} itemID package id * @param {string} cid cluster id * @param {string} queryid log id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async orgsOidClustersCidSlowqueriesQueryidGet(xCsrfToken: string, oid: string, itemID: string, cid: string, queryid: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.orgsOidClustersCidSlowqueriesQueryidGet(xCsrfToken, oid, itemID, cid, queryid, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } }; /** * DefaultApi - factory interface * @export */ export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = DefaultApiFp(configuration) return { /** * get slow log list in cluster * @summary get slow log list * @param {string} xCsrfToken get value from login.ValidationResp response * @param {string} oid organization id * @param {string} itemID package id * @param {string} cid cluster id * @param {number} [beginTime] start time * @param {number} [endTime] end time * @param {Array} [db] db list * @param {number} [limit] limit * @param {string} [text] text * @param {string} [orderBy] orderBy * @param {boolean} [desc] desc * @param {Array} [plans] plans * @param {string} [digest] digest * @param {*} [options] Override http request option. * @throws {RequiredError} */ orgsOidClustersCidSlowqueriesGet(xCsrfToken: string, oid: string, itemID: string, cid: string, beginTime?: number, endTime?: number, db?: Array, limit?: number, text?: string, orderBy?: string, desc?: boolean, plans?: Array, digest?: string, options?: any): AxiosPromise> { return localVarFp.orgsOidClustersCidSlowqueriesGet(xCsrfToken, oid, itemID, cid, beginTime, endTime, db, limit, text, orderBy, desc, plans, digest, options).then((request) => request(axios, basePath)); }, /** * get slow log detail in cluster * @summary get slow log detail * @param {string} xCsrfToken get value from login.ValidationResp response * @param {string} oid organization id * @param {string} itemID package id * @param {string} cid cluster id * @param {string} queryid log id * @param {*} [options] Override http request option. * @throws {RequiredError} */ orgsOidClustersCidSlowqueriesQueryidGet(xCsrfToken: string, oid: string, itemID: string, cid: string, queryid: string, options?: any): AxiosPromise { return localVarFp.orgsOidClustersCidSlowqueriesQueryidGet(xCsrfToken, oid, itemID, cid, queryid, options).then((request) => request(axios, basePath)); }, }; }; /** * Request parameters for orgsOidClustersCidSlowqueriesGet operation in DefaultApi. * @export * @interface DefaultApiOrgsOidClustersCidSlowqueriesGetRequest */ export interface DefaultApiOrgsOidClustersCidSlowqueriesGetRequest { /** * get value from login.ValidationResp response * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly xCsrfToken: string /** * organization id * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly oid: string /** * package id * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly itemID: string /** * cluster id * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly cid: string /** * start time * @type {number} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly beginTime?: number /** * end time * @type {number} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly endTime?: number /** * db list * @type {Array} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly db?: Array /** * limit * @type {number} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly limit?: number /** * text * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly text?: string /** * orderBy * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly orderBy?: string /** * desc * @type {boolean} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly desc?: boolean /** * plans * @type {Array} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly plans?: Array /** * digest * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesGet */ readonly digest?: string } /** * Request parameters for orgsOidClustersCidSlowqueriesQueryidGet operation in DefaultApi. * @export * @interface DefaultApiOrgsOidClustersCidSlowqueriesQueryidGetRequest */ export interface DefaultApiOrgsOidClustersCidSlowqueriesQueryidGetRequest { /** * get value from login.ValidationResp response * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesQueryidGet */ readonly xCsrfToken: string /** * organization id * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesQueryidGet */ readonly oid: string /** * package id * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesQueryidGet */ readonly itemID: string /** * cluster id * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesQueryidGet */ readonly cid: string /** * log id * @type {string} * @memberof DefaultApiOrgsOidClustersCidSlowqueriesQueryidGet */ readonly queryid: string } /** * DefaultApi - object-oriented interface * @export * @class DefaultApi * @extends {BaseAPI} */ export class DefaultApi extends BaseAPI { /** * get slow log list in cluster * @summary get slow log list * @param {DefaultApiOrgsOidClustersCidSlowqueriesGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public orgsOidClustersCidSlowqueriesGet(requestParameters: DefaultApiOrgsOidClustersCidSlowqueriesGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).orgsOidClustersCidSlowqueriesGet(requestParameters.xCsrfToken, requestParameters.oid, requestParameters.itemID, requestParameters.cid, requestParameters.beginTime, requestParameters.endTime, requestParameters.db, requestParameters.limit, requestParameters.text, requestParameters.orderBy, requestParameters.desc, requestParameters.plans, requestParameters.digest, options).then((request) => request(this.axios, this.basePath)); } /** * get slow log detail in cluster * @summary get slow log detail * @param {DefaultApiOrgsOidClustersCidSlowqueriesQueryidGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public orgsOidClustersCidSlowqueriesQueryidGet(requestParameters: DefaultApiOrgsOidClustersCidSlowqueriesQueryidGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).orgsOidClustersCidSlowqueriesQueryidGet(requestParameters.xCsrfToken, requestParameters.oid, requestParameters.itemID, requestParameters.cid, requestParameters.queryid, options).then((request) => request(this.axios, this.basePath)); } } ================================================ FILE: ui/packages/clinic-client/src/client/api/base.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Clinic Example API * This is a Clinic server. * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { Configuration } from "./configuration"; // Some imports not used depending on template conditions // @ts-ignore import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; export const BASE_PATH = "/clinic/api/v1".replace(/\/+$/, ""); /** * * @export */ export const COLLECTION_FORMATS = { csv: ",", ssv: " ", tsv: "\t", pipes: "|", }; /** * * @export * @interface RequestArgs */ export interface RequestArgs { url: string; options: AxiosRequestConfig; } /** * * @export * @class BaseAPI */ export class BaseAPI { protected configuration: Configuration | undefined; constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { if (configuration) { this.configuration = configuration; this.basePath = configuration.basePath || this.basePath; } } }; /** * * @export * @class RequiredError * @extends {Error} */ export class RequiredError extends Error { name: "RequiredError" = "RequiredError"; constructor(public field: string, msg?: string) { super(msg); } } ================================================ FILE: ui/packages/clinic-client/src/client/api/common.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Clinic Example API * This is a Clinic server. * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { Configuration } from "./configuration"; import { RequiredError, RequestArgs } from "./base"; import { AxiosInstance, AxiosResponse } from 'axios'; /** * * @export */ export const DUMMY_BASE_URL = 'https://example.com' /** * * @throws {RequiredError} * @export */ export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { if (paramValue === null || paramValue === undefined) { throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); } } /** * * @export */ export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { if (configuration && configuration.apiKey) { const localVarApiKeyValue = typeof configuration.apiKey === 'function' ? await configuration.apiKey(keyParamName) : await configuration.apiKey; object[keyParamName] = localVarApiKeyValue; } } /** * * @export */ export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { if (configuration && (configuration.username || configuration.password)) { object["auth"] = { username: configuration.username, password: configuration.password }; } } /** * * @export */ export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { if (configuration && configuration.accessToken) { const accessToken = typeof configuration.accessToken === 'function' ? await configuration.accessToken() : await configuration.accessToken; object["Authorization"] = "Bearer " + accessToken; } } /** * * @export */ export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { if (configuration && configuration.accessToken) { const localVarAccessTokenValue = typeof configuration.accessToken === 'function' ? await configuration.accessToken(name, scopes) : await configuration.accessToken; object["Authorization"] = "Bearer " + localVarAccessTokenValue; } } /** * * @export */ export const setSearchParams = function (url: URL, ...objects: any[]) { const searchParams = new URLSearchParams(url.search); for (const object of objects) { for (const key in object) { if (Array.isArray(object[key])) { searchParams.delete(key); for (const item of object[key]) { searchParams.append(key, item); } } else { searchParams.set(key, object[key]); } } } url.search = searchParams.toString(); } /** * * @export */ export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { const nonString = typeof value !== 'string'; const needsSerialization = nonString && configuration && configuration.isJsonMime ? configuration.isJsonMime(requestOptions.headers['Content-Type']) : nonString; return needsSerialization ? JSON.stringify(value !== undefined ? value : {}) : (value || ""); } /** * * @export */ export const toPathString = function (url: URL) { return url.pathname + url.search + url.hash } /** * * @export */ export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url}; return axios.request(axiosRequestArgs); }; } ================================================ FILE: ui/packages/clinic-client/src/client/api/configuration.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Clinic Example API * This is a Clinic server. * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ export interface ConfigurationParameters { apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); username?: string; password?: string; accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); basePath?: string; baseOptions?: any; formDataCtor?: new () => any; } export class Configuration { /** * parameter for apiKey security * @param name security name * @memberof Configuration */ apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); /** * parameter for basic security * * @type {string} * @memberof Configuration */ username?: string; /** * parameter for basic security * * @type {string} * @memberof Configuration */ password?: string; /** * parameter for oauth2 security * @param name security name * @param scopes oauth2 scope * @memberof Configuration */ accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); /** * override base path * * @type {string} * @memberof Configuration */ basePath?: string; /** * base options for axios calls * * @type {any} * @memberof Configuration */ baseOptions?: any; /** * The FormData constructor that will be used to create multipart form data * requests. You can inject this here so that execution environments that * do not support the FormData class can still run the generated client. * * @type {new () => FormData} */ formDataCtor?: new () => any; constructor(param: ConfigurationParameters = {}) { this.apiKey = param.apiKey; this.username = param.username; this.password = param.password; this.accessToken = param.accessToken; this.basePath = param.basePath; this.baseOptions = param.baseOptions; this.formDataCtor = param.formDataCtor; } /** * Check if the given MIME is a JSON MIME. * JSON MIME examples: * application/json * application/json; charset=UTF8 * APPLICATION/JSON * application/vnd.company+json * @param mime - MIME (Multipurpose Internet Mail Extensions) * @return True if the given MIME is JSON, false otherwise. */ public isJsonMime(mime: string): boolean { const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); } } ================================================ FILE: ui/packages/clinic-client/src/client/api/index.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Clinic Example API * This is a Clinic server. * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ export * from "./api"; export * from "./configuration"; ================================================ FILE: ui/packages/clinic-client/swagger/.openapi_config.yaml ================================================ # https://openapi-generator.tech/docs/generators/typescript-axios#config-options enumPropertyNaming: original modelPropertyNaming: original supportsES6: true # Setting this property to true will generate functions with a single argument containing all API endpoint parameters instead of one argument per parameter. useSingleRequestParameter: true # conflicts with `useSingleRequestParameter` # withInterfaces: true # Put the model and api in separate folders and in separate classes # https://github.com/OpenAPITools/openapi-generator/issues/5008#issuecomment-613791804 # withSeparateModelsAndApi: true # apiPackage: api # modelPackage: models ================================================ FILE: ui/packages/clinic-client/swagger/gen_api.sh ================================================ #!/usr/bin/env bash set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" PROJECT_DIR="$(dirname "$DIR")" API_SPEC_DIR=$PROJECT_DIR/swagger/spec.json OPENAPI_CONFIG_DIR=$PROJECT_DIR/swagger/.openapi_config.yaml OUTPUT_DIR=$PROJECT_DIR/src/client/api cd $PROJECT_DIR/swagger # touch spec.json && rm spec.json # curl -o spec.json -fsSL http://clinic-staging-1072990385.us-west-2.elb.amazonaws.com:8085/swagger/doc.json pnpm openapi-generator-cli generate -i $API_SPEC_DIR -g typescript-axios -c $OPENAPI_CONFIG_DIR -o $OUTPUT_DIR rm -rf $OUTPUT_DIR/.openapi-generator rm $OUTPUT_DIR/.gitignore rm $OUTPUT_DIR/.npmignore rm $OUTPUT_DIR/.openapi-generator-ignore rm $OUTPUT_DIR/git_push.sh ================================================ FILE: ui/packages/clinic-client/swagger/spec.json ================================================ { "schemes": [], "swagger": "2.0", "info": { "description": "This is a Clinic server.", "title": "Clinic Example API", "contact": {}, "version": "1.0" }, "host": "", "basePath": "/clinic/api/v1", "paths": { "/orgs/{oid}/clusters/{cid}/slowqueries": { "get": { "description": "get slow log list in cluster", "produces": ["application/json"], "summary": "get slow log list", "parameters": [ { "type": "string", "description": "get value from login.ValidationResp response", "name": "x-csrf-token", "in": "header", "required": true }, { "type": "string", "description": "organization id", "name": "oid", "in": "path", "required": true }, { "type": "string", "description": "package id", "name": "itemID", "in": "query", "required": true }, { "type": "integer", "description": "start time", "name": "begin_time", "in": "query" }, { "type": "integer", "description": "end time", "name": "end_time", "in": "query" }, { "type": "array", "items": { "type": "string" }, "description": "db list", "name": "db", "in": "query" }, { "type": "integer", "description": "limit", "name": "limit", "in": "query" }, { "type": "string", "description": "text", "name": "text", "in": "query" }, { "type": "string", "description": "orderBy", "name": "orderBy", "in": "query" }, { "type": "boolean", "description": "desc", "name": "desc", "in": "query" }, { "type": "array", "items": { "type": "string" }, "description": "plans", "name": "plans", "in": "query" }, { "type": "string", "description": "digest", "name": "digest", "in": "query" }, { "type": "string", "description": "cluster id", "name": "cid", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/cluster.SlowQueryProfile" } } } } } }, "/orgs/{oid}/clusters/{cid}/slowqueries/{queryid}": { "get": { "description": "get slow log detail in cluster", "produces": ["application/json"], "summary": "get slow log detail", "parameters": [ { "type": "string", "description": "get value from login.ValidationResp response", "name": "x-csrf-token", "in": "header", "required": true }, { "type": "string", "description": "organization id", "name": "oid", "in": "path", "required": true }, { "type": "string", "description": "package id", "name": "itemID", "in": "query", "required": true }, { "type": "string", "description": "cluster id", "name": "cid", "in": "path", "required": true }, { "type": "string", "description": "log id", "name": "queryid", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/cluster.SlowQueryProfile" } } } } } }, "definitions": { "cluster.SlowQueryProfile": { "type": "object", "additionalProperties": true } } } ================================================ FILE: ui/packages/clinic-client/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", "declaration": true } } ================================================ FILE: ui/packages/tidb-dashboard-client/gulpfile.js ================================================ const { task, watch, series, parallel } = require('gulp') const shell = require('gulp-shell') /////////////////////////// task( 'swagger:gen_spec', shell.task('../../../scripts/generate_swagger_spec.sh') ) task('swagger:gen_client', shell.task('./swagger/gen_api.sh')) task('swagger:gen', series('swagger:gen_spec', 'swagger:gen_client')) task('swagger:watch', () => watch(['../../../cmd/**/*.go', '../../../pkg/**/*.go'], series('swagger:gen')) ) /////////////////////////// task('tsc:watch', shell.task('tsc -w')) task('tsc:build', shell.task('tsc')) /////////////////////////// if (process.env.WATCH_API === '1') { task('dev', series('swagger:gen', parallel('swagger:watch', 'tsc:watch'))) } else if (process.env.SKIP_GEN_API === '1') { task('dev', series('tsc:build')) } else { // WATCH_API = 0 task('dev', series('swagger:gen', 'tsc:build')) } // in netlify or vercel, we only build frontend, we don't need to generate api, else it will fail because we don't have go and java if (process.env.SKIP_GEN_API === '1') { task('build', shell.task('echo "skip gen api" & tsc')) } else { task('build', series('swagger:gen', 'tsc:build')) } ================================================ FILE: ui/packages/tidb-dashboard-client/openapitools.json ================================================ { "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { "version": "5.4.0" } } ================================================ FILE: ui/packages/tidb-dashboard-client/package.json ================================================ { "name": "@pingcap/tidb-dashboard-client", "version": "1.0.0", "description": "", "private": true, "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "dev": "gulp dev", "build": "gulp build" }, "author": "", "license": "ISC", "devDependencies": { "@openapitools/openapi-generator-cli": "^2.5.1", "gulp": "^4.0.2", "gulp-shell": "^0.8.0", "typescript": "^4.7.3" }, "dependencies": { "axios": "^1.12.0" } } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/api/default-api.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; import { Configuration } from '../configuration'; // Some imports not used depending on template conditions // @ts-ignore import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base'; // @ts-ignore import { ClusterinfoClusterStatistics } from '../models'; // @ts-ignore import { ClusterinfoGetHostsInfoResponse } from '../models'; // @ts-ignore import { ClusterinfoStoreTopologyResponse } from '../models'; // @ts-ignore import { CodeShareRequest } from '../models'; // @ts-ignore import { CodeShareResponse } from '../models'; // @ts-ignore import { ConfigKeyVisualConfig } from '../models'; // @ts-ignore import { ConfigProfilingConfig } from '../models'; // @ts-ignore import { ConfigSSOCoreConfig } from '../models'; // @ts-ignore import { ConfigurationAllConfigItems } from '../models'; // @ts-ignore import { ConfigurationEditRequest } from '../models'; // @ts-ignore import { ConfigurationEditResponse } from '../models'; // @ts-ignore import { ConprofComponent } from '../models'; // @ts-ignore import { ConprofEstimateSizeRes } from '../models'; // @ts-ignore import { ConprofGroupProfileDetail } from '../models'; // @ts-ignore import { ConprofGroupProfiles } from '../models'; // @ts-ignore import { ConprofNgMonitoringConfig } from '../models'; // @ts-ignore import { DeadlockModel } from '../models'; // @ts-ignore import { DiagnoseGenDiagnosisReportRequest } from '../models'; // @ts-ignore import { DiagnoseGenerateMetricsRelationRequest } from '../models'; // @ts-ignore import { DiagnoseGenerateReportRequest } from '../models'; // @ts-ignore import { DiagnoseReport } from '../models'; // @ts-ignore import { DiagnoseTableDef } from '../models'; // @ts-ignore import { EndpointAPIDefinition } from '../models'; // @ts-ignore import { EndpointRequestPayload } from '../models'; // @ts-ignore import { InfoInfoResponse } from '../models'; // @ts-ignore import { InfoTableSchema } from '../models'; // @ts-ignore import { InfoWhoAmIResponse } from '../models'; // @ts-ignore import { LogsearchCreateTaskGroupRequest } from '../models'; // @ts-ignore import { LogsearchPreviewModel } from '../models'; // @ts-ignore import { LogsearchTaskGroupModel } from '../models'; // @ts-ignore import { LogsearchTaskGroupResponse } from '../models'; // @ts-ignore import { MatrixMatrix } from '../models'; // @ts-ignore import { MetricsGetPromAddressConfigResponse } from '../models'; // @ts-ignore import { MetricsPutCustomPromAddressRequest } from '../models'; // @ts-ignore import { MetricsPutCustomPromAddressResponse } from '../models'; // @ts-ignore import { MetricsQueryResponse } from '../models'; // @ts-ignore import { ProfilingGroupDetailResponse } from '../models'; // @ts-ignore import { ProfilingStartRequest } from '../models'; // @ts-ignore import { ProfilingTaskGroupModel } from '../models'; // @ts-ignore import { QueryeditorRunRequest } from '../models'; // @ts-ignore import { QueryeditorRunResponse } from '../models'; // @ts-ignore import { ResourcemanagerCalibrateResponse } from '../models'; // @ts-ignore import { ResourcemanagerGetConfigResponse } from '../models'; // @ts-ignore import { ResourcemanagerResourceInfoRowDef } from '../models'; // @ts-ignore import { RestErrorResponse } from '../models'; // @ts-ignore import { SlowqueryGetListRequest } from '../models'; // @ts-ignore import { SlowqueryModel } from '../models'; // @ts-ignore import { SsoCreateImpersonationRequest } from '../models'; // @ts-ignore import { SsoSSOImpersonationModel } from '../models'; // @ts-ignore import { SsoSetConfigRequest } from '../models'; // @ts-ignore import { StatementBinding } from '../models'; // @ts-ignore import { StatementEditableConfig } from '../models'; // @ts-ignore import { StatementGetStatementsRequest } from '../models'; // @ts-ignore import { StatementModel } from '../models'; // @ts-ignore import { TopologyAlertManagerInfo } from '../models'; // @ts-ignore import { TopologyGrafanaInfo } from '../models'; // @ts-ignore import { TopologyPDInfo } from '../models'; // @ts-ignore import { TopologySchedulingInfo } from '../models'; // @ts-ignore import { TopologyStoreLocation } from '../models'; // @ts-ignore import { TopologyTSOInfo } from '../models'; // @ts-ignore import { TopologyTiCDCInfo } from '../models'; // @ts-ignore import { TopologyTiDBInfo } from '../models'; // @ts-ignore import { TopologyTiProxyInfo } from '../models'; // @ts-ignore import { TopsqlEditableConfig } from '../models'; // @ts-ignore import { TopsqlInstanceResponse } from '../models'; // @ts-ignore import { TopsqlSummaryResponse } from '../models'; // @ts-ignore import { TopsqlTikvNetworkIoCollectionConfig } from '../models'; // @ts-ignore import { TopsqlUpdateTikvNetworkIoCollectionResponse } from '../models'; // @ts-ignore import { UserAuthenticateForm } from '../models'; // @ts-ignore import { UserGetLoginInfoResponse } from '../models'; // @ts-ignore import { UserSignOutInfo } from '../models'; // @ts-ignore import { UserTokenResponse } from '../models'; /** * DefaultApi - axios parameter creator * @export */ export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { return { /** * Cancel all profling tasks with a given group ID * @summary Cancel all tasks with a given group ID * @param {string} groupId group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ cancelProfilingGroup: async (groupId: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'groupId' is not null or undefined assertParamExists('cancelProfilingGroup', 'groupId', groupId) const localVarPath = `/profiling/group/cancel/{groupId}` .replace(`{${"groupId"}}`, encodeURIComponent(String(groupId))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get information of all hosts * @param {*} [options] Override http request option. * @throws {RequiredError} */ clusterInfoGetHostsInfo: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/host/all`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get cluster statistics * @param {*} [options] Override http request option. * @throws {RequiredError} */ clusterInfoGetStatistics: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/host/statistics`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Edit a configuration * @param {ConfigurationEditRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ configurationEdit: async (request: ConfigurationEditRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('configurationEdit', 'request', request) const localVarPath = `/configuration/edit`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get all configurations * @param {*} [options] Override http request option. * @throws {RequiredError} */ configurationGetAll: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/configuration/all`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get action token for download or view profile * @param {string} q target query string * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingActionTokenGet: async (q: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'q' is not null or undefined assertParamExists('continuousProfilingActionTokenGet', 'q', q) const localVarPath = `/continuous_profiling/action_token`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (q !== undefined) { localVarQueryParameter['q'] = q; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get current scraping components * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingComponentsGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/continuous_profiling/components`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get Continuous Profiling Config * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingConfigGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/continuous_profiling/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Update Continuous Profiling Config * @param {ConprofNgMonitoringConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingConfigPost: async (request: ConprofNgMonitoringConfig, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('continuousProfilingConfigPost', 'request', request) const localVarPath = `/continuous_profiling/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Download Group Profile files * @param {number} ts timestamp * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingDownloadGet: async (ts: number, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'ts' is not null or undefined assertParamExists('continuousProfilingDownloadGet', 'ts', ts) const localVarPath = `/continuous_profiling/download`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (ts !== undefined) { localVarQueryParameter['ts'] = ts; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get Estimate Size * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingEstimateSizeGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/continuous_profiling/estimate_size`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get Group Profile Detail * @param {number} ts timestamp * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingGroupProfileDetailGet: async (ts: number, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'ts' is not null or undefined assertParamExists('continuousProfilingGroupProfileDetailGet', 'ts', ts) const localVarPath = `/continuous_profiling/group_profile/detail`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (ts !== undefined) { localVarQueryParameter['ts'] = ts; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get Group Profiles * @param {number} [beginTime] * @param {number} [endTime] * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingGroupProfilesGet: async (beginTime?: number, endTime?: number, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/continuous_profiling/group_profiles`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (beginTime !== undefined) { localVarQueryParameter['begin_time'] = beginTime; } if (endTime !== undefined) { localVarQueryParameter['end_time'] = endTime; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary View Single Profile files * @param {string} [address] * @param {string} [component] * @param {string} [profileType] * @param {number} [ts] * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingSingleProfileViewGet: async (address?: string, component?: string, profileType?: string, ts?: number, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/continuous_profiling/single_profile/view`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (address !== undefined) { localVarQueryParameter['address'] = address; } if (component !== undefined) { localVarQueryParameter['component'] = component; } if (profileType !== undefined) { localVarQueryParameter['profile_type'] = profileType; } if (ts !== undefined) { localVarQueryParameter['ts'] = ts; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary List all deadlock records * @param {*} [options] Override http request option. * @throws {RequiredError} */ deadlockListGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/deadlock/list`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get all endpoints * @param {*} [options] Override http request option. * @throws {RequiredError} */ debugAPIGetEndpoints: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/debug_api/endpoints`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Send request remote endpoint and return a token for downloading results * @param {EndpointRequestPayload} req request payload * @param {*} [options] Override http request option. * @throws {RequiredError} */ debugAPIRequestEndpoint: async (req: EndpointRequestPayload, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'req' is not null or undefined assertParamExists('debugAPIRequestEndpoint', 'req', req) const localVarPath = `/debug_api/endpoint`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(req, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Download a finished request result * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ debugApiDownloadGet: async (token: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'token' is not null or undefined assertParamExists('debugApiDownloadGet', 'token', token) const localVarPath = `/debug_api/download`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; if (token !== undefined) { localVarQueryParameter['token'] = token; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Delete all finished profiling tasks with a given group ID * @summary Delete all tasks with a given group ID * @param {string} groupId group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ deleteProfilingGroup: async (groupId: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'groupId' is not null or undefined assertParamExists('deleteProfilingGroup', 'groupId', groupId) const localVarPath = `/profiling/group/delete/{groupId}` .replace(`{${"groupId"}}`, encodeURIComponent(String(groupId))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Generate sql diagnosis report * @summary SQL diagnosis report * @param {DiagnoseGenDiagnosisReportRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseDiagnosisPost: async (request: DiagnoseGenDiagnosisReportRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('diagnoseDiagnosisPost', 'request', request) const localVarPath = `/diagnose/diagnosis`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Generate metrics relationship graph. * @param {DiagnoseGenerateMetricsRelationRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseGenerateMetricsRelationship: async (request: DiagnoseGenerateMetricsRelationRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('diagnoseGenerateMetricsRelationship', 'request', request) const localVarPath = `/diagnose/metrics_relation/generate`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary View metrics relationship graph. * @param {string} token token * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseMetricsRelationViewGet: async (token: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'token' is not null or undefined assertParamExists('diagnoseMetricsRelationViewGet', 'token', token) const localVarPath = `/diagnose/metrics_relation/view`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; if (token !== undefined) { localVarQueryParameter['token'] = token; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Get sql diagnosis reports history * @summary SQL diagnosis reports history * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseReportsGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/diagnose/reports`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Get sql diagnosis report data * @summary SQL diagnosis report data * @param {string} id report id * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseReportsIdDataJsGet: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('diagnoseReportsIdDataJsGet', 'id', id) const localVarPath = `/diagnose/reports/{id}/data.js` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Get sql diagnosis report HTML * @summary SQL diagnosis report * @param {string} id report id * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseReportsIdDetailGet: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('diagnoseReportsIdDetailGet', 'id', id) const localVarPath = `/diagnose/reports/{id}/detail` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Get diagnosis report status * @summary Diagnosis report status * @param {string} id report id * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseReportsIdStatusGet: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('diagnoseReportsIdStatusGet', 'id', id) const localVarPath = `/diagnose/reports/{id}/status` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Generate sql diagnosis report * @summary SQL diagnosis report * @param {DiagnoseGenerateReportRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseReportsPost: async (request: DiagnoseGenerateReportRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('diagnoseReportsPost', 'request', request) const localVarPath = `/diagnose/reports`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Download all finished profiling results of a task group * @summary Download all results of a task group * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ downloadProfilingGroup: async (token: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'token' is not null or undefined assertParamExists('downloadProfilingGroup', 'token', token) const localVarPath = `/profiling/group/download`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (token !== undefined) { localVarQueryParameter['token'] = token; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Download the finished profiling result of a task * @summary Download the result of a task * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ downloadProfilingSingle: async (token: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'token' is not null or undefined assertParamExists('downloadProfilingSingle', 'token', token) const localVarPath = `/profiling/single/download`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (token !== undefined) { localVarQueryParameter['token'] = token; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Get token with a given group ID or task ID and action type * @summary Get action token for download or view * @param {string} [id] group or task ID * @param {string} [action] action * @param {*} [options] Override http request option. * @throws {RequiredError} */ getActionToken: async (id?: string, action?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/profiling/action_token`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (id !== undefined) { localVarQueryParameter['id'] = id; } if (action !== undefined) { localVarQueryParameter['action'] = action; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get current alert count from AlertManager * @param {string} address ip:port * @param {*} [options] Override http request option. * @throws {RequiredError} */ getAlertManagerCounts: async (address: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'address' is not null or undefined assertParamExists('getAlertManagerCounts', 'address', address) const localVarPath = `/topology/alertmanager/{address}/count` .replace(`{${"address"}}`, encodeURIComponent(String(address))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get AlertManager instance * @param {*} [options] Override http request option. * @throws {RequiredError} */ getAlertManagerTopology: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topology/alertmanager`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get Grafana instance * @param {*} [options] Override http request option. * @throws {RequiredError} */ getGrafanaTopology: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topology/grafana`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get all PD instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getPDTopology: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topology/pd`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * List all profiling tasks with a given group ID * @summary List all tasks with a given group ID * @param {string} groupId group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ getProfilingGroupDetail: async (groupId: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'groupId' is not null or undefined assertParamExists('getProfilingGroupDetail', 'groupId', groupId) const localVarPath = `/profiling/group/detail/{groupId}` .replace(`{${"groupId"}}`, encodeURIComponent(String(groupId))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * List all profiling groups * @summary List all profiling groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ getProfilingGroups: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/profiling/group/list`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get all Scheduling instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getSchedulingTopology: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topology/scheduling`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get location labels of all TiKV / TiFlash instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getStoreLocationTopology: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topology/store_location`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get all TiKV / TiFlash instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getStoreTopology: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topology/store`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get all TSO instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getTSOTopology: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topology/tso`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get all TiCDC instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getTiCDCTopology: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topology/ticdc`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get all TiDB instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getTiDBTopology: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topology/tidb`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get all TiProxy instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getTiProxyTopology: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topology/tiproxy`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get information about this TiDB Dashboard * @param {*} [options] Override http request option. * @throws {RequiredError} */ infoGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/info/info`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary List all databases * @param {*} [options] Override http request option. * @throws {RequiredError} */ infoListDatabases: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/info/databases`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary List tables by database name * @param {string} [databaseName] Database name * @param {*} [options] Override http request option. * @throws {RequiredError} */ infoListTables: async (databaseName?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/info/tables`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (databaseName !== undefined) { localVarQueryParameter['database_name'] = databaseName; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get information about current session * @param {*} [options] Override http request option. * @throws {RequiredError} */ infoWhoami: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/info/whoami`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get Key Visual Dynamic Config * @param {*} [options] Override http request option. * @throws {RequiredError} */ keyvisualConfigGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/keyvisual/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Set Key Visual Dynamic Config * @param {ConfigKeyVisualConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ keyvisualConfigPut: async (request: ConfigKeyVisualConfig, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('keyvisualConfigPut', 'request', request) const localVarPath = `/keyvisual/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Heatmaps in a given range to visualize TiKV usage * @summary Key Visual Heatmaps * @param {string} [startkey] The start of the key range * @param {string} [endkey] The end of the key range * @param {number} [starttime] The start of the time range (Unix) * @param {number} [endtime] The end of the time range (Unix) * @param {'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration'} [type] Main types of data * @param {*} [options] Override http request option. * @throws {RequiredError} */ keyvisualHeatmapsGet: async (startkey?: string, endkey?: string, starttime?: number, endtime?: number, type?: 'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration', options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/keyvisual/heatmaps`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (startkey !== undefined) { localVarQueryParameter['startkey'] = startkey; } if (endkey !== undefined) { localVarQueryParameter['endkey'] = endkey; } if (starttime !== undefined) { localVarQueryParameter['starttime'] = starttime; } if (endtime !== undefined) { localVarQueryParameter['endtime'] = endtime; } if (type !== undefined) { localVarQueryParameter['type'] = type; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Generate a download token for downloading logs * @param {Array} [id] task id * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsDownloadAcquireTokenGet: async (id?: Array, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/logs/download/acquire_token`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (id) { localVarQueryParameter['id'] = id.join(COLLECTION_FORMATS.csv); } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Download logs * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsDownloadGet: async (token: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'token' is not null or undefined assertParamExists('logsDownloadGet', 'token', token) const localVarPath = `/logs/download`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; if (token !== undefined) { localVarQueryParameter['token'] = token; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Create and run a new log search task group * @param {LogsearchCreateTaskGroupRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupPut: async (request: LogsearchCreateTaskGroupRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('logsTaskgroupPut', 'request', request) const localVarPath = `/logs/taskgroup`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary List all log search task groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/logs/taskgroups`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Cancel running tasks in a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsIdCancelPost: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('logsTaskgroupsIdCancelPost', 'id', id) const localVarPath = `/logs/taskgroups/{id}/cancel` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Delete a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsIdDelete: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('logsTaskgroupsIdDelete', 'id', id) const localVarPath = `/logs/taskgroups/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary List tasks in a log search task group * @param {string} id Task Group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsIdGet: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('logsTaskgroupsIdGet', 'id', id) const localVarPath = `/logs/taskgroups/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Preview a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsIdPreviewGet: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('logsTaskgroupsIdPreviewGet', 'id', id) const localVarPath = `/logs/taskgroups/{id}/preview` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Retry failed tasks in a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsIdRetryPost: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('logsTaskgroupsIdRetryPost', 'id', id) const localVarPath = `/logs/taskgroups/{id}/retry` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get the Prometheus address cluster config * @param {*} [options] Override http request option. * @throws {RequiredError} */ metricsGetPromAddress: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/metrics/prom_address`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Query metrics in the given range * @summary Query metrics * @param {number} [endTimeSec] * @param {string} [query] * @param {number} [startTimeSec] * @param {number} [stepSec] * @param {*} [options] Override http request option. * @throws {RequiredError} */ metricsQueryGet: async (endTimeSec?: number, query?: string, startTimeSec?: number, stepSec?: number, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/metrics/query`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (endTimeSec !== undefined) { localVarQueryParameter['end_time_sec'] = endTimeSec; } if (query !== undefined) { localVarQueryParameter['query'] = query; } if (startTimeSec !== undefined) { localVarQueryParameter['start_time_sec'] = startTimeSec; } if (stepSec !== undefined) { localVarQueryParameter['step_sec'] = stepSec; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Set or clear the customized Prometheus address * @param {MetricsPutCustomPromAddressRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ metricsSetCustomPromAddress: async (request: MetricsPutCustomPromAddressRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('metricsSetCustomPromAddress', 'request', request) const localVarPath = `/metrics/prom_address`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get Profiling Dynamic Config * @param {*} [options] Override http request option. * @throws {RequiredError} */ profilingConfigGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/profiling/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Set Profiling Dynamic Config * @param {ConfigProfilingConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ profilingConfigPut: async (request: ConfigProfilingConfig, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('profilingConfigPut', 'request', request) const localVarPath = `/profiling/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Run statements * @param {QueryeditorRunRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ queryEditorRun: async (request: QueryeditorRunRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('queryEditorRun', 'request', request) const localVarPath = `/query_editor/run`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get calibrate of Resource Groups by actual workload * @param {number} [endTime] * @param {number} [startTime] * @param {*} [options] Override http request option. * @throws {RequiredError} */ resourceManagerCalibrateActualGet: async (endTime?: number, startTime?: number, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/resource_manager/calibrate/actual`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (endTime !== undefined) { localVarQueryParameter['end_time'] = endTime; } if (startTime !== undefined) { localVarQueryParameter['start_time'] = startTime; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get calibrate of Resource Groups by hardware deployment * @param {string} workload workload * @param {*} [options] Override http request option. * @throws {RequiredError} */ resourceManagerCalibrateHardwareGet: async (workload: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'workload' is not null or undefined assertParamExists('resourceManagerCalibrateHardwareGet', 'workload', workload) const localVarPath = `/resource_manager/calibrate/hardware`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (workload !== undefined) { localVarQueryParameter['workload'] = workload; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get Resource Control enable config * @param {*} [options] Override http request option. * @throws {RequiredError} */ resourceManagerConfigGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/resource_manager/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get Information of Resource Groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ resourceManagerInformationGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/resource_manager/information`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary List all resource groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ resourceManagerInformationGroupNamesGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/resource_manager/information/group_names`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Get available field names by slowquery table columns * @summary Get available field names * @param {*} [options] Override http request option. * @throws {RequiredError} */ slowQueryAvailableFieldsGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/slow_query/available_fields`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get details of a slow query * @param {string} [connectId] TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. * @param {string} [digest] * @param {number} [timestamp] * @param {*} [options] Override http request option. * @throws {RequiredError} */ slowQueryDetailGet: async (connectId?: string, digest?: string, timestamp?: number, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/slow_query/detail`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (connectId !== undefined) { localVarQueryParameter['connect_id'] = connectId; } if (digest !== undefined) { localVarQueryParameter['digest'] = digest; } if (timestamp !== undefined) { localVarQueryParameter['timestamp'] = timestamp; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Download slow query statements * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ slowQueryDownloadGet: async (token: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'token' is not null or undefined assertParamExists('slowQueryDownloadGet', 'token', token) const localVarPath = `/slow_query/download`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; if (token !== undefined) { localVarQueryParameter['token'] = token; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Generate a download token for exported slow query statements * @param {SlowqueryGetListRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ slowQueryDownloadTokenPost: async (request: SlowqueryGetListRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('slowQueryDownloadTokenPost', 'request', request) const localVarPath = `/slow_query/download/token`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary List all slow queries * @param {number} [beginTime] * @param {Array} [db] * @param {boolean} [desc] * @param {string} [digest] * @param {number} [endTime] * @param {string} [fields] example: \"Query,Digest\" * @param {number} [limit] * @param {string} [orderBy] * @param {Array} [plans] for showing slow queries in the statement detail page * @param {Array} [resourceGroup] * @param {string} [text] * @param {*} [options] Override http request option. * @throws {RequiredError} */ slowQueryListGet: async (beginTime?: number, db?: Array, desc?: boolean, digest?: string, endTime?: number, fields?: string, limit?: number, orderBy?: string, plans?: Array, resourceGroup?: Array, text?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/slow_query/list`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (beginTime !== undefined) { localVarQueryParameter['begin_time'] = beginTime; } if (db) { localVarQueryParameter['db'] = db; } if (desc !== undefined) { localVarQueryParameter['desc'] = desc; } if (digest !== undefined) { localVarQueryParameter['digest'] = digest; } if (endTime !== undefined) { localVarQueryParameter['end_time'] = endTime; } if (fields !== undefined) { localVarQueryParameter['fields'] = fields; } if (limit !== undefined) { localVarQueryParameter['limit'] = limit; } if (orderBy !== undefined) { localVarQueryParameter['orderBy'] = orderBy; } if (plans) { localVarQueryParameter['plans'] = plans; } if (resourceGroup) { localVarQueryParameter['resource_group'] = resourceGroup; } if (text !== undefined) { localVarQueryParameter['text'] = text; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Start a profiling task group * @summary Start profiling * @param {ProfilingStartRequest} req profiling request * @param {*} [options] Override http request option. * @throws {RequiredError} */ startProfiling: async (req: ProfilingStartRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'req' is not null or undefined assertParamExists('startProfiling', 'req', req) const localVarPath = `/profiling/group/start`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(req, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * Get available field names by statements table columns * @summary Get available field names * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsAvailableFieldsGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/statements/available_fields`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get statement configurations * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsConfigGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/statements/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Update statement configurations * @param {StatementEditableConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsConfigPost: async (request: StatementEditableConfig, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('statementsConfigPost', 'request', request) const localVarPath = `/statements/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Download statements * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsDownloadGet: async (token: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'token' is not null or undefined assertParamExists('statementsDownloadGet', 'token', token) const localVarPath = `/statements/download`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; if (token !== undefined) { localVarQueryParameter['token'] = token; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Generate a download token for exported statements * @param {StatementGetStatementsRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsDownloadTokenPost: async (request: StatementGetStatementsRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('statementsDownloadTokenPost', 'request', request) const localVarPath = `/statements/download/token`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get a list of statements * @param {number} [beginTime] * @param {number} [endTime] * @param {string} [fields] * @param {Array} [resourceGroups] * @param {Array} [schemas] * @param {Array} [stmtTypes] * @param {string} [text] * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsListGet: async (beginTime?: number, endTime?: number, fields?: string, resourceGroups?: Array, schemas?: Array, stmtTypes?: Array, text?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/statements/list`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (beginTime !== undefined) { localVarQueryParameter['begin_time'] = beginTime; } if (endTime !== undefined) { localVarQueryParameter['end_time'] = endTime; } if (fields !== undefined) { localVarQueryParameter['fields'] = fields; } if (resourceGroups) { localVarQueryParameter['resource_groups'] = resourceGroups; } if (schemas) { localVarQueryParameter['schemas'] = schemas; } if (stmtTypes) { localVarQueryParameter['stmt_types'] = stmtTypes; } if (text !== undefined) { localVarQueryParameter['text'] = text; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Drop all manually created bindings for a statement * @param {string} sqlDigest query template ID (a.k.a. sql digest) * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingDelete: async (sqlDigest: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'sqlDigest' is not null or undefined assertParamExists('statementsPlanBindingDelete', 'sqlDigest', sqlDigest) const localVarPath = `/statements/plan/binding`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (sqlDigest !== undefined) { localVarQueryParameter['sql_digest'] = sqlDigest; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get the bound plan digest (if exists) of a statement * @param {string} sqlDigest query template id * @param {number} beginTime begin time * @param {number} endTime end time * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingGet: async (sqlDigest: string, beginTime: number, endTime: number, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'sqlDigest' is not null or undefined assertParamExists('statementsPlanBindingGet', 'sqlDigest', sqlDigest) // verify required parameter 'beginTime' is not null or undefined assertParamExists('statementsPlanBindingGet', 'beginTime', beginTime) // verify required parameter 'endTime' is not null or undefined assertParamExists('statementsPlanBindingGet', 'endTime', endTime) const localVarPath = `/statements/plan/binding`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (sqlDigest !== undefined) { localVarQueryParameter['sql_digest'] = sqlDigest; } if (beginTime !== undefined) { localVarQueryParameter['begin_time'] = beginTime; } if (endTime !== undefined) { localVarQueryParameter['end_time'] = endTime; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Create a binding for a statement and a plan * @param {string} planDigest plan digest id * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingPost: async (planDigest: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'planDigest' is not null or undefined assertParamExists('statementsPlanBindingPost', 'planDigest', planDigest) const localVarPath = `/statements/plan/binding`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (planDigest !== undefined) { localVarQueryParameter['plan_digest'] = planDigest; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get details of a statement in an execution plan * @param {number} [beginTime] * @param {string} [digest] * @param {number} [endTime] * @param {Array} [plans] * @param {string} [schemaName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanDetailGet: async (beginTime?: number, digest?: string, endTime?: number, plans?: Array, schemaName?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/statements/plan/detail`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (beginTime !== undefined) { localVarQueryParameter['begin_time'] = beginTime; } if (digest !== undefined) { localVarQueryParameter['digest'] = digest; } if (endTime !== undefined) { localVarQueryParameter['end_time'] = endTime; } if (plans) { localVarQueryParameter['plans'] = plans; } if (schemaName !== undefined) { localVarQueryParameter['schema_name'] = schemaName; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get execution plans of a statement * @param {number} [beginTime] * @param {string} [digest] * @param {number} [endTime] * @param {string} [schemaName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlansGet: async (beginTime?: number, digest?: string, endTime?: number, schemaName?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/statements/plans`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (beginTime !== undefined) { localVarQueryParameter['begin_time'] = beginTime; } if (digest !== undefined) { localVarQueryParameter['digest'] = digest; } if (endTime !== undefined) { localVarQueryParameter['end_time'] = endTime; } if (schemaName !== undefined) { localVarQueryParameter['schema_name'] = schemaName; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get all statement types * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsStmtTypesGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/statements/stmt_types`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Hide a TiDB instance * @param {string} address ip:port * @param {*} [options] Override http request option. * @throws {RequiredError} */ topologyTidbAddressDelete: async (address: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'address' is not null or undefined assertParamExists('topologyTidbAddressDelete', 'address', address) const localVarPath = `/topology/tidb/{address}` .replace(`{${"address"}}`, encodeURIComponent(String(address))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get Top SQL config * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlConfigGet: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topsql/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Update Top SQL config * @param {TopsqlEditableConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlConfigPost: async (request: TopsqlEditableConfig, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('topsqlConfigPost', 'request', request) const localVarPath = `/topsql/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get TiKV network IO collection config * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlGetTiKVNetworkIOCollection: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topsql/tikv_network_io_collection`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get available instances * @param {string} [dataSource] * @param {string} [end] * @param {string} [start] * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlInstancesGet: async (dataSource?: string, end?: string, start?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topsql/instances`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (dataSource !== undefined) { localVarQueryParameter['data_source'] = dataSource; } if (end !== undefined) { localVarQueryParameter['end'] = end; } if (start !== undefined) { localVarQueryParameter['start'] = start; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get summaries * @param {string} [dataSource] * @param {string} [end] * @param {string} [groupBy] * @param {string} [instance] * @param {string} [instanceType] * @param {string} [orderBy] * @param {string} [start] * @param {string} [top] * @param {string} [window] * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlSummaryGet: async (dataSource?: string, end?: string, groupBy?: string, instance?: string, instanceType?: string, orderBy?: string, start?: string, top?: string, window?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topsql/summary`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (dataSource !== undefined) { localVarQueryParameter['data_source'] = dataSource; } if (end !== undefined) { localVarQueryParameter['end'] = end; } if (groupBy !== undefined) { localVarQueryParameter['group_by'] = groupBy; } if (instance !== undefined) { localVarQueryParameter['instance'] = instance; } if (instanceType !== undefined) { localVarQueryParameter['instance_type'] = instanceType; } if (orderBy !== undefined) { localVarQueryParameter['order_by'] = orderBy; } if (start !== undefined) { localVarQueryParameter['start'] = start; } if (top !== undefined) { localVarQueryParameter['top'] = top; } if (window !== undefined) { localVarQueryParameter['window'] = window; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Update TiKV network IO collection config * @param {TopsqlTikvNetworkIoCollectionConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlUpdateTiKVNetworkIOCollection: async (request: TopsqlTikvNetworkIoCollectionConfig, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('topsqlUpdateTiKVNetworkIOCollection', 'request', request) const localVarPath = `/topsql/tikv_network_io_collection`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get log in information, like supported authenticate types * @param {*} [options] Override http request option. * @throws {RequiredError} */ userGetLoginInfo: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/user/login_info`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get sign out info * @param {string} [redirectUrl] * @param {*} [options] Override http request option. * @throws {RequiredError} */ userGetSignOutInfo: async (redirectUrl?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/user/sign_out_info`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (redirectUrl !== undefined) { localVarQueryParameter['redirect_url'] = redirectUrl; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Log in * @param {UserAuthenticateForm} message Credentials * @param {*} [options] Override http request option. * @throws {RequiredError} */ userLogin: async (message: UserAuthenticateForm, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'message' is not null or undefined assertParamExists('userLogin', 'message', message) const localVarPath = `/user/login`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(message, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Reset encryption key to revoke all authorized codes * @param {*} [options] Override http request option. * @throws {RequiredError} */ userRevokeSession: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/user/share/revoke`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Create an impersonation * @param {SsoCreateImpersonationRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ userSSOCreateImpersonation: async (request: SsoCreateImpersonationRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('userSSOCreateImpersonation', 'request', request) const localVarPath = `/user/sso/impersonation`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get SSO Auth URL * @param {string} [codeVerifier] * @param {string} [redirectUrl] * @param {string} [state] * @param {*} [options] Override http request option. * @throws {RequiredError} */ userSSOGetAuthURL: async (codeVerifier?: string, redirectUrl?: string, state?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/user/sso/auth_url`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; if (codeVerifier !== undefined) { localVarQueryParameter['code_verifier'] = codeVerifier; } if (redirectUrl !== undefined) { localVarQueryParameter['redirect_url'] = redirectUrl; } if (state !== undefined) { localVarQueryParameter['state'] = state; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get SSO config * @param {*} [options] Override http request option. * @throws {RequiredError} */ userSSOGetConfig: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/user/sso/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary List all impersonations * @param {*} [options] Override http request option. * @throws {RequiredError} */ userSSOListImpersonations: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/user/sso/impersonations/list`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Set SSO config * @param {SsoSetConfigRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ userSSOSetConfig: async (request: SsoSetConfigRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('userSSOSetConfig', 'request', request) const localVarPath = `/user/sso/config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Share current session and generate a sharing code * @param {CodeShareRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ userShareSession: async (request: CodeShareRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'request' is not null or undefined assertParamExists('userShareSession', 'request', request) const localVarPath = `/user/share/code`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * View the finished profiling result of a task * @summary View the result of a task * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ viewProfilingSingle: async (token: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'token' is not null or undefined assertParamExists('viewProfilingSingle', 'token', token) const localVarPath = `/profiling/single/view`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (token !== undefined) { localVarQueryParameter['token'] = token; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, } }; /** * DefaultApi - functional programming interface * @export */ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) return { /** * Cancel all profling tasks with a given group ID * @summary Cancel all tasks with a given group ID * @param {string} groupId group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ async cancelProfilingGroup(groupId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.cancelProfilingGroup(groupId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get information of all hosts * @param {*} [options] Override http request option. * @throws {RequiredError} */ async clusterInfoGetHostsInfo(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.clusterInfoGetHostsInfo(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get cluster statistics * @param {*} [options] Override http request option. * @throws {RequiredError} */ async clusterInfoGetStatistics(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.clusterInfoGetStatistics(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Edit a configuration * @param {ConfigurationEditRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async configurationEdit(request: ConfigurationEditRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.configurationEdit(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get all configurations * @param {*} [options] Override http request option. * @throws {RequiredError} */ async configurationGetAll(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.configurationGetAll(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get action token for download or view profile * @param {string} q target query string * @param {*} [options] Override http request option. * @throws {RequiredError} */ async continuousProfilingActionTokenGet(q: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.continuousProfilingActionTokenGet(q, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get current scraping components * @param {*} [options] Override http request option. * @throws {RequiredError} */ async continuousProfilingComponentsGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.continuousProfilingComponentsGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get Continuous Profiling Config * @param {*} [options] Override http request option. * @throws {RequiredError} */ async continuousProfilingConfigGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.continuousProfilingConfigGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Update Continuous Profiling Config * @param {ConprofNgMonitoringConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async continuousProfilingConfigPost(request: ConprofNgMonitoringConfig, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.continuousProfilingConfigPost(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Download Group Profile files * @param {number} ts timestamp * @param {*} [options] Override http request option. * @throws {RequiredError} */ async continuousProfilingDownloadGet(ts: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.continuousProfilingDownloadGet(ts, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get Estimate Size * @param {*} [options] Override http request option. * @throws {RequiredError} */ async continuousProfilingEstimateSizeGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.continuousProfilingEstimateSizeGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get Group Profile Detail * @param {number} ts timestamp * @param {*} [options] Override http request option. * @throws {RequiredError} */ async continuousProfilingGroupProfileDetailGet(ts: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.continuousProfilingGroupProfileDetailGet(ts, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get Group Profiles * @param {number} [beginTime] * @param {number} [endTime] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async continuousProfilingGroupProfilesGet(beginTime?: number, endTime?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.continuousProfilingGroupProfilesGet(beginTime, endTime, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary View Single Profile files * @param {string} [address] * @param {string} [component] * @param {string} [profileType] * @param {number} [ts] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async continuousProfilingSingleProfileViewGet(address?: string, component?: string, profileType?: string, ts?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.continuousProfilingSingleProfileViewGet(address, component, profileType, ts, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary List all deadlock records * @param {*} [options] Override http request option. * @throws {RequiredError} */ async deadlockListGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.deadlockListGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get all endpoints * @param {*} [options] Override http request option. * @throws {RequiredError} */ async debugAPIGetEndpoints(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.debugAPIGetEndpoints(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Send request remote endpoint and return a token for downloading results * @param {EndpointRequestPayload} req request payload * @param {*} [options] Override http request option. * @throws {RequiredError} */ async debugAPIRequestEndpoint(req: EndpointRequestPayload, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.debugAPIRequestEndpoint(req, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Download a finished request result * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ async debugApiDownloadGet(token: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.debugApiDownloadGet(token, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Delete all finished profiling tasks with a given group ID * @summary Delete all tasks with a given group ID * @param {string} groupId group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ async deleteProfilingGroup(groupId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.deleteProfilingGroup(groupId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Generate sql diagnosis report * @summary SQL diagnosis report * @param {DiagnoseGenDiagnosisReportRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async diagnoseDiagnosisPost(request: DiagnoseGenDiagnosisReportRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.diagnoseDiagnosisPost(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Generate metrics relationship graph. * @param {DiagnoseGenerateMetricsRelationRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async diagnoseGenerateMetricsRelationship(request: DiagnoseGenerateMetricsRelationRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.diagnoseGenerateMetricsRelationship(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary View metrics relationship graph. * @param {string} token token * @param {*} [options] Override http request option. * @throws {RequiredError} */ async diagnoseMetricsRelationViewGet(token: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.diagnoseMetricsRelationViewGet(token, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Get sql diagnosis reports history * @summary SQL diagnosis reports history * @param {*} [options] Override http request option. * @throws {RequiredError} */ async diagnoseReportsGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.diagnoseReportsGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Get sql diagnosis report data * @summary SQL diagnosis report data * @param {string} id report id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async diagnoseReportsIdDataJsGet(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.diagnoseReportsIdDataJsGet(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Get sql diagnosis report HTML * @summary SQL diagnosis report * @param {string} id report id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async diagnoseReportsIdDetailGet(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.diagnoseReportsIdDetailGet(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Get diagnosis report status * @summary Diagnosis report status * @param {string} id report id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async diagnoseReportsIdStatusGet(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.diagnoseReportsIdStatusGet(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Generate sql diagnosis report * @summary SQL diagnosis report * @param {DiagnoseGenerateReportRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async diagnoseReportsPost(request: DiagnoseGenerateReportRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.diagnoseReportsPost(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Download all finished profiling results of a task group * @summary Download all results of a task group * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ async downloadProfilingGroup(token: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.downloadProfilingGroup(token, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Download the finished profiling result of a task * @summary Download the result of a task * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ async downloadProfilingSingle(token: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.downloadProfilingSingle(token, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Get token with a given group ID or task ID and action type * @summary Get action token for download or view * @param {string} [id] group or task ID * @param {string} [action] action * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getActionToken(id?: string, action?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.getActionToken(id, action, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get current alert count from AlertManager * @param {string} address ip:port * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getAlertManagerCounts(address: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.getAlertManagerCounts(address, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get AlertManager instance * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getAlertManagerTopology(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.getAlertManagerTopology(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get Grafana instance * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getGrafanaTopology(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.getGrafanaTopology(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get all PD instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getPDTopology(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getPDTopology(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * List all profiling tasks with a given group ID * @summary List all tasks with a given group ID * @param {string} groupId group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getProfilingGroupDetail(groupId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.getProfilingGroupDetail(groupId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * List all profiling groups * @summary List all profiling groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getProfilingGroups(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getProfilingGroups(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get all Scheduling instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getSchedulingTopology(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getSchedulingTopology(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get location labels of all TiKV / TiFlash instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getStoreLocationTopology(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.getStoreLocationTopology(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get all TiKV / TiFlash instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getStoreTopology(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.getStoreTopology(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get all TSO instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getTSOTopology(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getTSOTopology(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get all TiCDC instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getTiCDCTopology(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getTiCDCTopology(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get all TiDB instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getTiDBTopology(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getTiDBTopology(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get all TiProxy instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ async getTiProxyTopology(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getTiProxyTopology(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get information about this TiDB Dashboard * @param {*} [options] Override http request option. * @throws {RequiredError} */ async infoGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.infoGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary List all databases * @param {*} [options] Override http request option. * @throws {RequiredError} */ async infoListDatabases(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.infoListDatabases(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary List tables by database name * @param {string} [databaseName] Database name * @param {*} [options] Override http request option. * @throws {RequiredError} */ async infoListTables(databaseName?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.infoListTables(databaseName, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get information about current session * @param {*} [options] Override http request option. * @throws {RequiredError} */ async infoWhoami(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.infoWhoami(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get Key Visual Dynamic Config * @param {*} [options] Override http request option. * @throws {RequiredError} */ async keyvisualConfigGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.keyvisualConfigGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Set Key Visual Dynamic Config * @param {ConfigKeyVisualConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async keyvisualConfigPut(request: ConfigKeyVisualConfig, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.keyvisualConfigPut(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Heatmaps in a given range to visualize TiKV usage * @summary Key Visual Heatmaps * @param {string} [startkey] The start of the key range * @param {string} [endkey] The end of the key range * @param {number} [starttime] The start of the time range (Unix) * @param {number} [endtime] The end of the time range (Unix) * @param {'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration'} [type] Main types of data * @param {*} [options] Override http request option. * @throws {RequiredError} */ async keyvisualHeatmapsGet(startkey?: string, endkey?: string, starttime?: number, endtime?: number, type?: 'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.keyvisualHeatmapsGet(startkey, endkey, starttime, endtime, type, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Generate a download token for downloading logs * @param {Array} [id] task id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async logsDownloadAcquireTokenGet(id?: Array, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.logsDownloadAcquireTokenGet(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Download logs * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ async logsDownloadGet(token: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.logsDownloadGet(token, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Create and run a new log search task group * @param {LogsearchCreateTaskGroupRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async logsTaskgroupPut(request: LogsearchCreateTaskGroupRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.logsTaskgroupPut(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary List all log search task groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ async logsTaskgroupsGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.logsTaskgroupsGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Cancel running tasks in a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async logsTaskgroupsIdCancelPost(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.logsTaskgroupsIdCancelPost(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Delete a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async logsTaskgroupsIdDelete(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.logsTaskgroupsIdDelete(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary List tasks in a log search task group * @param {string} id Task Group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ async logsTaskgroupsIdGet(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.logsTaskgroupsIdGet(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Preview a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async logsTaskgroupsIdPreviewGet(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.logsTaskgroupsIdPreviewGet(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Retry failed tasks in a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async logsTaskgroupsIdRetryPost(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.logsTaskgroupsIdRetryPost(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get the Prometheus address cluster config * @param {*} [options] Override http request option. * @throws {RequiredError} */ async metricsGetPromAddress(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.metricsGetPromAddress(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Query metrics in the given range * @summary Query metrics * @param {number} [endTimeSec] * @param {string} [query] * @param {number} [startTimeSec] * @param {number} [stepSec] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async metricsQueryGet(endTimeSec?: number, query?: string, startTimeSec?: number, stepSec?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.metricsQueryGet(endTimeSec, query, startTimeSec, stepSec, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Set or clear the customized Prometheus address * @param {MetricsPutCustomPromAddressRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async metricsSetCustomPromAddress(request: MetricsPutCustomPromAddressRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.metricsSetCustomPromAddress(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get Profiling Dynamic Config * @param {*} [options] Override http request option. * @throws {RequiredError} */ async profilingConfigGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.profilingConfigGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Set Profiling Dynamic Config * @param {ConfigProfilingConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async profilingConfigPut(request: ConfigProfilingConfig, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.profilingConfigPut(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Run statements * @param {QueryeditorRunRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async queryEditorRun(request: QueryeditorRunRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.queryEditorRun(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get calibrate of Resource Groups by actual workload * @param {number} [endTime] * @param {number} [startTime] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async resourceManagerCalibrateActualGet(endTime?: number, startTime?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.resourceManagerCalibrateActualGet(endTime, startTime, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get calibrate of Resource Groups by hardware deployment * @param {string} workload workload * @param {*} [options] Override http request option. * @throws {RequiredError} */ async resourceManagerCalibrateHardwareGet(workload: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.resourceManagerCalibrateHardwareGet(workload, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get Resource Control enable config * @param {*} [options] Override http request option. * @throws {RequiredError} */ async resourceManagerConfigGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.resourceManagerConfigGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get Information of Resource Groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ async resourceManagerInformationGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.resourceManagerInformationGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary List all resource groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ async resourceManagerInformationGroupNamesGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.resourceManagerInformationGroupNamesGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Get available field names by slowquery table columns * @summary Get available field names * @param {*} [options] Override http request option. * @throws {RequiredError} */ async slowQueryAvailableFieldsGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.slowQueryAvailableFieldsGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get details of a slow query * @param {string} [connectId] TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. * @param {string} [digest] * @param {number} [timestamp] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async slowQueryDetailGet(connectId?: string, digest?: string, timestamp?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.slowQueryDetailGet(connectId, digest, timestamp, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Download slow query statements * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ async slowQueryDownloadGet(token: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.slowQueryDownloadGet(token, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Generate a download token for exported slow query statements * @param {SlowqueryGetListRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async slowQueryDownloadTokenPost(request: SlowqueryGetListRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.slowQueryDownloadTokenPost(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary List all slow queries * @param {number} [beginTime] * @param {Array} [db] * @param {boolean} [desc] * @param {string} [digest] * @param {number} [endTime] * @param {string} [fields] example: \"Query,Digest\" * @param {number} [limit] * @param {string} [orderBy] * @param {Array} [plans] for showing slow queries in the statement detail page * @param {Array} [resourceGroup] * @param {string} [text] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async slowQueryListGet(beginTime?: number, db?: Array, desc?: boolean, digest?: string, endTime?: number, fields?: string, limit?: number, orderBy?: string, plans?: Array, resourceGroup?: Array, text?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.slowQueryListGet(beginTime, db, desc, digest, endTime, fields, limit, orderBy, plans, resourceGroup, text, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Start a profiling task group * @summary Start profiling * @param {ProfilingStartRequest} req profiling request * @param {*} [options] Override http request option. * @throws {RequiredError} */ async startProfiling(req: ProfilingStartRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.startProfiling(req, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * Get available field names by statements table columns * @summary Get available field names * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsAvailableFieldsGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsAvailableFieldsGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get statement configurations * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsConfigGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsConfigGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Update statement configurations * @param {StatementEditableConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsConfigPost(request: StatementEditableConfig, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsConfigPost(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Download statements * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsDownloadGet(token: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsDownloadGet(token, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Generate a download token for exported statements * @param {StatementGetStatementsRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsDownloadTokenPost(request: StatementGetStatementsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsDownloadTokenPost(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get a list of statements * @param {number} [beginTime] * @param {number} [endTime] * @param {string} [fields] * @param {Array} [resourceGroups] * @param {Array} [schemas] * @param {Array} [stmtTypes] * @param {string} [text] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsListGet(beginTime?: number, endTime?: number, fields?: string, resourceGroups?: Array, schemas?: Array, stmtTypes?: Array, text?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsListGet(beginTime, endTime, fields, resourceGroups, schemas, stmtTypes, text, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Drop all manually created bindings for a statement * @param {string} sqlDigest query template ID (a.k.a. sql digest) * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsPlanBindingDelete(sqlDigest: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsPlanBindingDelete(sqlDigest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get the bound plan digest (if exists) of a statement * @param {string} sqlDigest query template id * @param {number} beginTime begin time * @param {number} endTime end time * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsPlanBindingGet(sqlDigest: string, beginTime: number, endTime: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsPlanBindingGet(sqlDigest, beginTime, endTime, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Create a binding for a statement and a plan * @param {string} planDigest plan digest id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsPlanBindingPost(planDigest: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsPlanBindingPost(planDigest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get details of a statement in an execution plan * @param {number} [beginTime] * @param {string} [digest] * @param {number} [endTime] * @param {Array} [plans] * @param {string} [schemaName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsPlanDetailGet(beginTime?: number, digest?: string, endTime?: number, plans?: Array, schemaName?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsPlanDetailGet(beginTime, digest, endTime, plans, schemaName, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get execution plans of a statement * @param {number} [beginTime] * @param {string} [digest] * @param {number} [endTime] * @param {string} [schemaName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsPlansGet(beginTime?: number, digest?: string, endTime?: number, schemaName?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsPlansGet(beginTime, digest, endTime, schemaName, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get all statement types * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsStmtTypesGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsStmtTypesGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Hide a TiDB instance * @param {string} address ip:port * @param {*} [options] Override http request option. * @throws {RequiredError} */ async topologyTidbAddressDelete(address: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.topologyTidbAddressDelete(address, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get Top SQL config * @param {*} [options] Override http request option. * @throws {RequiredError} */ async topsqlConfigGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.topsqlConfigGet(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Update Top SQL config * @param {TopsqlEditableConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async topsqlConfigPost(request: TopsqlEditableConfig, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.topsqlConfigPost(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get TiKV network IO collection config * @param {*} [options] Override http request option. * @throws {RequiredError} */ async topsqlGetTiKVNetworkIOCollection(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.topsqlGetTiKVNetworkIOCollection(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get available instances * @param {string} [dataSource] * @param {string} [end] * @param {string} [start] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async topsqlInstancesGet(dataSource?: string, end?: string, start?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.topsqlInstancesGet(dataSource, end, start, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get summaries * @param {string} [dataSource] * @param {string} [end] * @param {string} [groupBy] * @param {string} [instance] * @param {string} [instanceType] * @param {string} [orderBy] * @param {string} [start] * @param {string} [top] * @param {string} [window] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async topsqlSummaryGet(dataSource?: string, end?: string, groupBy?: string, instance?: string, instanceType?: string, orderBy?: string, start?: string, top?: string, window?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.topsqlSummaryGet(dataSource, end, groupBy, instance, instanceType, orderBy, start, top, window, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Update TiKV network IO collection config * @param {TopsqlTikvNetworkIoCollectionConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async topsqlUpdateTiKVNetworkIOCollection(request: TopsqlTikvNetworkIoCollectionConfig, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.topsqlUpdateTiKVNetworkIOCollection(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get log in information, like supported authenticate types * @param {*} [options] Override http request option. * @throws {RequiredError} */ async userGetLoginInfo(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.userGetLoginInfo(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get sign out info * @param {string} [redirectUrl] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async userGetSignOutInfo(redirectUrl?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.userGetSignOutInfo(redirectUrl, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Log in * @param {UserAuthenticateForm} message Credentials * @param {*} [options] Override http request option. * @throws {RequiredError} */ async userLogin(message: UserAuthenticateForm, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.userLogin(message, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Reset encryption key to revoke all authorized codes * @param {*} [options] Override http request option. * @throws {RequiredError} */ async userRevokeSession(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.userRevokeSession(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Create an impersonation * @param {SsoCreateImpersonationRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async userSSOCreateImpersonation(request: SsoCreateImpersonationRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.userSSOCreateImpersonation(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get SSO Auth URL * @param {string} [codeVerifier] * @param {string} [redirectUrl] * @param {string} [state] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async userSSOGetAuthURL(codeVerifier?: string, redirectUrl?: string, state?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.userSSOGetAuthURL(codeVerifier, redirectUrl, state, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get SSO config * @param {*} [options] Override http request option. * @throws {RequiredError} */ async userSSOGetConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.userSSOGetConfig(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary List all impersonations * @param {*} [options] Override http request option. * @throws {RequiredError} */ async userSSOListImpersonations(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.userSSOListImpersonations(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Set SSO config * @param {SsoSetConfigRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async userSSOSetConfig(request: SsoSetConfigRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.userSSOSetConfig(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Share current session and generate a sharing code * @param {CodeShareRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ async userShareSession(request: CodeShareRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.userShareSession(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * View the finished profiling result of a task * @summary View the result of a task * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ async viewProfilingSingle(token: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.viewProfilingSingle(token, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } }; /** * DefaultApi - factory interface * @export */ export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = DefaultApiFp(configuration) return { /** * Cancel all profling tasks with a given group ID * @summary Cancel all tasks with a given group ID * @param {string} groupId group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ cancelProfilingGroup(groupId: string, options?: any): AxiosPromise { return localVarFp.cancelProfilingGroup(groupId, options).then((request) => request(axios, basePath)); }, /** * * @summary Get information of all hosts * @param {*} [options] Override http request option. * @throws {RequiredError} */ clusterInfoGetHostsInfo(options?: any): AxiosPromise { return localVarFp.clusterInfoGetHostsInfo(options).then((request) => request(axios, basePath)); }, /** * * @summary Get cluster statistics * @param {*} [options] Override http request option. * @throws {RequiredError} */ clusterInfoGetStatistics(options?: any): AxiosPromise { return localVarFp.clusterInfoGetStatistics(options).then((request) => request(axios, basePath)); }, /** * * @summary Edit a configuration * @param {ConfigurationEditRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ configurationEdit(request: ConfigurationEditRequest, options?: any): AxiosPromise { return localVarFp.configurationEdit(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Get all configurations * @param {*} [options] Override http request option. * @throws {RequiredError} */ configurationGetAll(options?: any): AxiosPromise { return localVarFp.configurationGetAll(options).then((request) => request(axios, basePath)); }, /** * * @summary Get action token for download or view profile * @param {string} q target query string * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingActionTokenGet(q: string, options?: any): AxiosPromise { return localVarFp.continuousProfilingActionTokenGet(q, options).then((request) => request(axios, basePath)); }, /** * * @summary Get current scraping components * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingComponentsGet(options?: any): AxiosPromise> { return localVarFp.continuousProfilingComponentsGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Get Continuous Profiling Config * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingConfigGet(options?: any): AxiosPromise { return localVarFp.continuousProfilingConfigGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Update Continuous Profiling Config * @param {ConprofNgMonitoringConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingConfigPost(request: ConprofNgMonitoringConfig, options?: any): AxiosPromise { return localVarFp.continuousProfilingConfigPost(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Download Group Profile files * @param {number} ts timestamp * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingDownloadGet(ts: number, options?: any): AxiosPromise { return localVarFp.continuousProfilingDownloadGet(ts, options).then((request) => request(axios, basePath)); }, /** * * @summary Get Estimate Size * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingEstimateSizeGet(options?: any): AxiosPromise { return localVarFp.continuousProfilingEstimateSizeGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Get Group Profile Detail * @param {number} ts timestamp * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingGroupProfileDetailGet(ts: number, options?: any): AxiosPromise { return localVarFp.continuousProfilingGroupProfileDetailGet(ts, options).then((request) => request(axios, basePath)); }, /** * * @summary Get Group Profiles * @param {number} [beginTime] * @param {number} [endTime] * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingGroupProfilesGet(beginTime?: number, endTime?: number, options?: any): AxiosPromise> { return localVarFp.continuousProfilingGroupProfilesGet(beginTime, endTime, options).then((request) => request(axios, basePath)); }, /** * * @summary View Single Profile files * @param {string} [address] * @param {string} [component] * @param {string} [profileType] * @param {number} [ts] * @param {*} [options] Override http request option. * @throws {RequiredError} */ continuousProfilingSingleProfileViewGet(address?: string, component?: string, profileType?: string, ts?: number, options?: any): AxiosPromise { return localVarFp.continuousProfilingSingleProfileViewGet(address, component, profileType, ts, options).then((request) => request(axios, basePath)); }, /** * * @summary List all deadlock records * @param {*} [options] Override http request option. * @throws {RequiredError} */ deadlockListGet(options?: any): AxiosPromise> { return localVarFp.deadlockListGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Get all endpoints * @param {*} [options] Override http request option. * @throws {RequiredError} */ debugAPIGetEndpoints(options?: any): AxiosPromise> { return localVarFp.debugAPIGetEndpoints(options).then((request) => request(axios, basePath)); }, /** * * @summary Send request remote endpoint and return a token for downloading results * @param {EndpointRequestPayload} req request payload * @param {*} [options] Override http request option. * @throws {RequiredError} */ debugAPIRequestEndpoint(req: EndpointRequestPayload, options?: any): AxiosPromise { return localVarFp.debugAPIRequestEndpoint(req, options).then((request) => request(axios, basePath)); }, /** * * @summary Download a finished request result * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ debugApiDownloadGet(token: string, options?: any): AxiosPromise { return localVarFp.debugApiDownloadGet(token, options).then((request) => request(axios, basePath)); }, /** * Delete all finished profiling tasks with a given group ID * @summary Delete all tasks with a given group ID * @param {string} groupId group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ deleteProfilingGroup(groupId: string, options?: any): AxiosPromise { return localVarFp.deleteProfilingGroup(groupId, options).then((request) => request(axios, basePath)); }, /** * Generate sql diagnosis report * @summary SQL diagnosis report * @param {DiagnoseGenDiagnosisReportRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseDiagnosisPost(request: DiagnoseGenDiagnosisReportRequest, options?: any): AxiosPromise { return localVarFp.diagnoseDiagnosisPost(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Generate metrics relationship graph. * @param {DiagnoseGenerateMetricsRelationRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseGenerateMetricsRelationship(request: DiagnoseGenerateMetricsRelationRequest, options?: any): AxiosPromise { return localVarFp.diagnoseGenerateMetricsRelationship(request, options).then((request) => request(axios, basePath)); }, /** * * @summary View metrics relationship graph. * @param {string} token token * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseMetricsRelationViewGet(token: string, options?: any): AxiosPromise { return localVarFp.diagnoseMetricsRelationViewGet(token, options).then((request) => request(axios, basePath)); }, /** * Get sql diagnosis reports history * @summary SQL diagnosis reports history * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseReportsGet(options?: any): AxiosPromise> { return localVarFp.diagnoseReportsGet(options).then((request) => request(axios, basePath)); }, /** * Get sql diagnosis report data * @summary SQL diagnosis report data * @param {string} id report id * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseReportsIdDataJsGet(id: string, options?: any): AxiosPromise { return localVarFp.diagnoseReportsIdDataJsGet(id, options).then((request) => request(axios, basePath)); }, /** * Get sql diagnosis report HTML * @summary SQL diagnosis report * @param {string} id report id * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseReportsIdDetailGet(id: string, options?: any): AxiosPromise { return localVarFp.diagnoseReportsIdDetailGet(id, options).then((request) => request(axios, basePath)); }, /** * Get diagnosis report status * @summary Diagnosis report status * @param {string} id report id * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseReportsIdStatusGet(id: string, options?: any): AxiosPromise { return localVarFp.diagnoseReportsIdStatusGet(id, options).then((request) => request(axios, basePath)); }, /** * Generate sql diagnosis report * @summary SQL diagnosis report * @param {DiagnoseGenerateReportRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ diagnoseReportsPost(request: DiagnoseGenerateReportRequest, options?: any): AxiosPromise { return localVarFp.diagnoseReportsPost(request, options).then((request) => request(axios, basePath)); }, /** * Download all finished profiling results of a task group * @summary Download all results of a task group * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ downloadProfilingGroup(token: string, options?: any): AxiosPromise { return localVarFp.downloadProfilingGroup(token, options).then((request) => request(axios, basePath)); }, /** * Download the finished profiling result of a task * @summary Download the result of a task * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ downloadProfilingSingle(token: string, options?: any): AxiosPromise { return localVarFp.downloadProfilingSingle(token, options).then((request) => request(axios, basePath)); }, /** * Get token with a given group ID or task ID and action type * @summary Get action token for download or view * @param {string} [id] group or task ID * @param {string} [action] action * @param {*} [options] Override http request option. * @throws {RequiredError} */ getActionToken(id?: string, action?: string, options?: any): AxiosPromise { return localVarFp.getActionToken(id, action, options).then((request) => request(axios, basePath)); }, /** * * @summary Get current alert count from AlertManager * @param {string} address ip:port * @param {*} [options] Override http request option. * @throws {RequiredError} */ getAlertManagerCounts(address: string, options?: any): AxiosPromise { return localVarFp.getAlertManagerCounts(address, options).then((request) => request(axios, basePath)); }, /** * * @summary Get AlertManager instance * @param {*} [options] Override http request option. * @throws {RequiredError} */ getAlertManagerTopology(options?: any): AxiosPromise { return localVarFp.getAlertManagerTopology(options).then((request) => request(axios, basePath)); }, /** * * @summary Get Grafana instance * @param {*} [options] Override http request option. * @throws {RequiredError} */ getGrafanaTopology(options?: any): AxiosPromise { return localVarFp.getGrafanaTopology(options).then((request) => request(axios, basePath)); }, /** * * @summary Get all PD instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getPDTopology(options?: any): AxiosPromise> { return localVarFp.getPDTopology(options).then((request) => request(axios, basePath)); }, /** * List all profiling tasks with a given group ID * @summary List all tasks with a given group ID * @param {string} groupId group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ getProfilingGroupDetail(groupId: string, options?: any): AxiosPromise { return localVarFp.getProfilingGroupDetail(groupId, options).then((request) => request(axios, basePath)); }, /** * List all profiling groups * @summary List all profiling groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ getProfilingGroups(options?: any): AxiosPromise> { return localVarFp.getProfilingGroups(options).then((request) => request(axios, basePath)); }, /** * * @summary Get all Scheduling instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getSchedulingTopology(options?: any): AxiosPromise> { return localVarFp.getSchedulingTopology(options).then((request) => request(axios, basePath)); }, /** * * @summary Get location labels of all TiKV / TiFlash instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getStoreLocationTopology(options?: any): AxiosPromise { return localVarFp.getStoreLocationTopology(options).then((request) => request(axios, basePath)); }, /** * * @summary Get all TiKV / TiFlash instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getStoreTopology(options?: any): AxiosPromise { return localVarFp.getStoreTopology(options).then((request) => request(axios, basePath)); }, /** * * @summary Get all TSO instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getTSOTopology(options?: any): AxiosPromise> { return localVarFp.getTSOTopology(options).then((request) => request(axios, basePath)); }, /** * * @summary Get all TiCDC instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getTiCDCTopology(options?: any): AxiosPromise> { return localVarFp.getTiCDCTopology(options).then((request) => request(axios, basePath)); }, /** * * @summary Get all TiDB instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getTiDBTopology(options?: any): AxiosPromise> { return localVarFp.getTiDBTopology(options).then((request) => request(axios, basePath)); }, /** * * @summary Get all TiProxy instances * @param {*} [options] Override http request option. * @throws {RequiredError} */ getTiProxyTopology(options?: any): AxiosPromise> { return localVarFp.getTiProxyTopology(options).then((request) => request(axios, basePath)); }, /** * * @summary Get information about this TiDB Dashboard * @param {*} [options] Override http request option. * @throws {RequiredError} */ infoGet(options?: any): AxiosPromise { return localVarFp.infoGet(options).then((request) => request(axios, basePath)); }, /** * * @summary List all databases * @param {*} [options] Override http request option. * @throws {RequiredError} */ infoListDatabases(options?: any): AxiosPromise> { return localVarFp.infoListDatabases(options).then((request) => request(axios, basePath)); }, /** * * @summary List tables by database name * @param {string} [databaseName] Database name * @param {*} [options] Override http request option. * @throws {RequiredError} */ infoListTables(databaseName?: string, options?: any): AxiosPromise> { return localVarFp.infoListTables(databaseName, options).then((request) => request(axios, basePath)); }, /** * * @summary Get information about current session * @param {*} [options] Override http request option. * @throws {RequiredError} */ infoWhoami(options?: any): AxiosPromise { return localVarFp.infoWhoami(options).then((request) => request(axios, basePath)); }, /** * * @summary Get Key Visual Dynamic Config * @param {*} [options] Override http request option. * @throws {RequiredError} */ keyvisualConfigGet(options?: any): AxiosPromise { return localVarFp.keyvisualConfigGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Set Key Visual Dynamic Config * @param {ConfigKeyVisualConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ keyvisualConfigPut(request: ConfigKeyVisualConfig, options?: any): AxiosPromise { return localVarFp.keyvisualConfigPut(request, options).then((request) => request(axios, basePath)); }, /** * Heatmaps in a given range to visualize TiKV usage * @summary Key Visual Heatmaps * @param {string} [startkey] The start of the key range * @param {string} [endkey] The end of the key range * @param {number} [starttime] The start of the time range (Unix) * @param {number} [endtime] The end of the time range (Unix) * @param {'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration'} [type] Main types of data * @param {*} [options] Override http request option. * @throws {RequiredError} */ keyvisualHeatmapsGet(startkey?: string, endkey?: string, starttime?: number, endtime?: number, type?: 'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration', options?: any): AxiosPromise { return localVarFp.keyvisualHeatmapsGet(startkey, endkey, starttime, endtime, type, options).then((request) => request(axios, basePath)); }, /** * * @summary Generate a download token for downloading logs * @param {Array} [id] task id * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsDownloadAcquireTokenGet(id?: Array, options?: any): AxiosPromise { return localVarFp.logsDownloadAcquireTokenGet(id, options).then((request) => request(axios, basePath)); }, /** * * @summary Download logs * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsDownloadGet(token: string, options?: any): AxiosPromise { return localVarFp.logsDownloadGet(token, options).then((request) => request(axios, basePath)); }, /** * * @summary Create and run a new log search task group * @param {LogsearchCreateTaskGroupRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupPut(request: LogsearchCreateTaskGroupRequest, options?: any): AxiosPromise { return localVarFp.logsTaskgroupPut(request, options).then((request) => request(axios, basePath)); }, /** * * @summary List all log search task groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsGet(options?: any): AxiosPromise> { return localVarFp.logsTaskgroupsGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Cancel running tasks in a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsIdCancelPost(id: string, options?: any): AxiosPromise { return localVarFp.logsTaskgroupsIdCancelPost(id, options).then((request) => request(axios, basePath)); }, /** * * @summary Delete a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsIdDelete(id: string, options?: any): AxiosPromise { return localVarFp.logsTaskgroupsIdDelete(id, options).then((request) => request(axios, basePath)); }, /** * * @summary List tasks in a log search task group * @param {string} id Task Group ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsIdGet(id: string, options?: any): AxiosPromise { return localVarFp.logsTaskgroupsIdGet(id, options).then((request) => request(axios, basePath)); }, /** * * @summary Preview a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsIdPreviewGet(id: string, options?: any): AxiosPromise> { return localVarFp.logsTaskgroupsIdPreviewGet(id, options).then((request) => request(axios, basePath)); }, /** * * @summary Retry failed tasks in a log search task group * @param {string} id task group id * @param {*} [options] Override http request option. * @throws {RequiredError} */ logsTaskgroupsIdRetryPost(id: string, options?: any): AxiosPromise { return localVarFp.logsTaskgroupsIdRetryPost(id, options).then((request) => request(axios, basePath)); }, /** * * @summary Get the Prometheus address cluster config * @param {*} [options] Override http request option. * @throws {RequiredError} */ metricsGetPromAddress(options?: any): AxiosPromise { return localVarFp.metricsGetPromAddress(options).then((request) => request(axios, basePath)); }, /** * Query metrics in the given range * @summary Query metrics * @param {number} [endTimeSec] * @param {string} [query] * @param {number} [startTimeSec] * @param {number} [stepSec] * @param {*} [options] Override http request option. * @throws {RequiredError} */ metricsQueryGet(endTimeSec?: number, query?: string, startTimeSec?: number, stepSec?: number, options?: any): AxiosPromise { return localVarFp.metricsQueryGet(endTimeSec, query, startTimeSec, stepSec, options).then((request) => request(axios, basePath)); }, /** * * @summary Set or clear the customized Prometheus address * @param {MetricsPutCustomPromAddressRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ metricsSetCustomPromAddress(request: MetricsPutCustomPromAddressRequest, options?: any): AxiosPromise { return localVarFp.metricsSetCustomPromAddress(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Get Profiling Dynamic Config * @param {*} [options] Override http request option. * @throws {RequiredError} */ profilingConfigGet(options?: any): AxiosPromise { return localVarFp.profilingConfigGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Set Profiling Dynamic Config * @param {ConfigProfilingConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ profilingConfigPut(request: ConfigProfilingConfig, options?: any): AxiosPromise { return localVarFp.profilingConfigPut(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Run statements * @param {QueryeditorRunRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ queryEditorRun(request: QueryeditorRunRequest, options?: any): AxiosPromise { return localVarFp.queryEditorRun(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Get calibrate of Resource Groups by actual workload * @param {number} [endTime] * @param {number} [startTime] * @param {*} [options] Override http request option. * @throws {RequiredError} */ resourceManagerCalibrateActualGet(endTime?: number, startTime?: number, options?: any): AxiosPromise { return localVarFp.resourceManagerCalibrateActualGet(endTime, startTime, options).then((request) => request(axios, basePath)); }, /** * * @summary Get calibrate of Resource Groups by hardware deployment * @param {string} workload workload * @param {*} [options] Override http request option. * @throws {RequiredError} */ resourceManagerCalibrateHardwareGet(workload: string, options?: any): AxiosPromise { return localVarFp.resourceManagerCalibrateHardwareGet(workload, options).then((request) => request(axios, basePath)); }, /** * * @summary Get Resource Control enable config * @param {*} [options] Override http request option. * @throws {RequiredError} */ resourceManagerConfigGet(options?: any): AxiosPromise { return localVarFp.resourceManagerConfigGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Get Information of Resource Groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ resourceManagerInformationGet(options?: any): AxiosPromise> { return localVarFp.resourceManagerInformationGet(options).then((request) => request(axios, basePath)); }, /** * * @summary List all resource groups * @param {*} [options] Override http request option. * @throws {RequiredError} */ resourceManagerInformationGroupNamesGet(options?: any): AxiosPromise> { return localVarFp.resourceManagerInformationGroupNamesGet(options).then((request) => request(axios, basePath)); }, /** * Get available field names by slowquery table columns * @summary Get available field names * @param {*} [options] Override http request option. * @throws {RequiredError} */ slowQueryAvailableFieldsGet(options?: any): AxiosPromise> { return localVarFp.slowQueryAvailableFieldsGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Get details of a slow query * @param {string} [connectId] TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. * @param {string} [digest] * @param {number} [timestamp] * @param {*} [options] Override http request option. * @throws {RequiredError} */ slowQueryDetailGet(connectId?: string, digest?: string, timestamp?: number, options?: any): AxiosPromise { return localVarFp.slowQueryDetailGet(connectId, digest, timestamp, options).then((request) => request(axios, basePath)); }, /** * * @summary Download slow query statements * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ slowQueryDownloadGet(token: string, options?: any): AxiosPromise { return localVarFp.slowQueryDownloadGet(token, options).then((request) => request(axios, basePath)); }, /** * * @summary Generate a download token for exported slow query statements * @param {SlowqueryGetListRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ slowQueryDownloadTokenPost(request: SlowqueryGetListRequest, options?: any): AxiosPromise { return localVarFp.slowQueryDownloadTokenPost(request, options).then((request) => request(axios, basePath)); }, /** * * @summary List all slow queries * @param {number} [beginTime] * @param {Array} [db] * @param {boolean} [desc] * @param {string} [digest] * @param {number} [endTime] * @param {string} [fields] example: \"Query,Digest\" * @param {number} [limit] * @param {string} [orderBy] * @param {Array} [plans] for showing slow queries in the statement detail page * @param {Array} [resourceGroup] * @param {string} [text] * @param {*} [options] Override http request option. * @throws {RequiredError} */ slowQueryListGet(beginTime?: number, db?: Array, desc?: boolean, digest?: string, endTime?: number, fields?: string, limit?: number, orderBy?: string, plans?: Array, resourceGroup?: Array, text?: string, options?: any): AxiosPromise> { return localVarFp.slowQueryListGet(beginTime, db, desc, digest, endTime, fields, limit, orderBy, plans, resourceGroup, text, options).then((request) => request(axios, basePath)); }, /** * Start a profiling task group * @summary Start profiling * @param {ProfilingStartRequest} req profiling request * @param {*} [options] Override http request option. * @throws {RequiredError} */ startProfiling(req: ProfilingStartRequest, options?: any): AxiosPromise { return localVarFp.startProfiling(req, options).then((request) => request(axios, basePath)); }, /** * Get available field names by statements table columns * @summary Get available field names * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsAvailableFieldsGet(options?: any): AxiosPromise> { return localVarFp.statementsAvailableFieldsGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Get statement configurations * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsConfigGet(options?: any): AxiosPromise { return localVarFp.statementsConfigGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Update statement configurations * @param {StatementEditableConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsConfigPost(request: StatementEditableConfig, options?: any): AxiosPromise { return localVarFp.statementsConfigPost(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Download statements * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsDownloadGet(token: string, options?: any): AxiosPromise { return localVarFp.statementsDownloadGet(token, options).then((request) => request(axios, basePath)); }, /** * * @summary Generate a download token for exported statements * @param {StatementGetStatementsRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsDownloadTokenPost(request: StatementGetStatementsRequest, options?: any): AxiosPromise { return localVarFp.statementsDownloadTokenPost(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Get a list of statements * @param {number} [beginTime] * @param {number} [endTime] * @param {string} [fields] * @param {Array} [resourceGroups] * @param {Array} [schemas] * @param {Array} [stmtTypes] * @param {string} [text] * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsListGet(beginTime?: number, endTime?: number, fields?: string, resourceGroups?: Array, schemas?: Array, stmtTypes?: Array, text?: string, options?: any): AxiosPromise> { return localVarFp.statementsListGet(beginTime, endTime, fields, resourceGroups, schemas, stmtTypes, text, options).then((request) => request(axios, basePath)); }, /** * * @summary Drop all manually created bindings for a statement * @param {string} sqlDigest query template ID (a.k.a. sql digest) * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingDelete(sqlDigest: string, options?: any): AxiosPromise { return localVarFp.statementsPlanBindingDelete(sqlDigest, options).then((request) => request(axios, basePath)); }, /** * * @summary Get the bound plan digest (if exists) of a statement * @param {string} sqlDigest query template id * @param {number} beginTime begin time * @param {number} endTime end time * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingGet(sqlDigest: string, beginTime: number, endTime: number, options?: any): AxiosPromise { return localVarFp.statementsPlanBindingGet(sqlDigest, beginTime, endTime, options).then((request) => request(axios, basePath)); }, /** * * @summary Create a binding for a statement and a plan * @param {string} planDigest plan digest id * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingPost(planDigest: string, options?: any): AxiosPromise { return localVarFp.statementsPlanBindingPost(planDigest, options).then((request) => request(axios, basePath)); }, /** * * @summary Get details of a statement in an execution plan * @param {number} [beginTime] * @param {string} [digest] * @param {number} [endTime] * @param {Array} [plans] * @param {string} [schemaName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanDetailGet(beginTime?: number, digest?: string, endTime?: number, plans?: Array, schemaName?: string, options?: any): AxiosPromise { return localVarFp.statementsPlanDetailGet(beginTime, digest, endTime, plans, schemaName, options).then((request) => request(axios, basePath)); }, /** * * @summary Get execution plans of a statement * @param {number} [beginTime] * @param {string} [digest] * @param {number} [endTime] * @param {string} [schemaName] * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlansGet(beginTime?: number, digest?: string, endTime?: number, schemaName?: string, options?: any): AxiosPromise> { return localVarFp.statementsPlansGet(beginTime, digest, endTime, schemaName, options).then((request) => request(axios, basePath)); }, /** * * @summary Get all statement types * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsStmtTypesGet(options?: any): AxiosPromise> { return localVarFp.statementsStmtTypesGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Hide a TiDB instance * @param {string} address ip:port * @param {*} [options] Override http request option. * @throws {RequiredError} */ topologyTidbAddressDelete(address: string, options?: any): AxiosPromise { return localVarFp.topologyTidbAddressDelete(address, options).then((request) => request(axios, basePath)); }, /** * * @summary Get Top SQL config * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlConfigGet(options?: any): AxiosPromise { return localVarFp.topsqlConfigGet(options).then((request) => request(axios, basePath)); }, /** * * @summary Update Top SQL config * @param {TopsqlEditableConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlConfigPost(request: TopsqlEditableConfig, options?: any): AxiosPromise { return localVarFp.topsqlConfigPost(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Get TiKV network IO collection config * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlGetTiKVNetworkIOCollection(options?: any): AxiosPromise { return localVarFp.topsqlGetTiKVNetworkIOCollection(options).then((request) => request(axios, basePath)); }, /** * * @summary Get available instances * @param {string} [dataSource] * @param {string} [end] * @param {string} [start] * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlInstancesGet(dataSource?: string, end?: string, start?: string, options?: any): AxiosPromise { return localVarFp.topsqlInstancesGet(dataSource, end, start, options).then((request) => request(axios, basePath)); }, /** * * @summary Get summaries * @param {string} [dataSource] * @param {string} [end] * @param {string} [groupBy] * @param {string} [instance] * @param {string} [instanceType] * @param {string} [orderBy] * @param {string} [start] * @param {string} [top] * @param {string} [window] * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlSummaryGet(dataSource?: string, end?: string, groupBy?: string, instance?: string, instanceType?: string, orderBy?: string, start?: string, top?: string, window?: string, options?: any): AxiosPromise { return localVarFp.topsqlSummaryGet(dataSource, end, groupBy, instance, instanceType, orderBy, start, top, window, options).then((request) => request(axios, basePath)); }, /** * * @summary Update TiKV network IO collection config * @param {TopsqlTikvNetworkIoCollectionConfig} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ topsqlUpdateTiKVNetworkIOCollection(request: TopsqlTikvNetworkIoCollectionConfig, options?: any): AxiosPromise { return localVarFp.topsqlUpdateTiKVNetworkIOCollection(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Get log in information, like supported authenticate types * @param {*} [options] Override http request option. * @throws {RequiredError} */ userGetLoginInfo(options?: any): AxiosPromise { return localVarFp.userGetLoginInfo(options).then((request) => request(axios, basePath)); }, /** * * @summary Get sign out info * @param {string} [redirectUrl] * @param {*} [options] Override http request option. * @throws {RequiredError} */ userGetSignOutInfo(redirectUrl?: string, options?: any): AxiosPromise { return localVarFp.userGetSignOutInfo(redirectUrl, options).then((request) => request(axios, basePath)); }, /** * * @summary Log in * @param {UserAuthenticateForm} message Credentials * @param {*} [options] Override http request option. * @throws {RequiredError} */ userLogin(message: UserAuthenticateForm, options?: any): AxiosPromise { return localVarFp.userLogin(message, options).then((request) => request(axios, basePath)); }, /** * * @summary Reset encryption key to revoke all authorized codes * @param {*} [options] Override http request option. * @throws {RequiredError} */ userRevokeSession(options?: any): AxiosPromise { return localVarFp.userRevokeSession(options).then((request) => request(axios, basePath)); }, /** * * @summary Create an impersonation * @param {SsoCreateImpersonationRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ userSSOCreateImpersonation(request: SsoCreateImpersonationRequest, options?: any): AxiosPromise { return localVarFp.userSSOCreateImpersonation(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Get SSO Auth URL * @param {string} [codeVerifier] * @param {string} [redirectUrl] * @param {string} [state] * @param {*} [options] Override http request option. * @throws {RequiredError} */ userSSOGetAuthURL(codeVerifier?: string, redirectUrl?: string, state?: string, options?: any): AxiosPromise { return localVarFp.userSSOGetAuthURL(codeVerifier, redirectUrl, state, options).then((request) => request(axios, basePath)); }, /** * * @summary Get SSO config * @param {*} [options] Override http request option. * @throws {RequiredError} */ userSSOGetConfig(options?: any): AxiosPromise { return localVarFp.userSSOGetConfig(options).then((request) => request(axios, basePath)); }, /** * * @summary List all impersonations * @param {*} [options] Override http request option. * @throws {RequiredError} */ userSSOListImpersonations(options?: any): AxiosPromise> { return localVarFp.userSSOListImpersonations(options).then((request) => request(axios, basePath)); }, /** * * @summary Set SSO config * @param {SsoSetConfigRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ userSSOSetConfig(request: SsoSetConfigRequest, options?: any): AxiosPromise { return localVarFp.userSSOSetConfig(request, options).then((request) => request(axios, basePath)); }, /** * * @summary Share current session and generate a sharing code * @param {CodeShareRequest} request Request body * @param {*} [options] Override http request option. * @throws {RequiredError} */ userShareSession(request: CodeShareRequest, options?: any): AxiosPromise { return localVarFp.userShareSession(request, options).then((request) => request(axios, basePath)); }, /** * View the finished profiling result of a task * @summary View the result of a task * @param {string} token download token * @param {*} [options] Override http request option. * @throws {RequiredError} */ viewProfilingSingle(token: string, options?: any): AxiosPromise { return localVarFp.viewProfilingSingle(token, options).then((request) => request(axios, basePath)); }, }; }; /** * Request parameters for cancelProfilingGroup operation in DefaultApi. * @export * @interface DefaultApiCancelProfilingGroupRequest */ export interface DefaultApiCancelProfilingGroupRequest { /** * group ID * @type {string} * @memberof DefaultApiCancelProfilingGroup */ readonly groupId: string } /** * Request parameters for configurationEdit operation in DefaultApi. * @export * @interface DefaultApiConfigurationEditRequest */ export interface DefaultApiConfigurationEditRequest { /** * Request body * @type {ConfigurationEditRequest} * @memberof DefaultApiConfigurationEdit */ readonly request: ConfigurationEditRequest } /** * Request parameters for continuousProfilingActionTokenGet operation in DefaultApi. * @export * @interface DefaultApiContinuousProfilingActionTokenGetRequest */ export interface DefaultApiContinuousProfilingActionTokenGetRequest { /** * target query string * @type {string} * @memberof DefaultApiContinuousProfilingActionTokenGet */ readonly q: string } /** * Request parameters for continuousProfilingConfigPost operation in DefaultApi. * @export * @interface DefaultApiContinuousProfilingConfigPostRequest */ export interface DefaultApiContinuousProfilingConfigPostRequest { /** * Request body * @type {ConprofNgMonitoringConfig} * @memberof DefaultApiContinuousProfilingConfigPost */ readonly request: ConprofNgMonitoringConfig } /** * Request parameters for continuousProfilingDownloadGet operation in DefaultApi. * @export * @interface DefaultApiContinuousProfilingDownloadGetRequest */ export interface DefaultApiContinuousProfilingDownloadGetRequest { /** * timestamp * @type {number} * @memberof DefaultApiContinuousProfilingDownloadGet */ readonly ts: number } /** * Request parameters for continuousProfilingGroupProfileDetailGet operation in DefaultApi. * @export * @interface DefaultApiContinuousProfilingGroupProfileDetailGetRequest */ export interface DefaultApiContinuousProfilingGroupProfileDetailGetRequest { /** * timestamp * @type {number} * @memberof DefaultApiContinuousProfilingGroupProfileDetailGet */ readonly ts: number } /** * Request parameters for continuousProfilingGroupProfilesGet operation in DefaultApi. * @export * @interface DefaultApiContinuousProfilingGroupProfilesGetRequest */ export interface DefaultApiContinuousProfilingGroupProfilesGetRequest { /** * * @type {number} * @memberof DefaultApiContinuousProfilingGroupProfilesGet */ readonly beginTime?: number /** * * @type {number} * @memberof DefaultApiContinuousProfilingGroupProfilesGet */ readonly endTime?: number } /** * Request parameters for continuousProfilingSingleProfileViewGet operation in DefaultApi. * @export * @interface DefaultApiContinuousProfilingSingleProfileViewGetRequest */ export interface DefaultApiContinuousProfilingSingleProfileViewGetRequest { /** * * @type {string} * @memberof DefaultApiContinuousProfilingSingleProfileViewGet */ readonly address?: string /** * * @type {string} * @memberof DefaultApiContinuousProfilingSingleProfileViewGet */ readonly component?: string /** * * @type {string} * @memberof DefaultApiContinuousProfilingSingleProfileViewGet */ readonly profileType?: string /** * * @type {number} * @memberof DefaultApiContinuousProfilingSingleProfileViewGet */ readonly ts?: number } /** * Request parameters for debugAPIRequestEndpoint operation in DefaultApi. * @export * @interface DefaultApiDebugAPIRequestEndpointRequest */ export interface DefaultApiDebugAPIRequestEndpointRequest { /** * request payload * @type {EndpointRequestPayload} * @memberof DefaultApiDebugAPIRequestEndpoint */ readonly req: EndpointRequestPayload } /** * Request parameters for debugApiDownloadGet operation in DefaultApi. * @export * @interface DefaultApiDebugApiDownloadGetRequest */ export interface DefaultApiDebugApiDownloadGetRequest { /** * download token * @type {string} * @memberof DefaultApiDebugApiDownloadGet */ readonly token: string } /** * Request parameters for deleteProfilingGroup operation in DefaultApi. * @export * @interface DefaultApiDeleteProfilingGroupRequest */ export interface DefaultApiDeleteProfilingGroupRequest { /** * group ID * @type {string} * @memberof DefaultApiDeleteProfilingGroup */ readonly groupId: string } /** * Request parameters for diagnoseDiagnosisPost operation in DefaultApi. * @export * @interface DefaultApiDiagnoseDiagnosisPostRequest */ export interface DefaultApiDiagnoseDiagnosisPostRequest { /** * Request body * @type {DiagnoseGenDiagnosisReportRequest} * @memberof DefaultApiDiagnoseDiagnosisPost */ readonly request: DiagnoseGenDiagnosisReportRequest } /** * Request parameters for diagnoseGenerateMetricsRelationship operation in DefaultApi. * @export * @interface DefaultApiDiagnoseGenerateMetricsRelationshipRequest */ export interface DefaultApiDiagnoseGenerateMetricsRelationshipRequest { /** * Request body * @type {DiagnoseGenerateMetricsRelationRequest} * @memberof DefaultApiDiagnoseGenerateMetricsRelationship */ readonly request: DiagnoseGenerateMetricsRelationRequest } /** * Request parameters for diagnoseMetricsRelationViewGet operation in DefaultApi. * @export * @interface DefaultApiDiagnoseMetricsRelationViewGetRequest */ export interface DefaultApiDiagnoseMetricsRelationViewGetRequest { /** * token * @type {string} * @memberof DefaultApiDiagnoseMetricsRelationViewGet */ readonly token: string } /** * Request parameters for diagnoseReportsIdDataJsGet operation in DefaultApi. * @export * @interface DefaultApiDiagnoseReportsIdDataJsGetRequest */ export interface DefaultApiDiagnoseReportsIdDataJsGetRequest { /** * report id * @type {string} * @memberof DefaultApiDiagnoseReportsIdDataJsGet */ readonly id: string } /** * Request parameters for diagnoseReportsIdDetailGet operation in DefaultApi. * @export * @interface DefaultApiDiagnoseReportsIdDetailGetRequest */ export interface DefaultApiDiagnoseReportsIdDetailGetRequest { /** * report id * @type {string} * @memberof DefaultApiDiagnoseReportsIdDetailGet */ readonly id: string } /** * Request parameters for diagnoseReportsIdStatusGet operation in DefaultApi. * @export * @interface DefaultApiDiagnoseReportsIdStatusGetRequest */ export interface DefaultApiDiagnoseReportsIdStatusGetRequest { /** * report id * @type {string} * @memberof DefaultApiDiagnoseReportsIdStatusGet */ readonly id: string } /** * Request parameters for diagnoseReportsPost operation in DefaultApi. * @export * @interface DefaultApiDiagnoseReportsPostRequest */ export interface DefaultApiDiagnoseReportsPostRequest { /** * Request body * @type {DiagnoseGenerateReportRequest} * @memberof DefaultApiDiagnoseReportsPost */ readonly request: DiagnoseGenerateReportRequest } /** * Request parameters for downloadProfilingGroup operation in DefaultApi. * @export * @interface DefaultApiDownloadProfilingGroupRequest */ export interface DefaultApiDownloadProfilingGroupRequest { /** * download token * @type {string} * @memberof DefaultApiDownloadProfilingGroup */ readonly token: string } /** * Request parameters for downloadProfilingSingle operation in DefaultApi. * @export * @interface DefaultApiDownloadProfilingSingleRequest */ export interface DefaultApiDownloadProfilingSingleRequest { /** * download token * @type {string} * @memberof DefaultApiDownloadProfilingSingle */ readonly token: string } /** * Request parameters for getActionToken operation in DefaultApi. * @export * @interface DefaultApiGetActionTokenRequest */ export interface DefaultApiGetActionTokenRequest { /** * group or task ID * @type {string} * @memberof DefaultApiGetActionToken */ readonly id?: string /** * action * @type {string} * @memberof DefaultApiGetActionToken */ readonly action?: string } /** * Request parameters for getAlertManagerCounts operation in DefaultApi. * @export * @interface DefaultApiGetAlertManagerCountsRequest */ export interface DefaultApiGetAlertManagerCountsRequest { /** * ip:port * @type {string} * @memberof DefaultApiGetAlertManagerCounts */ readonly address: string } /** * Request parameters for getProfilingGroupDetail operation in DefaultApi. * @export * @interface DefaultApiGetProfilingGroupDetailRequest */ export interface DefaultApiGetProfilingGroupDetailRequest { /** * group ID * @type {string} * @memberof DefaultApiGetProfilingGroupDetail */ readonly groupId: string } /** * Request parameters for infoListTables operation in DefaultApi. * @export * @interface DefaultApiInfoListTablesRequest */ export interface DefaultApiInfoListTablesRequest { /** * Database name * @type {string} * @memberof DefaultApiInfoListTables */ readonly databaseName?: string } /** * Request parameters for keyvisualConfigPut operation in DefaultApi. * @export * @interface DefaultApiKeyvisualConfigPutRequest */ export interface DefaultApiKeyvisualConfigPutRequest { /** * Request body * @type {ConfigKeyVisualConfig} * @memberof DefaultApiKeyvisualConfigPut */ readonly request: ConfigKeyVisualConfig } /** * Request parameters for keyvisualHeatmapsGet operation in DefaultApi. * @export * @interface DefaultApiKeyvisualHeatmapsGetRequest */ export interface DefaultApiKeyvisualHeatmapsGetRequest { /** * The start of the key range * @type {string} * @memberof DefaultApiKeyvisualHeatmapsGet */ readonly startkey?: string /** * The end of the key range * @type {string} * @memberof DefaultApiKeyvisualHeatmapsGet */ readonly endkey?: string /** * The start of the time range (Unix) * @type {number} * @memberof DefaultApiKeyvisualHeatmapsGet */ readonly starttime?: number /** * The end of the time range (Unix) * @type {number} * @memberof DefaultApiKeyvisualHeatmapsGet */ readonly endtime?: number /** * Main types of data * @type {'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration'} * @memberof DefaultApiKeyvisualHeatmapsGet */ readonly type?: 'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration' } /** * Request parameters for logsDownloadAcquireTokenGet operation in DefaultApi. * @export * @interface DefaultApiLogsDownloadAcquireTokenGetRequest */ export interface DefaultApiLogsDownloadAcquireTokenGetRequest { /** * task id * @type {Array} * @memberof DefaultApiLogsDownloadAcquireTokenGet */ readonly id?: Array } /** * Request parameters for logsDownloadGet operation in DefaultApi. * @export * @interface DefaultApiLogsDownloadGetRequest */ export interface DefaultApiLogsDownloadGetRequest { /** * download token * @type {string} * @memberof DefaultApiLogsDownloadGet */ readonly token: string } /** * Request parameters for logsTaskgroupPut operation in DefaultApi. * @export * @interface DefaultApiLogsTaskgroupPutRequest */ export interface DefaultApiLogsTaskgroupPutRequest { /** * Request body * @type {LogsearchCreateTaskGroupRequest} * @memberof DefaultApiLogsTaskgroupPut */ readonly request: LogsearchCreateTaskGroupRequest } /** * Request parameters for logsTaskgroupsIdCancelPost operation in DefaultApi. * @export * @interface DefaultApiLogsTaskgroupsIdCancelPostRequest */ export interface DefaultApiLogsTaskgroupsIdCancelPostRequest { /** * task group id * @type {string} * @memberof DefaultApiLogsTaskgroupsIdCancelPost */ readonly id: string } /** * Request parameters for logsTaskgroupsIdDelete operation in DefaultApi. * @export * @interface DefaultApiLogsTaskgroupsIdDeleteRequest */ export interface DefaultApiLogsTaskgroupsIdDeleteRequest { /** * task group id * @type {string} * @memberof DefaultApiLogsTaskgroupsIdDelete */ readonly id: string } /** * Request parameters for logsTaskgroupsIdGet operation in DefaultApi. * @export * @interface DefaultApiLogsTaskgroupsIdGetRequest */ export interface DefaultApiLogsTaskgroupsIdGetRequest { /** * Task Group ID * @type {string} * @memberof DefaultApiLogsTaskgroupsIdGet */ readonly id: string } /** * Request parameters for logsTaskgroupsIdPreviewGet operation in DefaultApi. * @export * @interface DefaultApiLogsTaskgroupsIdPreviewGetRequest */ export interface DefaultApiLogsTaskgroupsIdPreviewGetRequest { /** * task group id * @type {string} * @memberof DefaultApiLogsTaskgroupsIdPreviewGet */ readonly id: string } /** * Request parameters for logsTaskgroupsIdRetryPost operation in DefaultApi. * @export * @interface DefaultApiLogsTaskgroupsIdRetryPostRequest */ export interface DefaultApiLogsTaskgroupsIdRetryPostRequest { /** * task group id * @type {string} * @memberof DefaultApiLogsTaskgroupsIdRetryPost */ readonly id: string } /** * Request parameters for metricsQueryGet operation in DefaultApi. * @export * @interface DefaultApiMetricsQueryGetRequest */ export interface DefaultApiMetricsQueryGetRequest { /** * * @type {number} * @memberof DefaultApiMetricsQueryGet */ readonly endTimeSec?: number /** * * @type {string} * @memberof DefaultApiMetricsQueryGet */ readonly query?: string /** * * @type {number} * @memberof DefaultApiMetricsQueryGet */ readonly startTimeSec?: number /** * * @type {number} * @memberof DefaultApiMetricsQueryGet */ readonly stepSec?: number } /** * Request parameters for metricsSetCustomPromAddress operation in DefaultApi. * @export * @interface DefaultApiMetricsSetCustomPromAddressRequest */ export interface DefaultApiMetricsSetCustomPromAddressRequest { /** * Request body * @type {MetricsPutCustomPromAddressRequest} * @memberof DefaultApiMetricsSetCustomPromAddress */ readonly request: MetricsPutCustomPromAddressRequest } /** * Request parameters for profilingConfigPut operation in DefaultApi. * @export * @interface DefaultApiProfilingConfigPutRequest */ export interface DefaultApiProfilingConfigPutRequest { /** * Request body * @type {ConfigProfilingConfig} * @memberof DefaultApiProfilingConfigPut */ readonly request: ConfigProfilingConfig } /** * Request parameters for queryEditorRun operation in DefaultApi. * @export * @interface DefaultApiQueryEditorRunRequest */ export interface DefaultApiQueryEditorRunRequest { /** * Request body * @type {QueryeditorRunRequest} * @memberof DefaultApiQueryEditorRun */ readonly request: QueryeditorRunRequest } /** * Request parameters for resourceManagerCalibrateActualGet operation in DefaultApi. * @export * @interface DefaultApiResourceManagerCalibrateActualGetRequest */ export interface DefaultApiResourceManagerCalibrateActualGetRequest { /** * * @type {number} * @memberof DefaultApiResourceManagerCalibrateActualGet */ readonly endTime?: number /** * * @type {number} * @memberof DefaultApiResourceManagerCalibrateActualGet */ readonly startTime?: number } /** * Request parameters for resourceManagerCalibrateHardwareGet operation in DefaultApi. * @export * @interface DefaultApiResourceManagerCalibrateHardwareGetRequest */ export interface DefaultApiResourceManagerCalibrateHardwareGetRequest { /** * workload * @type {string} * @memberof DefaultApiResourceManagerCalibrateHardwareGet */ readonly workload: string } /** * Request parameters for slowQueryDetailGet operation in DefaultApi. * @export * @interface DefaultApiSlowQueryDetailGetRequest */ export interface DefaultApiSlowQueryDetailGetRequest { /** * TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. * @type {string} * @memberof DefaultApiSlowQueryDetailGet */ readonly connectId?: string /** * * @type {string} * @memberof DefaultApiSlowQueryDetailGet */ readonly digest?: string /** * * @type {number} * @memberof DefaultApiSlowQueryDetailGet */ readonly timestamp?: number } /** * Request parameters for slowQueryDownloadGet operation in DefaultApi. * @export * @interface DefaultApiSlowQueryDownloadGetRequest */ export interface DefaultApiSlowQueryDownloadGetRequest { /** * download token * @type {string} * @memberof DefaultApiSlowQueryDownloadGet */ readonly token: string } /** * Request parameters for slowQueryDownloadTokenPost operation in DefaultApi. * @export * @interface DefaultApiSlowQueryDownloadTokenPostRequest */ export interface DefaultApiSlowQueryDownloadTokenPostRequest { /** * Request body * @type {SlowqueryGetListRequest} * @memberof DefaultApiSlowQueryDownloadTokenPost */ readonly request: SlowqueryGetListRequest } /** * Request parameters for slowQueryListGet operation in DefaultApi. * @export * @interface DefaultApiSlowQueryListGetRequest */ export interface DefaultApiSlowQueryListGetRequest { /** * * @type {number} * @memberof DefaultApiSlowQueryListGet */ readonly beginTime?: number /** * * @type {Array} * @memberof DefaultApiSlowQueryListGet */ readonly db?: Array /** * * @type {boolean} * @memberof DefaultApiSlowQueryListGet */ readonly desc?: boolean /** * * @type {string} * @memberof DefaultApiSlowQueryListGet */ readonly digest?: string /** * * @type {number} * @memberof DefaultApiSlowQueryListGet */ readonly endTime?: number /** * example: \"Query,Digest\" * @type {string} * @memberof DefaultApiSlowQueryListGet */ readonly fields?: string /** * * @type {number} * @memberof DefaultApiSlowQueryListGet */ readonly limit?: number /** * * @type {string} * @memberof DefaultApiSlowQueryListGet */ readonly orderBy?: string /** * for showing slow queries in the statement detail page * @type {Array} * @memberof DefaultApiSlowQueryListGet */ readonly plans?: Array /** * * @type {Array} * @memberof DefaultApiSlowQueryListGet */ readonly resourceGroup?: Array /** * * @type {string} * @memberof DefaultApiSlowQueryListGet */ readonly text?: string } /** * Request parameters for startProfiling operation in DefaultApi. * @export * @interface DefaultApiStartProfilingRequest */ export interface DefaultApiStartProfilingRequest { /** * profiling request * @type {ProfilingStartRequest} * @memberof DefaultApiStartProfiling */ readonly req: ProfilingStartRequest } /** * Request parameters for statementsConfigPost operation in DefaultApi. * @export * @interface DefaultApiStatementsConfigPostRequest */ export interface DefaultApiStatementsConfigPostRequest { /** * Request body * @type {StatementEditableConfig} * @memberof DefaultApiStatementsConfigPost */ readonly request: StatementEditableConfig } /** * Request parameters for statementsDownloadGet operation in DefaultApi. * @export * @interface DefaultApiStatementsDownloadGetRequest */ export interface DefaultApiStatementsDownloadGetRequest { /** * download token * @type {string} * @memberof DefaultApiStatementsDownloadGet */ readonly token: string } /** * Request parameters for statementsDownloadTokenPost operation in DefaultApi. * @export * @interface DefaultApiStatementsDownloadTokenPostRequest */ export interface DefaultApiStatementsDownloadTokenPostRequest { /** * Request body * @type {StatementGetStatementsRequest} * @memberof DefaultApiStatementsDownloadTokenPost */ readonly request: StatementGetStatementsRequest } /** * Request parameters for statementsListGet operation in DefaultApi. * @export * @interface DefaultApiStatementsListGetRequest */ export interface DefaultApiStatementsListGetRequest { /** * * @type {number} * @memberof DefaultApiStatementsListGet */ readonly beginTime?: number /** * * @type {number} * @memberof DefaultApiStatementsListGet */ readonly endTime?: number /** * * @type {string} * @memberof DefaultApiStatementsListGet */ readonly fields?: string /** * * @type {Array} * @memberof DefaultApiStatementsListGet */ readonly resourceGroups?: Array /** * * @type {Array} * @memberof DefaultApiStatementsListGet */ readonly schemas?: Array /** * * @type {Array} * @memberof DefaultApiStatementsListGet */ readonly stmtTypes?: Array /** * * @type {string} * @memberof DefaultApiStatementsListGet */ readonly text?: string } /** * Request parameters for statementsPlanBindingDelete operation in DefaultApi. * @export * @interface DefaultApiStatementsPlanBindingDeleteRequest */ export interface DefaultApiStatementsPlanBindingDeleteRequest { /** * query template ID (a.k.a. sql digest) * @type {string} * @memberof DefaultApiStatementsPlanBindingDelete */ readonly sqlDigest: string } /** * Request parameters for statementsPlanBindingGet operation in DefaultApi. * @export * @interface DefaultApiStatementsPlanBindingGetRequest */ export interface DefaultApiStatementsPlanBindingGetRequest { /** * query template id * @type {string} * @memberof DefaultApiStatementsPlanBindingGet */ readonly sqlDigest: string /** * begin time * @type {number} * @memberof DefaultApiStatementsPlanBindingGet */ readonly beginTime: number /** * end time * @type {number} * @memberof DefaultApiStatementsPlanBindingGet */ readonly endTime: number } /** * Request parameters for statementsPlanBindingPost operation in DefaultApi. * @export * @interface DefaultApiStatementsPlanBindingPostRequest */ export interface DefaultApiStatementsPlanBindingPostRequest { /** * plan digest id * @type {string} * @memberof DefaultApiStatementsPlanBindingPost */ readonly planDigest: string } /** * Request parameters for statementsPlanDetailGet operation in DefaultApi. * @export * @interface DefaultApiStatementsPlanDetailGetRequest */ export interface DefaultApiStatementsPlanDetailGetRequest { /** * * @type {number} * @memberof DefaultApiStatementsPlanDetailGet */ readonly beginTime?: number /** * * @type {string} * @memberof DefaultApiStatementsPlanDetailGet */ readonly digest?: string /** * * @type {number} * @memberof DefaultApiStatementsPlanDetailGet */ readonly endTime?: number /** * * @type {Array} * @memberof DefaultApiStatementsPlanDetailGet */ readonly plans?: Array /** * * @type {string} * @memberof DefaultApiStatementsPlanDetailGet */ readonly schemaName?: string } /** * Request parameters for statementsPlansGet operation in DefaultApi. * @export * @interface DefaultApiStatementsPlansGetRequest */ export interface DefaultApiStatementsPlansGetRequest { /** * * @type {number} * @memberof DefaultApiStatementsPlansGet */ readonly beginTime?: number /** * * @type {string} * @memberof DefaultApiStatementsPlansGet */ readonly digest?: string /** * * @type {number} * @memberof DefaultApiStatementsPlansGet */ readonly endTime?: number /** * * @type {string} * @memberof DefaultApiStatementsPlansGet */ readonly schemaName?: string } /** * Request parameters for topologyTidbAddressDelete operation in DefaultApi. * @export * @interface DefaultApiTopologyTidbAddressDeleteRequest */ export interface DefaultApiTopologyTidbAddressDeleteRequest { /** * ip:port * @type {string} * @memberof DefaultApiTopologyTidbAddressDelete */ readonly address: string } /** * Request parameters for topsqlConfigPost operation in DefaultApi. * @export * @interface DefaultApiTopsqlConfigPostRequest */ export interface DefaultApiTopsqlConfigPostRequest { /** * Request body * @type {TopsqlEditableConfig} * @memberof DefaultApiTopsqlConfigPost */ readonly request: TopsqlEditableConfig } /** * Request parameters for topsqlInstancesGet operation in DefaultApi. * @export * @interface DefaultApiTopsqlInstancesGetRequest */ export interface DefaultApiTopsqlInstancesGetRequest { /** * * @type {string} * @memberof DefaultApiTopsqlInstancesGet */ readonly dataSource?: string /** * * @type {string} * @memberof DefaultApiTopsqlInstancesGet */ readonly end?: string /** * * @type {string} * @memberof DefaultApiTopsqlInstancesGet */ readonly start?: string } /** * Request parameters for topsqlSummaryGet operation in DefaultApi. * @export * @interface DefaultApiTopsqlSummaryGetRequest */ export interface DefaultApiTopsqlSummaryGetRequest { /** * * @type {string} * @memberof DefaultApiTopsqlSummaryGet */ readonly dataSource?: string /** * * @type {string} * @memberof DefaultApiTopsqlSummaryGet */ readonly end?: string /** * * @type {string} * @memberof DefaultApiTopsqlSummaryGet */ readonly groupBy?: string /** * * @type {string} * @memberof DefaultApiTopsqlSummaryGet */ readonly instance?: string /** * * @type {string} * @memberof DefaultApiTopsqlSummaryGet */ readonly instanceType?: string /** * * @type {string} * @memberof DefaultApiTopsqlSummaryGet */ readonly orderBy?: string /** * * @type {string} * @memberof DefaultApiTopsqlSummaryGet */ readonly start?: string /** * * @type {string} * @memberof DefaultApiTopsqlSummaryGet */ readonly top?: string /** * * @type {string} * @memberof DefaultApiTopsqlSummaryGet */ readonly window?: string } /** * Request parameters for topsqlUpdateTiKVNetworkIOCollection operation in DefaultApi. * @export * @interface DefaultApiTopsqlUpdateTiKVNetworkIOCollectionRequest */ export interface DefaultApiTopsqlUpdateTiKVNetworkIOCollectionRequest { /** * Request body * @type {TopsqlTikvNetworkIoCollectionConfig} * @memberof DefaultApiTopsqlUpdateTiKVNetworkIOCollection */ readonly request: TopsqlTikvNetworkIoCollectionConfig } /** * Request parameters for userGetSignOutInfo operation in DefaultApi. * @export * @interface DefaultApiUserGetSignOutInfoRequest */ export interface DefaultApiUserGetSignOutInfoRequest { /** * * @type {string} * @memberof DefaultApiUserGetSignOutInfo */ readonly redirectUrl?: string } /** * Request parameters for userLogin operation in DefaultApi. * @export * @interface DefaultApiUserLoginRequest */ export interface DefaultApiUserLoginRequest { /** * Credentials * @type {UserAuthenticateForm} * @memberof DefaultApiUserLogin */ readonly message: UserAuthenticateForm } /** * Request parameters for userSSOCreateImpersonation operation in DefaultApi. * @export * @interface DefaultApiUserSSOCreateImpersonationRequest */ export interface DefaultApiUserSSOCreateImpersonationRequest { /** * Request body * @type {SsoCreateImpersonationRequest} * @memberof DefaultApiUserSSOCreateImpersonation */ readonly request: SsoCreateImpersonationRequest } /** * Request parameters for userSSOGetAuthURL operation in DefaultApi. * @export * @interface DefaultApiUserSSOGetAuthURLRequest */ export interface DefaultApiUserSSOGetAuthURLRequest { /** * * @type {string} * @memberof DefaultApiUserSSOGetAuthURL */ readonly codeVerifier?: string /** * * @type {string} * @memberof DefaultApiUserSSOGetAuthURL */ readonly redirectUrl?: string /** * * @type {string} * @memberof DefaultApiUserSSOGetAuthURL */ readonly state?: string } /** * Request parameters for userSSOSetConfig operation in DefaultApi. * @export * @interface DefaultApiUserSSOSetConfigRequest */ export interface DefaultApiUserSSOSetConfigRequest { /** * Request body * @type {SsoSetConfigRequest} * @memberof DefaultApiUserSSOSetConfig */ readonly request: SsoSetConfigRequest } /** * Request parameters for userShareSession operation in DefaultApi. * @export * @interface DefaultApiUserShareSessionRequest */ export interface DefaultApiUserShareSessionRequest { /** * Request body * @type {CodeShareRequest} * @memberof DefaultApiUserShareSession */ readonly request: CodeShareRequest } /** * Request parameters for viewProfilingSingle operation in DefaultApi. * @export * @interface DefaultApiViewProfilingSingleRequest */ export interface DefaultApiViewProfilingSingleRequest { /** * download token * @type {string} * @memberof DefaultApiViewProfilingSingle */ readonly token: string } /** * DefaultApi - object-oriented interface * @export * @class DefaultApi * @extends {BaseAPI} */ export class DefaultApi extends BaseAPI { /** * Cancel all profling tasks with a given group ID * @summary Cancel all tasks with a given group ID * @param {DefaultApiCancelProfilingGroupRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public cancelProfilingGroup(requestParameters: DefaultApiCancelProfilingGroupRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).cancelProfilingGroup(requestParameters.groupId, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get information of all hosts * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public clusterInfoGetHostsInfo(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).clusterInfoGetHostsInfo(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get cluster statistics * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public clusterInfoGetStatistics(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).clusterInfoGetStatistics(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Edit a configuration * @param {DefaultApiConfigurationEditRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public configurationEdit(requestParameters: DefaultApiConfigurationEditRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).configurationEdit(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get all configurations * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public configurationGetAll(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).configurationGetAll(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get action token for download or view profile * @param {DefaultApiContinuousProfilingActionTokenGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public continuousProfilingActionTokenGet(requestParameters: DefaultApiContinuousProfilingActionTokenGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).continuousProfilingActionTokenGet(requestParameters.q, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get current scraping components * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public continuousProfilingComponentsGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).continuousProfilingComponentsGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get Continuous Profiling Config * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public continuousProfilingConfigGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).continuousProfilingConfigGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Update Continuous Profiling Config * @param {DefaultApiContinuousProfilingConfigPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public continuousProfilingConfigPost(requestParameters: DefaultApiContinuousProfilingConfigPostRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).continuousProfilingConfigPost(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Download Group Profile files * @param {DefaultApiContinuousProfilingDownloadGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public continuousProfilingDownloadGet(requestParameters: DefaultApiContinuousProfilingDownloadGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).continuousProfilingDownloadGet(requestParameters.ts, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get Estimate Size * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public continuousProfilingEstimateSizeGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).continuousProfilingEstimateSizeGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get Group Profile Detail * @param {DefaultApiContinuousProfilingGroupProfileDetailGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public continuousProfilingGroupProfileDetailGet(requestParameters: DefaultApiContinuousProfilingGroupProfileDetailGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).continuousProfilingGroupProfileDetailGet(requestParameters.ts, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get Group Profiles * @param {DefaultApiContinuousProfilingGroupProfilesGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public continuousProfilingGroupProfilesGet(requestParameters: DefaultApiContinuousProfilingGroupProfilesGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).continuousProfilingGroupProfilesGet(requestParameters.beginTime, requestParameters.endTime, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary View Single Profile files * @param {DefaultApiContinuousProfilingSingleProfileViewGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public continuousProfilingSingleProfileViewGet(requestParameters: DefaultApiContinuousProfilingSingleProfileViewGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).continuousProfilingSingleProfileViewGet(requestParameters.address, requestParameters.component, requestParameters.profileType, requestParameters.ts, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary List all deadlock records * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public deadlockListGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).deadlockListGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get all endpoints * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public debugAPIGetEndpoints(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).debugAPIGetEndpoints(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Send request remote endpoint and return a token for downloading results * @param {DefaultApiDebugAPIRequestEndpointRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public debugAPIRequestEndpoint(requestParameters: DefaultApiDebugAPIRequestEndpointRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).debugAPIRequestEndpoint(requestParameters.req, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Download a finished request result * @param {DefaultApiDebugApiDownloadGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public debugApiDownloadGet(requestParameters: DefaultApiDebugApiDownloadGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).debugApiDownloadGet(requestParameters.token, options).then((request) => request(this.axios, this.basePath)); } /** * Delete all finished profiling tasks with a given group ID * @summary Delete all tasks with a given group ID * @param {DefaultApiDeleteProfilingGroupRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public deleteProfilingGroup(requestParameters: DefaultApiDeleteProfilingGroupRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).deleteProfilingGroup(requestParameters.groupId, options).then((request) => request(this.axios, this.basePath)); } /** * Generate sql diagnosis report * @summary SQL diagnosis report * @param {DefaultApiDiagnoseDiagnosisPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public diagnoseDiagnosisPost(requestParameters: DefaultApiDiagnoseDiagnosisPostRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).diagnoseDiagnosisPost(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Generate metrics relationship graph. * @param {DefaultApiDiagnoseGenerateMetricsRelationshipRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public diagnoseGenerateMetricsRelationship(requestParameters: DefaultApiDiagnoseGenerateMetricsRelationshipRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).diagnoseGenerateMetricsRelationship(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary View metrics relationship graph. * @param {DefaultApiDiagnoseMetricsRelationViewGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public diagnoseMetricsRelationViewGet(requestParameters: DefaultApiDiagnoseMetricsRelationViewGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).diagnoseMetricsRelationViewGet(requestParameters.token, options).then((request) => request(this.axios, this.basePath)); } /** * Get sql diagnosis reports history * @summary SQL diagnosis reports history * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public diagnoseReportsGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).diagnoseReportsGet(options).then((request) => request(this.axios, this.basePath)); } /** * Get sql diagnosis report data * @summary SQL diagnosis report data * @param {DefaultApiDiagnoseReportsIdDataJsGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public diagnoseReportsIdDataJsGet(requestParameters: DefaultApiDiagnoseReportsIdDataJsGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).diagnoseReportsIdDataJsGet(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * Get sql diagnosis report HTML * @summary SQL diagnosis report * @param {DefaultApiDiagnoseReportsIdDetailGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public diagnoseReportsIdDetailGet(requestParameters: DefaultApiDiagnoseReportsIdDetailGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).diagnoseReportsIdDetailGet(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * Get diagnosis report status * @summary Diagnosis report status * @param {DefaultApiDiagnoseReportsIdStatusGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public diagnoseReportsIdStatusGet(requestParameters: DefaultApiDiagnoseReportsIdStatusGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).diagnoseReportsIdStatusGet(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * Generate sql diagnosis report * @summary SQL diagnosis report * @param {DefaultApiDiagnoseReportsPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public diagnoseReportsPost(requestParameters: DefaultApiDiagnoseReportsPostRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).diagnoseReportsPost(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * Download all finished profiling results of a task group * @summary Download all results of a task group * @param {DefaultApiDownloadProfilingGroupRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public downloadProfilingGroup(requestParameters: DefaultApiDownloadProfilingGroupRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).downloadProfilingGroup(requestParameters.token, options).then((request) => request(this.axios, this.basePath)); } /** * Download the finished profiling result of a task * @summary Download the result of a task * @param {DefaultApiDownloadProfilingSingleRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public downloadProfilingSingle(requestParameters: DefaultApiDownloadProfilingSingleRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).downloadProfilingSingle(requestParameters.token, options).then((request) => request(this.axios, this.basePath)); } /** * Get token with a given group ID or task ID and action type * @summary Get action token for download or view * @param {DefaultApiGetActionTokenRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getActionToken(requestParameters: DefaultApiGetActionTokenRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getActionToken(requestParameters.id, requestParameters.action, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get current alert count from AlertManager * @param {DefaultApiGetAlertManagerCountsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getAlertManagerCounts(requestParameters: DefaultApiGetAlertManagerCountsRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getAlertManagerCounts(requestParameters.address, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get AlertManager instance * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getAlertManagerTopology(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getAlertManagerTopology(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get Grafana instance * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getGrafanaTopology(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getGrafanaTopology(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get all PD instances * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getPDTopology(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getPDTopology(options).then((request) => request(this.axios, this.basePath)); } /** * List all profiling tasks with a given group ID * @summary List all tasks with a given group ID * @param {DefaultApiGetProfilingGroupDetailRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getProfilingGroupDetail(requestParameters: DefaultApiGetProfilingGroupDetailRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getProfilingGroupDetail(requestParameters.groupId, options).then((request) => request(this.axios, this.basePath)); } /** * List all profiling groups * @summary List all profiling groups * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getProfilingGroups(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getProfilingGroups(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get all Scheduling instances * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getSchedulingTopology(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getSchedulingTopology(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get location labels of all TiKV / TiFlash instances * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getStoreLocationTopology(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getStoreLocationTopology(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get all TiKV / TiFlash instances * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getStoreTopology(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getStoreTopology(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get all TSO instances * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getTSOTopology(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getTSOTopology(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get all TiCDC instances * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getTiCDCTopology(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getTiCDCTopology(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get all TiDB instances * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getTiDBTopology(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getTiDBTopology(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get all TiProxy instances * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public getTiProxyTopology(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).getTiProxyTopology(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get information about this TiDB Dashboard * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public infoGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).infoGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary List all databases * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public infoListDatabases(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).infoListDatabases(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary List tables by database name * @param {DefaultApiInfoListTablesRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public infoListTables(requestParameters: DefaultApiInfoListTablesRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).infoListTables(requestParameters.databaseName, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get information about current session * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public infoWhoami(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).infoWhoami(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get Key Visual Dynamic Config * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public keyvisualConfigGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).keyvisualConfigGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Set Key Visual Dynamic Config * @param {DefaultApiKeyvisualConfigPutRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public keyvisualConfigPut(requestParameters: DefaultApiKeyvisualConfigPutRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).keyvisualConfigPut(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * Heatmaps in a given range to visualize TiKV usage * @summary Key Visual Heatmaps * @param {DefaultApiKeyvisualHeatmapsGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public keyvisualHeatmapsGet(requestParameters: DefaultApiKeyvisualHeatmapsGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).keyvisualHeatmapsGet(requestParameters.startkey, requestParameters.endkey, requestParameters.starttime, requestParameters.endtime, requestParameters.type, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Generate a download token for downloading logs * @param {DefaultApiLogsDownloadAcquireTokenGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public logsDownloadAcquireTokenGet(requestParameters: DefaultApiLogsDownloadAcquireTokenGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).logsDownloadAcquireTokenGet(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Download logs * @param {DefaultApiLogsDownloadGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public logsDownloadGet(requestParameters: DefaultApiLogsDownloadGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).logsDownloadGet(requestParameters.token, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Create and run a new log search task group * @param {DefaultApiLogsTaskgroupPutRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public logsTaskgroupPut(requestParameters: DefaultApiLogsTaskgroupPutRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).logsTaskgroupPut(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary List all log search task groups * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public logsTaskgroupsGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).logsTaskgroupsGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Cancel running tasks in a log search task group * @param {DefaultApiLogsTaskgroupsIdCancelPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public logsTaskgroupsIdCancelPost(requestParameters: DefaultApiLogsTaskgroupsIdCancelPostRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).logsTaskgroupsIdCancelPost(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Delete a log search task group * @param {DefaultApiLogsTaskgroupsIdDeleteRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public logsTaskgroupsIdDelete(requestParameters: DefaultApiLogsTaskgroupsIdDeleteRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).logsTaskgroupsIdDelete(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary List tasks in a log search task group * @param {DefaultApiLogsTaskgroupsIdGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public logsTaskgroupsIdGet(requestParameters: DefaultApiLogsTaskgroupsIdGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).logsTaskgroupsIdGet(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Preview a log search task group * @param {DefaultApiLogsTaskgroupsIdPreviewGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public logsTaskgroupsIdPreviewGet(requestParameters: DefaultApiLogsTaskgroupsIdPreviewGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).logsTaskgroupsIdPreviewGet(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Retry failed tasks in a log search task group * @param {DefaultApiLogsTaskgroupsIdRetryPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public logsTaskgroupsIdRetryPost(requestParameters: DefaultApiLogsTaskgroupsIdRetryPostRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).logsTaskgroupsIdRetryPost(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get the Prometheus address cluster config * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public metricsGetPromAddress(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).metricsGetPromAddress(options).then((request) => request(this.axios, this.basePath)); } /** * Query metrics in the given range * @summary Query metrics * @param {DefaultApiMetricsQueryGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public metricsQueryGet(requestParameters: DefaultApiMetricsQueryGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).metricsQueryGet(requestParameters.endTimeSec, requestParameters.query, requestParameters.startTimeSec, requestParameters.stepSec, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Set or clear the customized Prometheus address * @param {DefaultApiMetricsSetCustomPromAddressRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public metricsSetCustomPromAddress(requestParameters: DefaultApiMetricsSetCustomPromAddressRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).metricsSetCustomPromAddress(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get Profiling Dynamic Config * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public profilingConfigGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).profilingConfigGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Set Profiling Dynamic Config * @param {DefaultApiProfilingConfigPutRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public profilingConfigPut(requestParameters: DefaultApiProfilingConfigPutRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).profilingConfigPut(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Run statements * @param {DefaultApiQueryEditorRunRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public queryEditorRun(requestParameters: DefaultApiQueryEditorRunRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).queryEditorRun(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get calibrate of Resource Groups by actual workload * @param {DefaultApiResourceManagerCalibrateActualGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public resourceManagerCalibrateActualGet(requestParameters: DefaultApiResourceManagerCalibrateActualGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).resourceManagerCalibrateActualGet(requestParameters.endTime, requestParameters.startTime, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get calibrate of Resource Groups by hardware deployment * @param {DefaultApiResourceManagerCalibrateHardwareGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public resourceManagerCalibrateHardwareGet(requestParameters: DefaultApiResourceManagerCalibrateHardwareGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).resourceManagerCalibrateHardwareGet(requestParameters.workload, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get Resource Control enable config * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public resourceManagerConfigGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).resourceManagerConfigGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get Information of Resource Groups * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public resourceManagerInformationGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).resourceManagerInformationGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary List all resource groups * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public resourceManagerInformationGroupNamesGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).resourceManagerInformationGroupNamesGet(options).then((request) => request(this.axios, this.basePath)); } /** * Get available field names by slowquery table columns * @summary Get available field names * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public slowQueryAvailableFieldsGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).slowQueryAvailableFieldsGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get details of a slow query * @param {DefaultApiSlowQueryDetailGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public slowQueryDetailGet(requestParameters: DefaultApiSlowQueryDetailGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).slowQueryDetailGet(requestParameters.connectId, requestParameters.digest, requestParameters.timestamp, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Download slow query statements * @param {DefaultApiSlowQueryDownloadGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public slowQueryDownloadGet(requestParameters: DefaultApiSlowQueryDownloadGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).slowQueryDownloadGet(requestParameters.token, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Generate a download token for exported slow query statements * @param {DefaultApiSlowQueryDownloadTokenPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public slowQueryDownloadTokenPost(requestParameters: DefaultApiSlowQueryDownloadTokenPostRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).slowQueryDownloadTokenPost(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary List all slow queries * @param {DefaultApiSlowQueryListGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public slowQueryListGet(requestParameters: DefaultApiSlowQueryListGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).slowQueryListGet(requestParameters.beginTime, requestParameters.db, requestParameters.desc, requestParameters.digest, requestParameters.endTime, requestParameters.fields, requestParameters.limit, requestParameters.orderBy, requestParameters.plans, requestParameters.resourceGroup, requestParameters.text, options).then((request) => request(this.axios, this.basePath)); } /** * Start a profiling task group * @summary Start profiling * @param {DefaultApiStartProfilingRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public startProfiling(requestParameters: DefaultApiStartProfilingRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).startProfiling(requestParameters.req, options).then((request) => request(this.axios, this.basePath)); } /** * Get available field names by statements table columns * @summary Get available field names * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsAvailableFieldsGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsAvailableFieldsGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get statement configurations * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsConfigGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsConfigGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Update statement configurations * @param {DefaultApiStatementsConfigPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsConfigPost(requestParameters: DefaultApiStatementsConfigPostRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsConfigPost(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Download statements * @param {DefaultApiStatementsDownloadGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsDownloadGet(requestParameters: DefaultApiStatementsDownloadGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsDownloadGet(requestParameters.token, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Generate a download token for exported statements * @param {DefaultApiStatementsDownloadTokenPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsDownloadTokenPost(requestParameters: DefaultApiStatementsDownloadTokenPostRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsDownloadTokenPost(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get a list of statements * @param {DefaultApiStatementsListGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsListGet(requestParameters: DefaultApiStatementsListGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsListGet(requestParameters.beginTime, requestParameters.endTime, requestParameters.fields, requestParameters.resourceGroups, requestParameters.schemas, requestParameters.stmtTypes, requestParameters.text, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Drop all manually created bindings for a statement * @param {DefaultApiStatementsPlanBindingDeleteRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsPlanBindingDelete(requestParameters: DefaultApiStatementsPlanBindingDeleteRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsPlanBindingDelete(requestParameters.sqlDigest, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get the bound plan digest (if exists) of a statement * @param {DefaultApiStatementsPlanBindingGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsPlanBindingGet(requestParameters: DefaultApiStatementsPlanBindingGetRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsPlanBindingGet(requestParameters.sqlDigest, requestParameters.beginTime, requestParameters.endTime, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Create a binding for a statement and a plan * @param {DefaultApiStatementsPlanBindingPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsPlanBindingPost(requestParameters: DefaultApiStatementsPlanBindingPostRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsPlanBindingPost(requestParameters.planDigest, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get details of a statement in an execution plan * @param {DefaultApiStatementsPlanDetailGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsPlanDetailGet(requestParameters: DefaultApiStatementsPlanDetailGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsPlanDetailGet(requestParameters.beginTime, requestParameters.digest, requestParameters.endTime, requestParameters.plans, requestParameters.schemaName, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get execution plans of a statement * @param {DefaultApiStatementsPlansGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsPlansGet(requestParameters: DefaultApiStatementsPlansGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsPlansGet(requestParameters.beginTime, requestParameters.digest, requestParameters.endTime, requestParameters.schemaName, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get all statement types * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public statementsStmtTypesGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).statementsStmtTypesGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Hide a TiDB instance * @param {DefaultApiTopologyTidbAddressDeleteRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public topologyTidbAddressDelete(requestParameters: DefaultApiTopologyTidbAddressDeleteRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).topologyTidbAddressDelete(requestParameters.address, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get Top SQL config * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public topsqlConfigGet(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).topsqlConfigGet(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Update Top SQL config * @param {DefaultApiTopsqlConfigPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public topsqlConfigPost(requestParameters: DefaultApiTopsqlConfigPostRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).topsqlConfigPost(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get TiKV network IO collection config * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public topsqlGetTiKVNetworkIOCollection(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).topsqlGetTiKVNetworkIOCollection(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get available instances * @param {DefaultApiTopsqlInstancesGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public topsqlInstancesGet(requestParameters: DefaultApiTopsqlInstancesGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).topsqlInstancesGet(requestParameters.dataSource, requestParameters.end, requestParameters.start, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get summaries * @param {DefaultApiTopsqlSummaryGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public topsqlSummaryGet(requestParameters: DefaultApiTopsqlSummaryGetRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).topsqlSummaryGet(requestParameters.dataSource, requestParameters.end, requestParameters.groupBy, requestParameters.instance, requestParameters.instanceType, requestParameters.orderBy, requestParameters.start, requestParameters.top, requestParameters.window, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Update TiKV network IO collection config * @param {DefaultApiTopsqlUpdateTiKVNetworkIOCollectionRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public topsqlUpdateTiKVNetworkIOCollection(requestParameters: DefaultApiTopsqlUpdateTiKVNetworkIOCollectionRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).topsqlUpdateTiKVNetworkIOCollection(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get log in information, like supported authenticate types * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public userGetLoginInfo(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).userGetLoginInfo(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get sign out info * @param {DefaultApiUserGetSignOutInfoRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public userGetSignOutInfo(requestParameters: DefaultApiUserGetSignOutInfoRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).userGetSignOutInfo(requestParameters.redirectUrl, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Log in * @param {DefaultApiUserLoginRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public userLogin(requestParameters: DefaultApiUserLoginRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).userLogin(requestParameters.message, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Reset encryption key to revoke all authorized codes * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public userRevokeSession(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).userRevokeSession(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Create an impersonation * @param {DefaultApiUserSSOCreateImpersonationRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public userSSOCreateImpersonation(requestParameters: DefaultApiUserSSOCreateImpersonationRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).userSSOCreateImpersonation(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get SSO Auth URL * @param {DefaultApiUserSSOGetAuthURLRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public userSSOGetAuthURL(requestParameters: DefaultApiUserSSOGetAuthURLRequest = {}, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).userSSOGetAuthURL(requestParameters.codeVerifier, requestParameters.redirectUrl, requestParameters.state, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get SSO config * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public userSSOGetConfig(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).userSSOGetConfig(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary List all impersonations * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public userSSOListImpersonations(options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).userSSOListImpersonations(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Set SSO config * @param {DefaultApiUserSSOSetConfigRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public userSSOSetConfig(requestParameters: DefaultApiUserSSOSetConfigRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).userSSOSetConfig(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Share current session and generate a sharing code * @param {DefaultApiUserShareSessionRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public userShareSession(requestParameters: DefaultApiUserShareSessionRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).userShareSession(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } /** * View the finished profiling result of a task * @summary View the result of a task * @param {DefaultApiViewProfilingSingleRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ public viewProfilingSingle(requestParameters: DefaultApiViewProfilingSingleRequest, options?: AxiosRequestConfig) { return DefaultApiFp(this.configuration).viewProfilingSingle(requestParameters.token, options).then((request) => request(this.axios, this.basePath)); } } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/api/statement-api.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; import { Configuration } from '../configuration'; // Some imports not used depending on template conditions // @ts-ignore import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base'; // @ts-ignore import { RestErrorResponse } from '../models'; // @ts-ignore import { StatementBinding } from '../models'; /** * StatementApi - axios parameter creator * @export */ export const StatementApiAxiosParamCreator = function (configuration?: Configuration) { return { /** * * @summary Drop all manually created bindings for a statement * @param {string} sqlDigest query template ID (a.k.a. sql digest) * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingDelete: async (sqlDigest: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'sqlDigest' is not null or undefined assertParamExists('statementsPlanBindingDelete', 'sqlDigest', sqlDigest) const localVarPath = `/statements/plan/binding`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (sqlDigest !== undefined) { localVarQueryParameter['sql_digest'] = sqlDigest; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Get the bound plan digest (if exists) of a statement * @param {string} sqlDigest query template id * @param {number} beginTime begin time * @param {number} endTime end time * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingGet: async (sqlDigest: string, beginTime: number, endTime: number, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'sqlDigest' is not null or undefined assertParamExists('statementsPlanBindingGet', 'sqlDigest', sqlDigest) // verify required parameter 'beginTime' is not null or undefined assertParamExists('statementsPlanBindingGet', 'beginTime', beginTime) // verify required parameter 'endTime' is not null or undefined assertParamExists('statementsPlanBindingGet', 'endTime', endTime) const localVarPath = `/statements/plan/binding`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (sqlDigest !== undefined) { localVarQueryParameter['sql_digest'] = sqlDigest; } if (beginTime !== undefined) { localVarQueryParameter['begin_time'] = beginTime; } if (endTime !== undefined) { localVarQueryParameter['end_time'] = endTime; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, /** * * @summary Create a binding for a statement and a plan * @param {string} planDigest plan digest id * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingPost: async (planDigest: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'planDigest' is not null or undefined assertParamExists('statementsPlanBindingPost', 'planDigest', planDigest) const localVarPath = `/statements/plan/binding`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; if (configuration) { baseOptions = configuration.baseOptions; } const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; // authentication JwtAuth required await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) if (planDigest !== undefined) { localVarQueryParameter['plan_digest'] = planDigest; } setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, }; }, } }; /** * StatementApi - functional programming interface * @export */ export const StatementApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = StatementApiAxiosParamCreator(configuration) return { /** * * @summary Drop all manually created bindings for a statement * @param {string} sqlDigest query template ID (a.k.a. sql digest) * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsPlanBindingDelete(sqlDigest: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsPlanBindingDelete(sqlDigest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Get the bound plan digest (if exists) of a statement * @param {string} sqlDigest query template id * @param {number} beginTime begin time * @param {number} endTime end time * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsPlanBindingGet(sqlDigest: string, beginTime: number, endTime: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsPlanBindingGet(sqlDigest, beginTime, endTime, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Create a binding for a statement and a plan * @param {string} planDigest plan digest id * @param {*} [options] Override http request option. * @throws {RequiredError} */ async statementsPlanBindingPost(planDigest: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.statementsPlanBindingPost(planDigest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } }; /** * StatementApi - factory interface * @export */ export const StatementApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = StatementApiFp(configuration) return { /** * * @summary Drop all manually created bindings for a statement * @param {string} sqlDigest query template ID (a.k.a. sql digest) * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingDelete(sqlDigest: string, options?: any): AxiosPromise { return localVarFp.statementsPlanBindingDelete(sqlDigest, options).then((request) => request(axios, basePath)); }, /** * * @summary Get the bound plan digest (if exists) of a statement * @param {string} sqlDigest query template id * @param {number} beginTime begin time * @param {number} endTime end time * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingGet(sqlDigest: string, beginTime: number, endTime: number, options?: any): AxiosPromise> { return localVarFp.statementsPlanBindingGet(sqlDigest, beginTime, endTime, options).then((request) => request(axios, basePath)); }, /** * * @summary Create a binding for a statement and a plan * @param {string} planDigest plan digest id * @param {*} [options] Override http request option. * @throws {RequiredError} */ statementsPlanBindingPost(planDigest: string, options?: any): AxiosPromise { return localVarFp.statementsPlanBindingPost(planDigest, options).then((request) => request(axios, basePath)); }, }; }; /** * Request parameters for statementsPlanBindingDelete operation in StatementApi. * @export * @interface StatementApiStatementsPlanBindingDeleteRequest */ export interface StatementApiStatementsPlanBindingDeleteRequest { /** * query template ID (a.k.a. sql digest) * @type {string} * @memberof StatementApiStatementsPlanBindingDelete */ readonly sqlDigest: string } /** * Request parameters for statementsPlanBindingGet operation in StatementApi. * @export * @interface StatementApiStatementsPlanBindingGetRequest */ export interface StatementApiStatementsPlanBindingGetRequest { /** * query template id * @type {string} * @memberof StatementApiStatementsPlanBindingGet */ readonly sqlDigest: string /** * begin time * @type {number} * @memberof StatementApiStatementsPlanBindingGet */ readonly beginTime: number /** * end time * @type {number} * @memberof StatementApiStatementsPlanBindingGet */ readonly endTime: number } /** * Request parameters for statementsPlanBindingPost operation in StatementApi. * @export * @interface StatementApiStatementsPlanBindingPostRequest */ export interface StatementApiStatementsPlanBindingPostRequest { /** * plan digest id * @type {string} * @memberof StatementApiStatementsPlanBindingPost */ readonly planDigest: string } /** * StatementApi - object-oriented interface * @export * @class StatementApi * @extends {BaseAPI} */ export class StatementApi extends BaseAPI { /** * * @summary Drop all manually created bindings for a statement * @param {StatementApiStatementsPlanBindingDeleteRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StatementApi */ public statementsPlanBindingDelete(requestParameters: StatementApiStatementsPlanBindingDeleteRequest, options?: AxiosRequestConfig) { return StatementApiFp(this.configuration).statementsPlanBindingDelete(requestParameters.sqlDigest, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Get the bound plan digest (if exists) of a statement * @param {StatementApiStatementsPlanBindingGetRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StatementApi */ public statementsPlanBindingGet(requestParameters: StatementApiStatementsPlanBindingGetRequest, options?: AxiosRequestConfig) { return StatementApiFp(this.configuration).statementsPlanBindingGet(requestParameters.sqlDigest, requestParameters.beginTime, requestParameters.endTime, options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Create a binding for a statement and a plan * @param {StatementApiStatementsPlanBindingPostRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StatementApi */ public statementsPlanBindingPost(requestParameters: StatementApiStatementsPlanBindingPostRequest, options?: AxiosRequestConfig) { return StatementApiFp(this.configuration).statementsPlanBindingPost(requestParameters.planDigest, options).then((request) => request(this.axios, this.basePath)); } } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/api.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ export * from './api/default-api'; ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/base.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { Configuration } from "./configuration"; // Some imports not used depending on template conditions // @ts-ignore import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; export const BASE_PATH = "/dashboard/api".replace(/\/+$/, ""); /** * * @export */ export const COLLECTION_FORMATS = { csv: ",", ssv: " ", tsv: "\t", pipes: "|", }; /** * * @export * @interface RequestArgs */ export interface RequestArgs { url: string; options: AxiosRequestConfig; } /** * * @export * @class BaseAPI */ export class BaseAPI { protected configuration: Configuration | undefined; constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { if (configuration) { this.configuration = configuration; this.basePath = configuration.basePath || this.basePath; } } }; /** * * @export * @class RequiredError * @extends {Error} */ export class RequiredError extends Error { name: "RequiredError" = "RequiredError"; constructor(public field: string, msg?: string) { super(msg); } } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/common.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { Configuration } from "./configuration"; import { RequiredError, RequestArgs } from "./base"; import { AxiosInstance, AxiosResponse } from 'axios'; /** * * @export */ export const DUMMY_BASE_URL = 'https://example.com' /** * * @throws {RequiredError} * @export */ export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { if (paramValue === null || paramValue === undefined) { throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); } } /** * * @export */ export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { if (configuration && configuration.apiKey) { const localVarApiKeyValue = typeof configuration.apiKey === 'function' ? await configuration.apiKey(keyParamName) : await configuration.apiKey; object[keyParamName] = localVarApiKeyValue; } } /** * * @export */ export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { if (configuration && (configuration.username || configuration.password)) { object["auth"] = { username: configuration.username, password: configuration.password }; } } /** * * @export */ export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { if (configuration && configuration.accessToken) { const accessToken = typeof configuration.accessToken === 'function' ? await configuration.accessToken() : await configuration.accessToken; object["Authorization"] = "Bearer " + accessToken; } } /** * * @export */ export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { if (configuration && configuration.accessToken) { const localVarAccessTokenValue = typeof configuration.accessToken === 'function' ? await configuration.accessToken(name, scopes) : await configuration.accessToken; object["Authorization"] = "Bearer " + localVarAccessTokenValue; } } /** * * @export */ export const setSearchParams = function (url: URL, ...objects: any[]) { const searchParams = new URLSearchParams(url.search); for (const object of objects) { for (const key in object) { if (Array.isArray(object[key])) { searchParams.delete(key); for (const item of object[key]) { searchParams.append(key, item); } } else { searchParams.set(key, object[key]); } } } url.search = searchParams.toString(); } /** * * @export */ export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { const nonString = typeof value !== 'string'; const needsSerialization = nonString && configuration && configuration.isJsonMime ? configuration.isJsonMime(requestOptions.headers['Content-Type']) : nonString; return needsSerialization ? JSON.stringify(value !== undefined ? value : {}) : (value || ""); } /** * * @export */ export const toPathString = function (url: URL) { return url.pathname + url.search + url.hash } /** * * @export */ export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url}; return axios.request(axiosRequestArgs); }; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/configuration.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ export interface ConfigurationParameters { apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); username?: string; password?: string; accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); basePath?: string; baseOptions?: any; formDataCtor?: new () => any; } export class Configuration { /** * parameter for apiKey security * @param name security name * @memberof Configuration */ apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); /** * parameter for basic security * * @type {string} * @memberof Configuration */ username?: string; /** * parameter for basic security * * @type {string} * @memberof Configuration */ password?: string; /** * parameter for oauth2 security * @param name security name * @param scopes oauth2 scope * @memberof Configuration */ accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); /** * override base path * * @type {string} * @memberof Configuration */ basePath?: string; /** * base options for axios calls * * @type {any} * @memberof Configuration */ baseOptions?: any; /** * The FormData constructor that will be used to create multipart form data * requests. You can inject this here so that execution environments that * do not support the FormData class can still run the generated client. * * @type {new () => FormData} */ formDataCtor?: new () => any; constructor(param: ConfigurationParameters = {}) { this.apiKey = param.apiKey; this.username = param.username; this.password = param.password; this.accessToken = param.accessToken; this.basePath = param.basePath; this.baseOptions = param.baseOptions; this.formDataCtor = param.formDataCtor; } /** * Check if the given MIME is a JSON MIME. * JSON MIME examples: * application/json * application/json; charset=UTF8 * APPLICATION/JSON * application/vnd.company+json * @param mime - MIME (Multipurpose Internet Mail Extensions) * @return True if the given MIME is JSON, false otherwise. */ public isJsonMime(mime: string): boolean { const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); } } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/index.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ export * from "./api"; export * from "./configuration"; export * from "./models"; ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/clusterinfo-cluster-statistics-partial.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ClusterinfoClusterStatisticsPartial */ export interface ClusterinfoClusterStatisticsPartial { /** * * @type {number} * @memberof ClusterinfoClusterStatisticsPartial */ 'number_of_hosts'?: number; /** * * @type {number} * @memberof ClusterinfoClusterStatisticsPartial */ 'number_of_instances'?: number; /** * * @type {number} * @memberof ClusterinfoClusterStatisticsPartial */ 'total_logical_cores'?: number; /** * * @type {number} * @memberof ClusterinfoClusterStatisticsPartial */ 'total_memory_capacity_bytes'?: number; /** * * @type {number} * @memberof ClusterinfoClusterStatisticsPartial */ 'total_physical_cores'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/clusterinfo-cluster-statistics.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ClusterinfoClusterStatisticsPartial } from './clusterinfo-cluster-statistics-partial'; /** * * @export * @interface ClusterinfoClusterStatistics */ export interface ClusterinfoClusterStatistics { /** * * @type {number} * @memberof ClusterinfoClusterStatistics */ 'probe_failure_hosts'?: number; /** * * @type {{ [key: string]: ClusterinfoClusterStatisticsPartial; }} * @memberof ClusterinfoClusterStatistics */ 'stats_by_instance_kind'?: { [key: string]: ClusterinfoClusterStatisticsPartial; }; /** * * @type {ClusterinfoClusterStatisticsPartial} * @memberof ClusterinfoClusterStatistics */ 'total_stats'?: ClusterinfoClusterStatisticsPartial; /** * * @type {Array} * @memberof ClusterinfoClusterStatistics */ 'versions'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/clusterinfo-get-hosts-info-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { HostinfoInfo } from './hostinfo-info'; import { RestErrorResponse } from './rest-error-response'; /** * * @export * @interface ClusterinfoGetHostsInfoResponse */ export interface ClusterinfoGetHostsInfoResponse { /** * * @type {Array} * @memberof ClusterinfoGetHostsInfoResponse */ 'hosts'?: Array; /** * * @type {RestErrorResponse} * @memberof ClusterinfoGetHostsInfoResponse */ 'warning'?: RestErrorResponse; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/clusterinfo-store-topology-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { TopologyStoreInfo } from './topology-store-info'; /** * * @export * @interface ClusterinfoStoreTopologyResponse */ export interface ClusterinfoStoreTopologyResponse { /** * * @type {Array} * @memberof ClusterinfoStoreTopologyResponse */ 'tiflash'?: Array; /** * * @type {Array} * @memberof ClusterinfoStoreTopologyResponse */ 'tikv'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/code-share-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface CodeShareRequest */ export interface CodeShareRequest { /** * * @type {number} * @memberof CodeShareRequest */ 'expire_in_sec'?: number; /** * * @type {boolean} * @memberof CodeShareRequest */ 'revoke_write_priv'?: boolean; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/code-share-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface CodeShareResponse */ export interface CodeShareResponse { /** * * @type {string} * @memberof CodeShareResponse */ 'code'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/config-key-visual-config.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ConfigKeyVisualConfig */ export interface ConfigKeyVisualConfig { /** * * @type {boolean} * @memberof ConfigKeyVisualConfig */ 'auto_collection_disabled'?: boolean; /** * * @type {string} * @memberof ConfigKeyVisualConfig */ 'policy'?: string; /** * * @type {string} * @memberof ConfigKeyVisualConfig */ 'policy_kv_separator'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/config-profiling-config.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ModelRequestTargetNode } from './model-request-target-node'; /** * * @export * @interface ConfigProfilingConfig */ export interface ConfigProfilingConfig { /** * * @type {number} * @memberof ConfigProfilingConfig */ 'auto_collection_duration_secs'?: number; /** * * @type {number} * @memberof ConfigProfilingConfig */ 'auto_collection_interval_secs'?: number; /** * * @type {Array} * @memberof ConfigProfilingConfig */ 'auto_collection_targets'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/config-ssocore-config.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ConfigSSOCoreConfig */ export interface ConfigSSOCoreConfig { /** * * @type {string} * @memberof ConfigSSOCoreConfig */ 'client_id'?: string; /** * * @type {string} * @memberof ConfigSSOCoreConfig */ 'client_secret'?: string; /** * * @type {string} * @memberof ConfigSSOCoreConfig */ 'discovery_url'?: string; /** * * @type {boolean} * @memberof ConfigSSOCoreConfig */ 'enabled'?: boolean; /** * * @type {boolean} * @memberof ConfigSSOCoreConfig */ 'is_read_only'?: boolean; /** * * @type {string} * @memberof ConfigSSOCoreConfig */ 'scopes'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/configuration-all-config-items.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ConfigurationItem } from './configuration-item'; import { RestErrorResponse } from './rest-error-response'; /** * * @export * @interface ConfigurationAllConfigItems */ export interface ConfigurationAllConfigItems { /** * * @type {Array} * @memberof ConfigurationAllConfigItems */ 'errors'?: Array; /** * * @type {{ [key: string]: Array; }} * @memberof ConfigurationAllConfigItems */ 'items'?: { [key: string]: Array; }; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/configuration-edit-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ConfigurationEditRequest */ export interface ConfigurationEditRequest { /** * * @type {string} * @memberof ConfigurationEditRequest */ 'id'?: string; /** * * @type {string} * @memberof ConfigurationEditRequest */ 'kind'?: string; /** * * @type {object} * @memberof ConfigurationEditRequest */ 'new_value'?: object; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/configuration-edit-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { RestErrorResponse } from './rest-error-response'; /** * * @export * @interface ConfigurationEditResponse */ export interface ConfigurationEditResponse { /** * * @type {Array} * @memberof ConfigurationEditResponse */ 'warnings'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/configuration-item.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ConfigurationItem */ export interface ConfigurationItem { /** * * @type {string} * @memberof ConfigurationItem */ 'id'?: string; /** * * @type {boolean} * @memberof ConfigurationItem */ 'is_editable'?: boolean; /** * TODO: Support per-instance config * @type {boolean} * @memberof ConfigurationItem */ 'is_multi_value'?: boolean; /** * When multi value present, this contains one of the value * @type {object} * @memberof ConfigurationItem */ 'value'?: object; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/conprof-component-num.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ConprofComponentNum */ export interface ConprofComponentNum { /** * * @type {number} * @memberof ConprofComponentNum */ 'pd'?: number; /** * * @type {number} * @memberof ConprofComponentNum */ 'ticdc'?: number; /** * * @type {number} * @memberof ConprofComponentNum */ 'tidb'?: number; /** * * @type {number} * @memberof ConprofComponentNum */ 'tiflash'?: number; /** * * @type {number} * @memberof ConprofComponentNum */ 'tikv'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/conprof-component.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ConprofComponent */ export interface ConprofComponent { /** * * @type {string} * @memberof ConprofComponent */ 'ip'?: string; /** * * @type {string} * @memberof ConprofComponent */ 'name'?: string; /** * * @type {number} * @memberof ConprofComponent */ 'port'?: number; /** * * @type {number} * @memberof ConprofComponent */ 'status_port'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/conprof-continuous-profiling-config.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ConprofContinuousProfilingConfig */ export interface ConprofContinuousProfilingConfig { /** * * @type {number} * @memberof ConprofContinuousProfilingConfig */ 'data_retention_seconds'?: number; /** * * @type {boolean} * @memberof ConprofContinuousProfilingConfig */ 'enable'?: boolean; /** * * @type {number} * @memberof ConprofContinuousProfilingConfig */ 'interval_seconds'?: number; /** * * @type {number} * @memberof ConprofContinuousProfilingConfig */ 'profile_seconds'?: number; /** * * @type {number} * @memberof ConprofContinuousProfilingConfig */ 'timeout_seconds'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/conprof-estimate-size-res.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ConprofEstimateSizeRes */ export interface ConprofEstimateSizeRes { /** * * @type {number} * @memberof ConprofEstimateSizeRes */ 'instance_count'?: number; /** * * @type {number} * @memberof ConprofEstimateSizeRes */ 'profile_size'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/conprof-group-profile-detail.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ConprofProfileDetail } from './conprof-profile-detail'; /** * * @export * @interface ConprofGroupProfileDetail */ export interface ConprofGroupProfileDetail { /** * * @type {number} * @memberof ConprofGroupProfileDetail */ 'profile_duration_secs'?: number; /** * * @type {string} * @memberof ConprofGroupProfileDetail */ 'state'?: string; /** * * @type {Array} * @memberof ConprofGroupProfileDetail */ 'target_profiles'?: Array; /** * * @type {number} * @memberof ConprofGroupProfileDetail */ 'ts'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/conprof-group-profiles.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ConprofComponentNum } from './conprof-component-num'; /** * * @export * @interface ConprofGroupProfiles */ export interface ConprofGroupProfiles { /** * * @type {ConprofComponentNum} * @memberof ConprofGroupProfiles */ 'component_num'?: ConprofComponentNum; /** * * @type {number} * @memberof ConprofGroupProfiles */ 'profile_duration_secs'?: number; /** * * @type {string} * @memberof ConprofGroupProfiles */ 'state'?: string; /** * * @type {number} * @memberof ConprofGroupProfiles */ 'ts'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/conprof-ng-monitoring-config.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ConprofContinuousProfilingConfig } from './conprof-continuous-profiling-config'; /** * * @export * @interface ConprofNgMonitoringConfig */ export interface ConprofNgMonitoringConfig { /** * * @type {ConprofContinuousProfilingConfig} * @memberof ConprofNgMonitoringConfig */ 'continuous_profiling'?: ConprofContinuousProfilingConfig; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/conprof-profile-detail.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ConprofTarget } from './conprof-target'; /** * * @export * @interface ConprofProfileDetail */ export interface ConprofProfileDetail { /** * * @type {string} * @memberof ConprofProfileDetail */ 'error'?: string; /** * * @type {string} * @memberof ConprofProfileDetail */ 'profile_type'?: string; /** * * @type {string} * @memberof ConprofProfileDetail */ 'state'?: string; /** * * @type {ConprofTarget} * @memberof ConprofProfileDetail */ 'target'?: ConprofTarget; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/conprof-target.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ConprofTarget */ export interface ConprofTarget { /** * * @type {string} * @memberof ConprofTarget */ 'address'?: string; /** * * @type {string} * @memberof ConprofTarget */ 'component'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/deadlock-model.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface DeadlockModel */ export interface DeadlockModel { /** * * @type {string} * @memberof DeadlockModel */ 'current_sql'?: string; /** * * @type {number} * @memberof DeadlockModel */ 'id'?: number; /** * * @type {string} * @memberof DeadlockModel */ 'instance'?: string; /** * * @type {string} * @memberof DeadlockModel */ 'key'?: string; /** * * @type {string} * @memberof DeadlockModel */ 'key_info'?: string; /** * * @type {string} * @memberof DeadlockModel */ 'occur_time'?: string; /** * * @type {boolean} * @memberof DeadlockModel */ 'retryable'?: boolean; /** * * @type {number} * @memberof DeadlockModel */ 'trx_holding_lock'?: number; /** * * @type {number} * @memberof DeadlockModel */ 'try_lock_trx_id'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/decorator-label-key.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface DecoratorLabelKey */ export interface DecoratorLabelKey { /** * * @type {string} * @memberof DecoratorLabelKey */ 'key': string; /** * * @type {Array} * @memberof DecoratorLabelKey */ 'labels': Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/diagnose-gen-diagnosis-report-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface DiagnoseGenDiagnosisReportRequest */ export interface DiagnoseGenDiagnosisReportRequest { /** * * @type {number} * @memberof DiagnoseGenDiagnosisReportRequest */ 'end_time'?: number; /** * values: config, error, performance * @type {string} * @memberof DiagnoseGenDiagnosisReportRequest */ 'kind'?: string; /** * * @type {number} * @memberof DiagnoseGenDiagnosisReportRequest */ 'start_time'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/diagnose-generate-metrics-relation-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface DiagnoseGenerateMetricsRelationRequest */ export interface DiagnoseGenerateMetricsRelationRequest { /** * * @type {number} * @memberof DiagnoseGenerateMetricsRelationRequest */ 'end_time'?: number; /** * * @type {number} * @memberof DiagnoseGenerateMetricsRelationRequest */ 'start_time'?: number; /** * * @type {string} * @memberof DiagnoseGenerateMetricsRelationRequest */ 'type'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/diagnose-generate-report-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface DiagnoseGenerateReportRequest */ export interface DiagnoseGenerateReportRequest { /** * * @type {number} * @memberof DiagnoseGenerateReportRequest */ 'compare_end_time'?: number; /** * * @type {number} * @memberof DiagnoseGenerateReportRequest */ 'compare_start_time'?: number; /** * * @type {number} * @memberof DiagnoseGenerateReportRequest */ 'end_time'?: number; /** * * @type {number} * @memberof DiagnoseGenerateReportRequest */ 'start_time'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/diagnose-report.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface DiagnoseReport */ export interface DiagnoseReport { /** * * @type {string} * @memberof DiagnoseReport */ 'compare_end_time'?: string; /** * * @type {string} * @memberof DiagnoseReport */ 'compare_start_time'?: string; /** * * @type {string} * @memberof DiagnoseReport */ 'content'?: string; /** * * @type {string} * @memberof DiagnoseReport */ 'created_at'?: string; /** * * @type {string} * @memberof DiagnoseReport */ 'end_time'?: string; /** * * @type {string} * @memberof DiagnoseReport */ 'id'?: string; /** * 0~100 * @type {number} * @memberof DiagnoseReport */ 'progress'?: number; /** * * @type {string} * @memberof DiagnoseReport */ 'start_time'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/diagnose-table-def.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { DiagnoseTableRowDef } from './diagnose-table-row-def'; /** * * @export * @interface DiagnoseTableDef */ export interface DiagnoseTableDef { /** * The category of the table, such as [TiDB] * @type {Array} * @memberof DiagnoseTableDef */ 'category'?: Array; /** * * @type {Array} * @memberof DiagnoseTableDef */ 'column'?: Array; /** * * @type {string} * @memberof DiagnoseTableDef */ 'comment'?: string; /** * * @type {Array} * @memberof DiagnoseTableDef */ 'rows'?: Array; /** * * @type {string} * @memberof DiagnoseTableDef */ 'title'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/diagnose-table-row-def.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface DiagnoseTableRowDef */ export interface DiagnoseTableRowDef { /** * * @type {string} * @memberof DiagnoseTableRowDef */ 'comment'?: string; /** * SubValues need fold default. * @type {Array>} * @memberof DiagnoseTableRowDef */ 'sub_values'?: Array>; /** * * @type {Array} * @memberof DiagnoseTableRowDef */ 'values'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/endpoint-apidefinition.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { EndpointAPIParamDefinition } from './endpoint-apiparam-definition'; /** * * @export * @interface EndpointAPIDefinition */ export interface EndpointAPIDefinition { /** * * @type {string} * @memberof EndpointAPIDefinition */ 'component'?: string; /** * * @type {string} * @memberof EndpointAPIDefinition */ 'id'?: string; /** * * @type {string} * @memberof EndpointAPIDefinition */ 'method'?: string; /** * * @type {string} * @memberof EndpointAPIDefinition */ 'path'?: string; /** * e.g. /stats/dump/{db}/{table} -> db, table * @type {Array} * @memberof EndpointAPIDefinition */ 'path_params'?: Array; /** * e.g. /debug/pprof?seconds=1 -> seconds * @type {Array} * @memberof EndpointAPIDefinition */ 'query_params'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/endpoint-apiparam-definition.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface EndpointAPIParamDefinition */ export interface EndpointAPIParamDefinition { /** * * @type {string} * @memberof EndpointAPIParamDefinition */ 'name'?: string; /** * * @type {boolean} * @memberof EndpointAPIParamDefinition */ 'required'?: boolean; /** * * @type {string} * @memberof EndpointAPIParamDefinition */ 'ui_kind'?: string; /** * varies by different ui kinds * @type {object} * @memberof EndpointAPIParamDefinition */ 'ui_props'?: object; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/endpoint-request-payload.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface EndpointRequestPayload */ export interface EndpointRequestPayload { /** * * @type {string} * @memberof EndpointRequestPayload */ 'api_id'?: string; /** * * @type {string} * @memberof EndpointRequestPayload */ 'host'?: string; /** * * @type {{ [key: string]: string; }} * @memberof EndpointRequestPayload */ 'param_values'?: { [key: string]: string; }; /** * * @type {number} * @memberof EndpointRequestPayload */ 'port'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/hostinfo-cpuinfo.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface HostinfoCPUInfo */ export interface HostinfoCPUInfo { /** * * @type {string} * @memberof HostinfoCPUInfo */ 'arch'?: string; /** * * @type {number} * @memberof HostinfoCPUInfo */ 'logical_cores'?: number; /** * * @type {number} * @memberof HostinfoCPUInfo */ 'physical_cores'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/hostinfo-cpuusage-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface HostinfoCPUUsageInfo */ export interface HostinfoCPUUsageInfo { /** * * @type {number} * @memberof HostinfoCPUUsageInfo */ 'idle'?: number; /** * * @type {number} * @memberof HostinfoCPUUsageInfo */ 'system'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/hostinfo-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { HostinfoCPUInfo } from './hostinfo-cpuinfo'; import { HostinfoCPUUsageInfo } from './hostinfo-cpuusage-info'; import { HostinfoInstanceInfo } from './hostinfo-instance-info'; import { HostinfoMemoryUsageInfo } from './hostinfo-memory-usage-info'; import { HostinfoPartitionInfo } from './hostinfo-partition-info'; /** * * @export * @interface HostinfoInfo */ export interface HostinfoInfo { /** * * @type {HostinfoCPUInfo} * @memberof HostinfoInfo */ 'cpu_info'?: HostinfoCPUInfo; /** * * @type {HostinfoCPUUsageInfo} * @memberof HostinfoInfo */ 'cpu_usage'?: HostinfoCPUUsageInfo; /** * * @type {string} * @memberof HostinfoInfo */ 'host'?: string; /** * Instances in the current host. The key is instance address * @type {{ [key: string]: HostinfoInstanceInfo; }} * @memberof HostinfoInfo */ 'instances'?: { [key: string]: HostinfoInstanceInfo; }; /** * * @type {HostinfoMemoryUsageInfo} * @memberof HostinfoInfo */ 'memory_usage'?: HostinfoMemoryUsageInfo; /** * 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. * @type {{ [key: string]: HostinfoPartitionInfo; }} * @memberof HostinfoInfo */ 'partitions'?: { [key: string]: HostinfoPartitionInfo; }; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/hostinfo-instance-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface HostinfoInstanceInfo */ export interface HostinfoInstanceInfo { /** * * @type {string} * @memberof HostinfoInstanceInfo */ 'partition_path_lower'?: string; /** * * @type {string} * @memberof HostinfoInstanceInfo */ 'type'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/hostinfo-memory-usage-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface HostinfoMemoryUsageInfo */ export interface HostinfoMemoryUsageInfo { /** * * @type {number} * @memberof HostinfoMemoryUsageInfo */ 'total'?: number; /** * * @type {number} * @memberof HostinfoMemoryUsageInfo */ 'used'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/hostinfo-partition-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface HostinfoPartitionInfo */ export interface HostinfoPartitionInfo { /** * * @type {number} * @memberof HostinfoPartitionInfo */ 'free'?: number; /** * * @type {string} * @memberof HostinfoPartitionInfo */ 'fstype'?: string; /** * * @type {string} * @memberof HostinfoPartitionInfo */ 'path'?: string; /** * * @type {number} * @memberof HostinfoPartitionInfo */ 'total'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/index.ts ================================================ export * from './clusterinfo-cluster-statistics'; export * from './clusterinfo-cluster-statistics-partial'; export * from './clusterinfo-get-hosts-info-response'; export * from './clusterinfo-store-topology-response'; export * from './code-share-request'; export * from './code-share-response'; export * from './config-key-visual-config'; export * from './config-profiling-config'; export * from './config-ssocore-config'; export * from './configuration-all-config-items'; export * from './configuration-edit-request'; export * from './configuration-edit-response'; export * from './configuration-item'; export * from './conprof-component'; export * from './conprof-component-num'; export * from './conprof-continuous-profiling-config'; export * from './conprof-estimate-size-res'; export * from './conprof-group-profile-detail'; export * from './conprof-group-profiles'; export * from './conprof-ng-monitoring-config'; export * from './conprof-profile-detail'; export * from './conprof-target'; export * from './deadlock-model'; export * from './decorator-label-key'; export * from './diagnose-gen-diagnosis-report-request'; export * from './diagnose-generate-metrics-relation-request'; export * from './diagnose-generate-report-request'; export * from './diagnose-report'; export * from './diagnose-table-def'; export * from './diagnose-table-row-def'; export * from './endpoint-apidefinition'; export * from './endpoint-apiparam-definition'; export * from './endpoint-request-payload'; export * from './hostinfo-cpuinfo'; export * from './hostinfo-cpuusage-info'; export * from './hostinfo-info'; export * from './hostinfo-instance-info'; export * from './hostinfo-memory-usage-info'; export * from './hostinfo-partition-info'; export * from './info-info-response'; export * from './info-table-schema'; export * from './info-who-am-iresponse'; export * from './logsearch-create-task-group-request'; export * from './logsearch-preview-model'; export * from './logsearch-search-log-request'; export * from './logsearch-task-group-model'; export * from './logsearch-task-group-response'; export * from './logsearch-task-model'; export * from './matrix-matrix'; export * from './metrics-get-prom-address-config-response'; export * from './metrics-put-custom-prom-address-request'; export * from './metrics-put-custom-prom-address-response'; export * from './metrics-query-response'; export * from './model-request-target-node'; export * from './model-request-target-statistics'; export * from './profiling-group-detail-response'; export * from './profiling-start-request'; export * from './profiling-task-group-model'; export * from './profiling-task-model'; export * from './queryeditor-run-request'; export * from './queryeditor-run-response'; export * from './resourcemanager-calibrate-response'; export * from './resourcemanager-get-config-response'; export * from './resourcemanager-resource-info-row-def'; export * from './rest-error-response'; export * from './slowquery-get-list-request'; export * from './slowquery-model'; export * from './sso-create-impersonation-request'; export * from './sso-ssoimpersonation-model'; export * from './sso-set-config-request'; export * from './statement-binding'; export * from './statement-editable-config'; export * from './statement-get-statements-request'; export * from './statement-model'; export * from './topology-alert-manager-info'; export * from './topology-grafana-info'; export * from './topology-pdinfo'; export * from './topology-scheduling-info'; export * from './topology-store-info'; export * from './topology-store-labels'; export * from './topology-store-location'; export * from './topology-tsoinfo'; export * from './topology-ti-cdcinfo'; export * from './topology-ti-dbinfo'; export * from './topology-ti-proxy-info'; export * from './topsql-editable-config'; export * from './topsql-instance-item'; export * from './topsql-instance-response'; export * from './topsql-summary-by-item'; export * from './topsql-summary-item'; export * from './topsql-summary-plan-item'; export * from './topsql-summary-response'; export * from './topsql-tikv-network-io-collection-config'; export * from './topsql-update-tikv-network-io-collection-response'; export * from './user-authenticate-form'; export * from './user-get-login-info-response'; export * from './user-sign-out-info'; export * from './user-token-response'; export * from './version-info'; ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/info-info-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { VersionInfo } from './version-info'; /** * * @export * @interface InfoInfoResponse */ export interface InfoInfoResponse { /** * * @type {boolean} * @memberof InfoInfoResponse */ 'enable_experimental'?: boolean; /** * * @type {boolean} * @memberof InfoInfoResponse */ 'enable_telemetry'?: boolean; /** * * @type {string} * @memberof InfoInfoResponse */ 'ngm_state'?: string; /** * * @type {Array} * @memberof InfoInfoResponse */ 'supported_features'?: Array; /** * * @type {VersionInfo} * @memberof InfoInfoResponse */ 'version'?: VersionInfo; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/info-table-schema.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface InfoTableSchema */ export interface InfoTableSchema { /** * * @type {string} * @memberof InfoTableSchema */ 'table_id'?: string; /** * * @type {string} * @memberof InfoTableSchema */ 'table_name'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/info-who-am-iresponse.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface InfoWhoAmIResponse */ export interface InfoWhoAmIResponse { /** * * @type {string} * @memberof InfoWhoAmIResponse */ 'display_name'?: string; /** * * @type {boolean} * @memberof InfoWhoAmIResponse */ 'is_shareable'?: boolean; /** * * @type {boolean} * @memberof InfoWhoAmIResponse */ 'is_writeable'?: boolean; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/logsearch-create-task-group-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { LogsearchSearchLogRequest } from './logsearch-search-log-request'; import { ModelRequestTargetNode } from './model-request-target-node'; /** * * @export * @interface LogsearchCreateTaskGroupRequest */ export interface LogsearchCreateTaskGroupRequest { /** * * @type {LogsearchSearchLogRequest} * @memberof LogsearchCreateTaskGroupRequest */ 'request': LogsearchSearchLogRequest; /** * * @type {Array} * @memberof LogsearchCreateTaskGroupRequest */ 'targets': Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/logsearch-preview-model.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface LogsearchPreviewModel */ export interface LogsearchPreviewModel { /** * * @type {number} * @memberof LogsearchPreviewModel */ 'id'?: number; /** * * @type {number} * @memberof LogsearchPreviewModel */ 'level'?: number; /** * * @type {string} * @memberof LogsearchPreviewModel */ 'message'?: string; /** * * @type {number} * @memberof LogsearchPreviewModel */ 'task_group_id'?: number; /** * * @type {number} * @memberof LogsearchPreviewModel */ 'task_id'?: number; /** * * @type {number} * @memberof LogsearchPreviewModel */ 'time'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/logsearch-search-log-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface LogsearchSearchLogRequest */ export interface LogsearchSearchLogRequest { /** * * @type {number} * @memberof LogsearchSearchLogRequest */ 'end_time'?: number; /** * * @type {number} * @memberof LogsearchSearchLogRequest */ 'min_level'?: number; /** * 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) * @type {Array} * @memberof LogsearchSearchLogRequest */ 'patterns'?: Array; /** * * @type {number} * @memberof LogsearchSearchLogRequest */ 'start_time'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/logsearch-task-group-model.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { LogsearchSearchLogRequest } from './logsearch-search-log-request'; import { ModelRequestTargetStatistics } from './model-request-target-statistics'; /** * * @export * @interface LogsearchTaskGroupModel */ export interface LogsearchTaskGroupModel { /** * * @type {number} * @memberof LogsearchTaskGroupModel */ 'id'?: number; /** * * @type {string} * @memberof LogsearchTaskGroupModel */ 'log_store_dir'?: string; /** * * @type {LogsearchSearchLogRequest} * @memberof LogsearchTaskGroupModel */ 'search_request'?: LogsearchSearchLogRequest; /** * * @type {number} * @memberof LogsearchTaskGroupModel */ 'state'?: number; /** * * @type {ModelRequestTargetStatistics} * @memberof LogsearchTaskGroupModel */ 'target_stats'?: ModelRequestTargetStatistics; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/logsearch-task-group-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { LogsearchTaskGroupModel } from './logsearch-task-group-model'; import { LogsearchTaskModel } from './logsearch-task-model'; /** * * @export * @interface LogsearchTaskGroupResponse */ export interface LogsearchTaskGroupResponse { /** * * @type {LogsearchTaskGroupModel} * @memberof LogsearchTaskGroupResponse */ 'task_group'?: LogsearchTaskGroupModel; /** * * @type {Array} * @memberof LogsearchTaskGroupResponse */ 'tasks'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/logsearch-task-model.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ModelRequestTargetNode } from './model-request-target-node'; /** * * @export * @interface LogsearchTaskModel */ export interface LogsearchTaskModel { /** * * @type {string} * @memberof LogsearchTaskModel */ 'error'?: string; /** * * @type {number} * @memberof LogsearchTaskModel */ 'id'?: number; /** * * @type {string} * @memberof LogsearchTaskModel */ 'log_store_path'?: string; /** * * @type {number} * @memberof LogsearchTaskModel */ 'size'?: number; /** * * @type {string} * @memberof LogsearchTaskModel */ 'slow_log_store_path'?: string; /** * * @type {number} * @memberof LogsearchTaskModel */ 'state'?: number; /** * * @type {ModelRequestTargetNode} * @memberof LogsearchTaskModel */ 'target'?: ModelRequestTargetNode; /** * * @type {number} * @memberof LogsearchTaskModel */ 'task_group_id'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/matrix-matrix.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { DecoratorLabelKey } from './decorator-label-key'; /** * * @export * @interface MatrixMatrix */ export interface MatrixMatrix { /** * * @type {{ [key: string]: Array>; }} * @memberof MatrixMatrix */ 'data': { [key: string]: Array>; }; /** * * @type {Array} * @memberof MatrixMatrix */ 'keyAxis': Array; /** * * @type {Array} * @memberof MatrixMatrix */ 'timeAxis': Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/metrics-get-prom-address-config-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface MetricsGetPromAddressConfigResponse */ export interface MetricsGetPromAddressConfigResponse { /** * * @type {string} * @memberof MetricsGetPromAddressConfigResponse */ 'customized_addr'?: string; /** * * @type {string} * @memberof MetricsGetPromAddressConfigResponse */ 'deployed_addr'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/metrics-put-custom-prom-address-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface MetricsPutCustomPromAddressRequest */ export interface MetricsPutCustomPromAddressRequest { /** * * @type {string} * @memberof MetricsPutCustomPromAddressRequest */ 'address'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/metrics-put-custom-prom-address-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface MetricsPutCustomPromAddressResponse */ export interface MetricsPutCustomPromAddressResponse { /** * * @type {string} * @memberof MetricsPutCustomPromAddressResponse */ 'normalized_address'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/metrics-query-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface MetricsQueryResponse */ export interface MetricsQueryResponse { /** * * @type {object} * @memberof MetricsQueryResponse */ 'data'?: object; /** * * @type {string} * @memberof MetricsQueryResponse */ 'status'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/model-request-target-node.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ModelRequestTargetNode */ export interface ModelRequestTargetNode { /** * * @type {string} * @memberof ModelRequestTargetNode */ 'display_name'?: string; /** * * @type {string} * @memberof ModelRequestTargetNode */ 'ip'?: string; /** * * @type {string} * @memberof ModelRequestTargetNode */ 'kind'?: string; /** * * @type {number} * @memberof ModelRequestTargetNode */ 'port'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/model-request-target-statistics.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ModelRequestTargetStatistics */ export interface ModelRequestTargetStatistics { /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_pd_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_scheduling_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_ticdc_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_tidb_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_tiflash_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_tikv_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_tiproxy_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_tso_nodes'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/profiling-group-detail-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ProfilingTaskGroupModel } from './profiling-task-group-model'; import { ProfilingTaskModel } from './profiling-task-model'; /** * * @export * @interface ProfilingGroupDetailResponse */ export interface ProfilingGroupDetailResponse { /** * * @type {number} * @memberof ProfilingGroupDetailResponse */ 'server_time'?: number; /** * * @type {ProfilingTaskGroupModel} * @memberof ProfilingGroupDetailResponse */ 'task_group_status'?: ProfilingTaskGroupModel; /** * * @type {Array} * @memberof ProfilingGroupDetailResponse */ 'tasks_status'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/profiling-start-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ModelRequestTargetNode } from './model-request-target-node'; /** * * @export * @interface ProfilingStartRequest */ export interface ProfilingStartRequest { /** * * @type {number} * @memberof ProfilingStartRequest */ 'duration_secs'?: number; /** * * @type {Array} * @memberof ProfilingStartRequest */ 'requsted_profiling_types'?: Array; /** * * @type {Array} * @memberof ProfilingStartRequest */ 'targets'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/profiling-task-group-model.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ModelRequestTargetStatistics } from './model-request-target-statistics'; /** * * @export * @interface ProfilingTaskGroupModel */ export interface ProfilingTaskGroupModel { /** * * @type {number} * @memberof ProfilingTaskGroupModel */ 'id'?: number; /** * * @type {number} * @memberof ProfilingTaskGroupModel */ 'profile_duration_secs'?: number; /** * * @type {Array} * @memberof ProfilingTaskGroupModel */ 'requsted_profiling_types'?: Array; /** * * @type {number} * @memberof ProfilingTaskGroupModel */ 'started_at'?: number; /** * * @type {number} * @memberof ProfilingTaskGroupModel */ 'state'?: number; /** * * @type {ModelRequestTargetStatistics} * @memberof ProfilingTaskGroupModel */ 'target_stats'?: ModelRequestTargetStatistics; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/profiling-task-model.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ModelRequestTargetNode } from './model-request-target-node'; /** * * @export * @interface ProfilingTaskModel */ export interface ProfilingTaskModel { /** * * @type {string} * @memberof ProfilingTaskModel */ 'error'?: string; /** * * @type {number} * @memberof ProfilingTaskModel */ 'id'?: number; /** * * @type {string} * @memberof ProfilingTaskModel */ 'profiling_type'?: string; /** * * @type {string} * @memberof ProfilingTaskModel */ 'raw_data_type'?: string; /** * The start running time, reset when retry. Used to estimate approximate profiling progress. * @type {number} * @memberof ProfilingTaskModel */ 'started_at'?: number; /** * * @type {number} * @memberof ProfilingTaskModel */ 'state'?: number; /** * * @type {ModelRequestTargetNode} * @memberof ProfilingTaskModel */ 'target'?: ModelRequestTargetNode; /** * * @type {number} * @memberof ProfilingTaskModel */ 'task_group_id'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/queryeditor-run-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface QueryeditorRunRequest */ export interface QueryeditorRunRequest { /** * * @type {number} * @memberof QueryeditorRunRequest */ 'max_rows'?: number; /** * * @type {string} * @memberof QueryeditorRunRequest */ 'statements'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/queryeditor-run-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface QueryeditorRunResponse */ export interface QueryeditorRunResponse { /** * * @type {number} * @memberof QueryeditorRunResponse */ 'actual_rows'?: number; /** * * @type {Array} * @memberof QueryeditorRunResponse */ 'column_names'?: Array; /** * * @type {string} * @memberof QueryeditorRunResponse */ 'error_msg'?: string; /** * * @type {number} * @memberof QueryeditorRunResponse */ 'execution_ms'?: number; /** * * @type {Array>} * @memberof QueryeditorRunResponse */ 'rows'?: Array>; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-calibrate-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ResourcemanagerCalibrateResponse */ export interface ResourcemanagerCalibrateResponse { /** * * @type {number} * @memberof ResourcemanagerCalibrateResponse */ 'estimated_capacity'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-get-config-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ResourcemanagerGetConfigResponse */ export interface ResourcemanagerGetConfigResponse { /** * * @type {boolean} * @memberof ResourcemanagerGetConfigResponse */ 'enable'?: boolean; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-resource-info-row-def.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface ResourcemanagerResourceInfoRowDef */ export interface ResourcemanagerResourceInfoRowDef { /** * * @type {string} * @memberof ResourcemanagerResourceInfoRowDef */ 'burstable'?: string; /** * * @type {string} * @memberof ResourcemanagerResourceInfoRowDef */ 'name'?: string; /** * * @type {string} * @memberof ResourcemanagerResourceInfoRowDef */ 'priority'?: string; /** * * @type {string} * @memberof ResourcemanagerResourceInfoRowDef */ 'ru_per_sec'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/rest-error-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface RestErrorResponse */ export interface RestErrorResponse { /** * * @type {string} * @memberof RestErrorResponse */ 'code'?: string; /** * * @type {boolean} * @memberof RestErrorResponse */ 'error'?: boolean; /** * * @type {string} * @memberof RestErrorResponse */ 'full_text'?: string; /** * * @type {string} * @memberof RestErrorResponse */ 'message'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/slowquery-get-list-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface SlowqueryGetListRequest */ export interface SlowqueryGetListRequest { /** * * @type {number} * @memberof SlowqueryGetListRequest */ 'begin_time'?: number; /** * * @type {Array} * @memberof SlowqueryGetListRequest */ 'db'?: Array; /** * * @type {boolean} * @memberof SlowqueryGetListRequest */ 'desc'?: boolean; /** * * @type {string} * @memberof SlowqueryGetListRequest */ 'digest'?: string; /** * * @type {number} * @memberof SlowqueryGetListRequest */ 'end_time'?: number; /** * example: \"Query,Digest\" * @type {string} * @memberof SlowqueryGetListRequest */ 'fields'?: string; /** * * @type {number} * @memberof SlowqueryGetListRequest */ 'limit'?: number; /** * * @type {string} * @memberof SlowqueryGetListRequest */ 'orderBy'?: string; /** * for showing slow queries in the statement detail page * @type {Array} * @memberof SlowqueryGetListRequest */ 'plans'?: Array; /** * * @type {Array} * @memberof SlowqueryGetListRequest */ 'resource_group'?: Array; /** * * @type {string} * @memberof SlowqueryGetListRequest */ 'text'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/slowquery-model.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface SlowqueryModel */ export interface SlowqueryModel { /** * * @type {number} * @memberof SlowqueryModel */ 'backoff_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'backoff_types'?: string; /** * * @type {string} * @memberof SlowqueryModel */ 'binary_plan'?: string; /** * Computed fields * @type {string} * @memberof SlowqueryModel */ 'binary_plan_json'?: string; /** * binary plan plain text * @type {string} * @memberof SlowqueryModel */ 'binary_plan_text'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'commit_backoff_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'commit_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'compile_time'?: number; /** * TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. * @type {string} * @memberof SlowqueryModel */ 'connection_id'?: string; /** * * @type {string} * @memberof SlowqueryModel */ 'cop_proc_addr'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_proc_avg'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_proc_max'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_proc_p90'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'cop_wait_addr'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_wait_avg'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_wait_max'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_wait_p90'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'db'?: string; /** * * @type {string} * @memberof SlowqueryModel */ 'digest'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'disk_max'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'exec_retry_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'get_commit_ts_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'host'?: string; /** * IA remote read * @type {number} * @memberof SlowqueryModel */ 'ia_remote_read_segment_size'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'ia_remote_read_segment_wait_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'index_names'?: string; /** * * @type {string} * @memberof SlowqueryModel */ 'instance'?: string; /** * Basic * @type {number} * @memberof SlowqueryModel */ 'is_internal'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'local_latch_wait_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'lock_keys_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'mem_arbitration'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'memory_max'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'optimize_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'parse_time'?: number; /** * deprecated, replaced by BinaryPlanText * @type {string} * @memberof SlowqueryModel */ 'plan'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'plan_from_binding'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'plan_from_cache'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'prepared'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'preproc_subqueries_time'?: number; /** * Detail * @type {string} * @memberof SlowqueryModel */ 'prev_stmt'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'prewrite_region'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'prewrite_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'process_keys'?: number; /** * Time * @type {number} * @memberof SlowqueryModel */ 'process_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'query'?: string; /** * latency * @type {number} * @memberof SlowqueryModel */ 'query_time'?: number; /** * Coprocessor * @type {number} * @memberof SlowqueryModel */ 'request_count'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'resolve_lock_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'resource_group'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'rewrite_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'rocksdb_block_cache_hit_count'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'rocksdb_block_read_byte'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'rocksdb_block_read_count'?: number; /** * RocksDB * @type {number} * @memberof SlowqueryModel */ 'rocksdb_delete_skipped_count'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'rocksdb_key_skipped_count'?: number; /** * Resource Control * @type {number} * @memberof SlowqueryModel */ 'ru'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'stats'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'success'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'time_queued_by_rc'?: number; /** * finish time * @type {number} * @memberof SlowqueryModel */ 'timestamp'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'total_keys'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'txn_retry'?: number; /** * TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. * @type {string} * @memberof SlowqueryModel */ 'txn_start_ts'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_received_tiflash_cross_zone'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_received_tiflash_total'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_received_tikv_cross_zone'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_received_tikv_total'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_sent_tiflash_cross_zone'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_sent_tiflash_total'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_sent_tikv_cross_zone'?: number; /** * Network fields * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_sent_tikv_total'?: number; /** * Connection * @type {string} * @memberof SlowqueryModel */ 'user'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'wait_prewrite_binlog_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'wait_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'wait_ts'?: number; /** * * @type {Array} * @memberof SlowqueryModel */ 'warnings'?: Array; /** * Transaction * @type {number} * @memberof SlowqueryModel */ 'write_keys'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'write_size'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'write_sql_response_total'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/sso-create-impersonation-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface SsoCreateImpersonationRequest */ export interface SsoCreateImpersonationRequest { /** * * @type {string} * @memberof SsoCreateImpersonationRequest */ 'password'?: string; /** * * @type {string} * @memberof SsoCreateImpersonationRequest */ 'sql_user'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/sso-set-config-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { ConfigSSOCoreConfig } from './config-ssocore-config'; /** * * @export * @interface SsoSetConfigRequest */ export interface SsoSetConfigRequest { /** * * @type {ConfigSSOCoreConfig} * @memberof SsoSetConfigRequest */ 'config'?: ConfigSSOCoreConfig; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/sso-ssoimpersonation-model.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface SsoSSOImpersonationModel */ export interface SsoSSOImpersonationModel { /** * * @type {string} * @memberof SsoSSOImpersonationModel */ 'last_impersonate_status'?: string; /** * * @type {string} * @memberof SsoSSOImpersonationModel */ 'sql_user'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/statement-binding.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface StatementBinding */ export interface StatementBinding { /** * * @type {string} * @memberof StatementBinding */ 'plan_digest'?: string; /** * * @type {string} * @memberof StatementBinding */ 'source'?: StatementBindingSourceEnum; /** * * @type {string} * @memberof StatementBinding */ 'status'?: StatementBindingStatusEnum; } export const StatementBindingSourceEnum = { manual: 'manual', history: 'history', capture: 'capture', evolve: 'evolve' } as const; export type StatementBindingSourceEnum = typeof StatementBindingSourceEnum[keyof typeof StatementBindingSourceEnum]; export const StatementBindingStatusEnum = { enabled: 'enabled', using: 'using', disabled: 'disabled', deleted: 'deleted', invalid: 'invalid', rejected: 'rejected', pending_verify: 'pending verify' } as const; export type StatementBindingStatusEnum = typeof StatementBindingStatusEnum[keyof typeof StatementBindingStatusEnum]; ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/statement-editable-config.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface StatementEditableConfig */ export interface StatementEditableConfig { /** * * @type {boolean} * @memberof StatementEditableConfig */ 'enable'?: boolean; /** * * @type {number} * @memberof StatementEditableConfig */ 'history_size'?: number; /** * * @type {boolean} * @memberof StatementEditableConfig */ 'internal_query'?: boolean; /** * * @type {number} * @memberof StatementEditableConfig */ 'max_size'?: number; /** * * @type {number} * @memberof StatementEditableConfig */ 'refresh_interval'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/statement-get-statements-request.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface StatementGetStatementsRequest */ export interface StatementGetStatementsRequest { /** * * @type {number} * @memberof StatementGetStatementsRequest */ 'begin_time'?: number; /** * * @type {number} * @memberof StatementGetStatementsRequest */ 'end_time'?: number; /** * * @type {string} * @memberof StatementGetStatementsRequest */ 'fields'?: string; /** * * @type {Array} * @memberof StatementGetStatementsRequest */ 'resource_groups'?: Array; /** * * @type {Array} * @memberof StatementGetStatementsRequest */ 'schemas'?: Array; /** * * @type {Array} * @memberof StatementGetStatementsRequest */ 'stmt_types'?: Array; /** * * @type {string} * @memberof StatementGetStatementsRequest */ 'text'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/statement-model.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface StatementModel */ export interface StatementModel { /** * * @type {number} * @memberof StatementModel */ 'avg_affected_rows'?: number; /** * avg total back off time per sql * @type {number} * @memberof StatementModel */ 'avg_backoff_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_commit_backoff_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_commit_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_compile_latency'?: number; /** * avg process time per copr task * @type {number} * @memberof StatementModel */ 'avg_cop_process_time'?: number; /** * avg wait time per copr task * @type {number} * @memberof StatementModel */ 'avg_cop_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_disk'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_get_commit_ts_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_ia_read_segment_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_ia_remote_read_segment_size'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_ia_remote_read_segment_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_latency'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_local_latch_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_mem'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_mem_arbitration'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_parse_latency'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_prewrite_regions'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_prewrite_time'?: number; /** * avg total process time per sql * @type {number} * @memberof StatementModel */ 'avg_process_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_processed_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_resolve_lock_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_rocksdb_block_cache_hit_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_rocksdb_block_read_byte'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_rocksdb_block_read_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_rocksdb_delete_skipped_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_rocksdb_key_skipped_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_ru'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_time_queued_by_rc'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_total_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_txn_retry'?: number; /** * avg total wait time per sql * @type {number} * @memberof StatementModel */ 'avg_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_write_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_write_size'?: number; /** * * @type {string} * @memberof StatementModel */ 'binary_plan'?: string; /** * * @type {string} * @memberof StatementModel */ 'binary_plan_json'?: string; /** * * @type {string} * @memberof StatementModel */ 'binary_plan_text'?: string; /** * * @type {string} * @memberof StatementModel */ 'digest'?: string; /** * * @type {string} * @memberof StatementModel */ 'digest_text'?: string; /** * * @type {number} * @memberof StatementModel */ 'exec_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'first_seen'?: number; /** * * @type {string} * @memberof StatementModel */ 'index_names'?: string; /** * * @type {number} * @memberof StatementModel */ 'last_seen'?: number; /** * max back off time per sql * @type {number} * @memberof StatementModel */ 'max_backoff_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_commit_backoff_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_commit_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_compile_latency'?: number; /** * max process time per copr task * @type {number} * @memberof StatementModel */ 'max_cop_process_time'?: number; /** * max wait time per copr task * @type {number} * @memberof StatementModel */ 'max_cop_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_disk'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_get_commit_ts_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_ia_read_segment_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_ia_remote_read_segment_size'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_ia_remote_read_segment_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_latency'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_local_latch_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_mem'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_mem_arbitration'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_parse_latency'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_prewrite_regions'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_prewrite_time'?: number; /** * max process time per sql * @type {number} * @memberof StatementModel */ 'max_process_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_processed_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_resolve_lock_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_rocksdb_block_cache_hit_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_rocksdb_block_read_byte'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_rocksdb_block_read_count'?: number; /** * RocksDB * @type {number} * @memberof StatementModel */ 'max_rocksdb_delete_skipped_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_rocksdb_key_skipped_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_ru'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_time_queued_by_rc'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_total_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_txn_retry'?: number; /** * max wait time per sql * @type {number} * @memberof StatementModel */ 'max_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_write_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_write_size'?: number; /** * * @type {number} * @memberof StatementModel */ 'min_latency'?: number; /** * deprecated, replaced by BinaryPlanText * @type {string} * @memberof StatementModel */ 'plan'?: string; /** * * @type {number} * @memberof StatementModel */ 'plan_cache_hits'?: number; /** * * @type {boolean} * @memberof StatementModel */ 'plan_can_be_bound'?: boolean; /** * * @type {number} * @memberof StatementModel */ 'plan_count'?: number; /** * * @type {string} * @memberof StatementModel */ 'plan_digest'?: string; /** * * @type {string} * @memberof StatementModel */ 'plan_hint'?: string; /** * * @type {string} * @memberof StatementModel */ 'prev_sample_text'?: string; /** * * @type {string} * @memberof StatementModel */ 'query_sample_text'?: string; /** * Computed fields * @type {string} * @memberof StatementModel */ 'related_schemas'?: string; /** * Resource Control * @type {string} * @memberof StatementModel */ 'resource_group'?: string; /** * * @type {string} * @memberof StatementModel */ 'sample_user'?: string; /** * * @type {string} * @memberof StatementModel */ 'schema_name'?: string; /** * * @type {string} * @memberof StatementModel */ 'stmt_type'?: string; /** * * @type {number} * @memberof StatementModel */ 'sum_backoff_times'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_cop_task_num'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_errors'?: number; /** * IA remote read segment metrics * @type {number} * @memberof StatementModel */ 'sum_ia_read_segment_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_ia_remote_read_segment_size'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_ia_remote_read_segment_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_latency'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_ru'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_received_tiflash_cross_zone'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_received_tiflash_total'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_received_tikv_cross_zone'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_received_tikv_total'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_sent_tiflash_cross_zone'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_sent_tiflash_total'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_sent_tikv_cross_zone'?: number; /** * Network Fields * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_sent_tikv_total'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_warnings'?: number; /** * * @type {number} * @memberof StatementModel */ 'summary_begin_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'summary_end_time'?: number; /** * * @type {string} * @memberof StatementModel */ 'table_names'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/statement-time-range.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface StatementTimeRange */ export interface StatementTimeRange { /** * * @type {number} * @memberof StatementTimeRange */ begin_time?: number /** * * @type {number} * @memberof StatementTimeRange */ end_time?: number } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-alert-manager-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopologyAlertManagerInfo */ export interface TopologyAlertManagerInfo { /** * * @type {string} * @memberof TopologyAlertManagerInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyAlertManagerInfo */ 'port'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-grafana-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopologyGrafanaInfo */ export interface TopologyGrafanaInfo { /** * * @type {string} * @memberof TopologyGrafanaInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyGrafanaInfo */ 'port'?: number; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-pdinfo.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopologyPDInfo */ export interface TopologyPDInfo { /** * * @type {string} * @memberof TopologyPDInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyPDInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyPDInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyPDInfo */ 'port'?: number; /** * Ts = 0 means unknown * @type {number} * @memberof TopologyPDInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyPDInfo */ 'status'?: number; /** * * @type {string} * @memberof TopologyPDInfo */ 'version'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-scheduling-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopologySchedulingInfo */ export interface TopologySchedulingInfo { /** * * @type {string} * @memberof TopologySchedulingInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologySchedulingInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologySchedulingInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologySchedulingInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologySchedulingInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologySchedulingInfo */ 'status'?: number; /** * * @type {string} * @memberof TopologySchedulingInfo */ 'version'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-store-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopologyStoreInfo */ export interface TopologyStoreInfo { /** * * @type {string} * @memberof TopologyStoreInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyStoreInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyStoreInfo */ 'ip'?: string; /** * * @type {{ [key: string]: string; }} * @memberof TopologyStoreInfo */ 'labels'?: { [key: string]: string; }; /** * * @type {number} * @memberof TopologyStoreInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologyStoreInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyStoreInfo */ 'status'?: number; /** * * @type {number} * @memberof TopologyStoreInfo */ 'status_port'?: number; /** * * @type {string} * @memberof TopologyStoreInfo */ 'version'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-store-labels.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopologyStoreLabels */ export interface TopologyStoreLabels { /** * * @type {string} * @memberof TopologyStoreLabels */ 'address'?: string; /** * * @type {{ [key: string]: string; }} * @memberof TopologyStoreLabels */ 'labels'?: { [key: string]: string; }; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-store-location.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { TopologyStoreLabels } from './topology-store-labels'; /** * * @export * @interface TopologyStoreLocation */ export interface TopologyStoreLocation { /** * * @type {Array} * @memberof TopologyStoreLocation */ 'location_labels'?: Array; /** * * @type {Array} * @memberof TopologyStoreLocation */ 'stores'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-ti-cdcinfo.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopologyTiCDCInfo */ export interface TopologyTiCDCInfo { /** * * @type {string} * @memberof TopologyTiCDCInfo */ 'cluster_name'?: string; /** * * @type {string} * @memberof TopologyTiCDCInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyTiCDCInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyTiCDCInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyTiCDCInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologyTiCDCInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyTiCDCInfo */ 'status'?: number; /** * * @type {number} * @memberof TopologyTiCDCInfo */ 'status_port'?: number; /** * * @type {string} * @memberof TopologyTiCDCInfo */ 'version'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-ti-dbinfo.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopologyTiDBInfo */ export interface TopologyTiDBInfo { /** * * @type {string} * @memberof TopologyTiDBInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyTiDBInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyTiDBInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyTiDBInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologyTiDBInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyTiDBInfo */ 'status'?: number; /** * * @type {number} * @memberof TopologyTiDBInfo */ 'status_port'?: number; /** * * @type {string} * @memberof TopologyTiDBInfo */ 'version'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-ti-proxy-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopologyTiProxyInfo */ export interface TopologyTiProxyInfo { /** * * @type {string} * @memberof TopologyTiProxyInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyTiProxyInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyTiProxyInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyTiProxyInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologyTiProxyInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyTiProxyInfo */ 'status'?: number; /** * * @type {number} * @memberof TopologyTiProxyInfo */ 'status_port'?: number; /** * * @type {string} * @memberof TopologyTiProxyInfo */ 'version'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topology-tsoinfo.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopologyTSOInfo */ export interface TopologyTSOInfo { /** * * @type {string} * @memberof TopologyTSOInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyTSOInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyTSOInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyTSOInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologyTSOInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyTSOInfo */ 'status'?: number; /** * * @type {string} * @memberof TopologyTSOInfo */ 'version'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topsql-editable-config.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopsqlEditableConfig */ export interface TopsqlEditableConfig { /** * * @type {boolean} * @memberof TopsqlEditableConfig */ 'enable'?: boolean; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topsql-instance-item.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopsqlInstanceItem */ export interface TopsqlInstanceItem { /** * * @type {string} * @memberof TopsqlInstanceItem */ 'instance'?: string; /** * * @type {string} * @memberof TopsqlInstanceItem */ 'instance_type'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topsql-instance-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { TopsqlInstanceItem } from './topsql-instance-item'; /** * * @export * @interface TopsqlInstanceResponse */ export interface TopsqlInstanceResponse { /** * * @type {Array} * @memberof TopsqlInstanceResponse */ 'data'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-by-item.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopsqlSummaryByItem */ export interface TopsqlSummaryByItem { /** * * @type {Array} * @memberof TopsqlSummaryByItem */ 'cpu_time_ms'?: Array; /** * * @type {number} * @memberof TopsqlSummaryByItem */ 'cpu_time_ms_sum'?: number; /** * * @type {Array} * @memberof TopsqlSummaryByItem */ 'logical_io_bytes'?: Array; /** * * @type {number} * @memberof TopsqlSummaryByItem */ 'logical_io_bytes_sum'?: number; /** * * @type {Array} * @memberof TopsqlSummaryByItem */ 'network_bytes'?: Array; /** * * @type {number} * @memberof TopsqlSummaryByItem */ 'network_bytes_sum'?: number; /** * * @type {string} * @memberof TopsqlSummaryByItem */ 'text'?: string; /** * * @type {Array} * @memberof TopsqlSummaryByItem */ 'timestamp_sec'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-item.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { TopsqlSummaryPlanItem } from './topsql-summary-plan-item'; /** * * @export * @interface TopsqlSummaryItem */ export interface TopsqlSummaryItem { /** * * @type {number} * @memberof TopsqlSummaryItem */ 'cpu_time_ms'?: number; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'duration_per_exec_ms'?: number; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'exec_count_per_sec'?: number; /** * * @type {boolean} * @memberof TopsqlSummaryItem */ 'is_other'?: boolean; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'logical_io_bytes'?: number; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'network_bytes'?: number; /** * * @type {Array} * @memberof TopsqlSummaryItem */ 'plans'?: Array; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'scan_indexes_per_sec'?: number; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'scan_records_per_sec'?: number; /** * * @type {string} * @memberof TopsqlSummaryItem */ 'sql_digest'?: string; /** * * @type {string} * @memberof TopsqlSummaryItem */ 'sql_text'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-plan-item.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopsqlSummaryPlanItem */ export interface TopsqlSummaryPlanItem { /** * * @type {Array} * @memberof TopsqlSummaryPlanItem */ 'cpu_time_ms'?: Array; /** * * @type {number} * @memberof TopsqlSummaryPlanItem */ 'duration_per_exec_ms'?: number; /** * * @type {number} * @memberof TopsqlSummaryPlanItem */ 'exec_count_per_sec'?: number; /** * * @type {Array} * @memberof TopsqlSummaryPlanItem */ 'logical_io_bytes'?: Array; /** * * @type {Array} * @memberof TopsqlSummaryPlanItem */ 'network_bytes'?: Array; /** * * @type {string} * @memberof TopsqlSummaryPlanItem */ 'plan_digest'?: string; /** * * @type {string} * @memberof TopsqlSummaryPlanItem */ 'plan_text'?: string; /** * * @type {number} * @memberof TopsqlSummaryPlanItem */ 'scan_indexes_per_sec'?: number; /** * * @type {number} * @memberof TopsqlSummaryPlanItem */ 'scan_records_per_sec'?: number; /** * * @type {Array} * @memberof TopsqlSummaryPlanItem */ 'timestamp_sec'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { TopsqlSummaryByItem } from './topsql-summary-by-item'; import { TopsqlSummaryItem } from './topsql-summary-item'; /** * * @export * @interface TopsqlSummaryResponse */ export interface TopsqlSummaryResponse { /** * * @type {Array} * @memberof TopsqlSummaryResponse */ 'data'?: Array; /** * * @type {Array} * @memberof TopsqlSummaryResponse */ 'data_by'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topsql-tikv-network-io-collection-config.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface TopsqlTikvNetworkIoCollectionConfig */ export interface TopsqlTikvNetworkIoCollectionConfig { /** * * @type {boolean} * @memberof TopsqlTikvNetworkIoCollectionConfig */ 'enable'?: boolean; /** * * @type {boolean} * @memberof TopsqlTikvNetworkIoCollectionConfig */ 'is_multi_value'?: boolean; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/topsql-update-tikv-network-io-collection-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ import { RestErrorResponse } from './rest-error-response'; /** * * @export * @interface TopsqlUpdateTikvNetworkIoCollectionResponse */ export interface TopsqlUpdateTikvNetworkIoCollectionResponse { /** * * @type {Array} * @memberof TopsqlUpdateTikvNetworkIoCollectionResponse */ 'warnings'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/user-authenticate-form.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface UserAuthenticateForm */ export interface UserAuthenticateForm { /** * FIXME: Use strong type * @type {string} * @memberof UserAuthenticateForm */ 'extra'?: string; /** * * @type {string} * @memberof UserAuthenticateForm */ 'password'?: string; /** * * @type {number} * @memberof UserAuthenticateForm */ 'type'?: number; /** * Does not present for AuthTypeSharingCode * @type {string} * @memberof UserAuthenticateForm */ 'username'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/user-get-login-info-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface UserGetLoginInfoResponse */ export interface UserGetLoginInfoResponse { /** * * @type {string} * @memberof UserGetLoginInfoResponse */ 'sql_auth_public_key'?: string; /** * * @type {Array} * @memberof UserGetLoginInfoResponse */ 'supported_auth_types'?: Array; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/user-sign-out-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface UserSignOutInfo */ export interface UserSignOutInfo { /** * * @type {string} * @memberof UserSignOutInfo */ 'end_session_url'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/user-token-response.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface UserTokenResponse */ export interface UserTokenResponse { /** * * @type {string} * @memberof UserTokenResponse */ 'expire'?: string; /** * * @type {string} * @memberof UserTokenResponse */ 'token'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/src/client/api/models/version-info.ts ================================================ /* tslint:disable */ /* eslint-disable */ /** * Dashboard API * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ /** * * @export * @interface VersionInfo */ export interface VersionInfo { /** * * @type {string} * @memberof VersionInfo */ 'build_git_hash'?: string; /** * * @type {string} * @memberof VersionInfo */ 'build_time'?: string; /** * * @type {string} * @memberof VersionInfo */ 'internal_version'?: string; /** * * @type {string} * @memberof VersionInfo */ 'pd_version'?: string; /** * * @type {string} * @memberof VersionInfo */ 'standalone'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-client/swagger/.openapi_config.yaml ================================================ # https://openapi-generator.tech/docs/generators/typescript-axios#config-options enumPropertyNaming: original modelPropertyNaming: original supportsES6: true # Setting this property to true will generate functions with a single argument containing all API endpoint parameters instead of one argument per parameter. useSingleRequestParameter: true # conflicts with `useSingleRequestParameter` # withInterfaces: true # Put the model and api in separate folders and in separate classes # https://github.com/OpenAPITools/openapi-generator/issues/5008#issuecomment-613791804 withSeparateModelsAndApi: true apiPackage: api modelPackage: models ================================================ FILE: ui/packages/tidb-dashboard-client/swagger/gen_api.sh ================================================ #!/usr/bin/env bash set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" PROJECT_DIR="$(dirname "$DIR")" SPEC_SOURCE_PATH=$PROJECT_DIR/../../../swaggerspec/swagger.json SPEC_TARGET_PATH=$PROJECT_DIR/swagger/spec.json OPENAPI_CONFIG_DIR=$PROJECT_DIR/swagger/.openapi_config.yaml OUTPUT_DIR=$PROJECT_DIR/src/client/api OUTPUT_MODELS_DIR=$PROJECT_DIR/src/client/api/models LIB_MODELS_DIR=$PROJECT_DIR/../tidb-dashboard-lib/src/client/ cd $PROJECT_DIR/swagger # copy spec if spec source exists if [ -f "$SPEC_SOURCE_PATH" ]; then cp $SPEC_SOURCE_PATH $SPEC_TARGET_PATH fi # gen api pnpm openapi-generator-cli generate -i $SPEC_TARGET_PATH -g typescript-axios -c $OPENAPI_CONFIG_DIR -o $OUTPUT_DIR # copy models to tidb-dashboard-lib # merge all models into a large file, to reduce the rebuild times for tidb-dashboard-lib cd $OUTPUT_MODELS_DIR MODEL_FILES="$(ls | grep -v index.ts)" echo "/* tslint:disable */" > ../_models.ts echo "/* eslint-disable */" >> ../_models.ts echo "/* This file is auto generated by tidb-dashboard-client/swagger/gen_api.sh */" >> ../_models.ts for f in $MODEL_FILES; do tail -n +14 $f >> ../_models.ts done sed '/^import/d' ../_models.ts > ../__models.ts rm ../_models.ts mv ../__models.ts $LIB_MODELS_DIR/models.ts # clean rm -rf $OUTPUT_DIR/.openapi-generator rm $OUTPUT_DIR/.gitignore rm $OUTPUT_DIR/.npmignore rm $OUTPUT_DIR/.openapi-generator-ignore rm $OUTPUT_DIR/git_push.sh ================================================ FILE: ui/packages/tidb-dashboard-client/swagger/spec.json ================================================ { "swagger": "2.0", "info": { "title": "Dashboard API", "contact": {}, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, "version": "1.0" }, "basePath": "/dashboard/api", "paths": { "/configuration/all": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get all configurations", "operationId": "configurationGetAll", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/configuration.AllConfigItems" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/configuration/edit": { "post": { "security": [ { "JwtAuth": [] } ], "summary": "Edit a configuration", "operationId": "configurationEdit", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/configuration.EditRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/configuration.EditResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/continuous_profiling/action_token": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get action token for download or view profile", "parameters": [ { "type": "string", "description": "target query string", "name": "q", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/continuous_profiling/components": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get current scraping components", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/conprof.Component" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/continuous_profiling/config": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get Continuous Profiling Config", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/conprof.NgMonitoringConfig" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "post": { "security": [ { "JwtAuth": [] } ], "summary": "Update Continuous Profiling Config", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/conprof.NgMonitoringConfig" } } ], "responses": { "200": { "description": "ok", "schema": { "type": "string" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/continuous_profiling/download": { "get": { "security": [ { "JwtAuth": [] } ], "produces": [ "application/x-gzip" ], "summary": "Download Group Profile files", "parameters": [ { "type": "number", "description": "timestamp", "name": "ts", "in": "query", "required": true } ], "responses": { "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/continuous_profiling/estimate_size": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get Estimate Size", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/conprof.EstimateSizeRes" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/continuous_profiling/group_profile/detail": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get Group Profile Detail", "parameters": [ { "type": "number", "description": "timestamp", "name": "ts", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/conprof.GroupProfileDetail" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/continuous_profiling/group_profiles": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get Group Profiles", "parameters": [ { "type": "integer", "name": "begin_time", "in": "query" }, { "type": "integer", "name": "end_time", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/conprof.GroupProfiles" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/continuous_profiling/single_profile/view": { "get": { "security": [ { "JwtAuth": [] } ], "produces": [ "text/html" ], "summary": "View Single Profile files", "parameters": [ { "type": "string", "name": "address", "in": "query" }, { "type": "string", "name": "component", "in": "query" }, { "type": "string", "name": "profile_type", "in": "query" }, { "type": "integer", "name": "ts", "in": "query" } ], "responses": { "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/deadlock/list": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "List all deadlock records", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/deadlock.Model" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/debug_api/download": { "get": { "summary": "Download a finished request result", "parameters": [ { "type": "string", "description": "download token", "name": "token", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/debug_api/endpoint": { "post": { "security": [ { "JwtAuth": [] } ], "summary": "Send request remote endpoint and return a token for downloading results", "operationId": "debugAPIRequestEndpoint", "parameters": [ { "description": "request payload", "name": "req", "in": "body", "required": true, "schema": { "$ref": "#/definitions/endpoint.RequestPayload" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/debug_api/endpoints": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get all endpoints", "operationId": "debugAPIGetEndpoints", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/endpoint.APIDefinition" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/diagnose/diagnosis": { "post": { "security": [ { "JwtAuth": [] } ], "description": "Generate sql diagnosis report", "produces": [ "application/json" ], "summary": "SQL diagnosis report", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/diagnose.GenDiagnosisReportRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/diagnose.TableDef" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/diagnose/metrics_relation/generate": { "post": { "security": [ { "JwtAuth": [] } ], "summary": "Generate metrics relationship graph.", "operationId": "diagnoseGenerateMetricsRelationship", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/diagnose.GenerateMetricsRelationRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/diagnose/metrics_relation/view": { "get": { "produces": [ "image/svg" ], "summary": "View metrics relationship graph.", "parameters": [ { "type": "string", "description": "token", "name": "token", "in": "query", "required": true } ], "responses": { "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/diagnose/reports": { "get": { "security": [ { "JwtAuth": [] } ], "description": "Get sql diagnosis reports history", "summary": "SQL diagnosis reports history", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/diagnose.Report" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "post": { "security": [ { "JwtAuth": [] } ], "description": "Generate sql diagnosis report", "summary": "SQL diagnosis report", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/diagnose.GenerateReportRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "integer" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/diagnose/reports/{id}/data.js": { "get": { "description": "Get sql diagnosis report data", "produces": [ "text/javascript" ], "summary": "SQL diagnosis report data", "parameters": [ { "type": "string", "description": "report id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/diagnose/reports/{id}/detail": { "get": { "description": "Get sql diagnosis report HTML", "produces": [ "text/html" ], "summary": "SQL diagnosis report", "parameters": [ { "type": "string", "description": "report id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/diagnose/reports/{id}/status": { "get": { "security": [ { "JwtAuth": [] } ], "description": "Get diagnosis report status", "summary": "Diagnosis report status", "parameters": [ { "type": "string", "description": "report id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/diagnose.Report" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/host/all": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get information of all hosts", "operationId": "clusterInfoGetHostsInfo", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/clusterinfo.GetHostsInfoResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/host/statistics": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get cluster statistics", "operationId": "clusterInfoGetStatistics", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/clusterinfo.ClusterStatistics" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/info/databases": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "List all databases", "operationId": "infoListDatabases", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "type": "string" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/info/info": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get information about this TiDB Dashboard", "operationId": "infoGet", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/info.InfoResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/info/tables": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "List tables by database name", "operationId": "infoListTables", "parameters": [ { "type": "string", "description": "Database name", "name": "database_name", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/info.tableSchema" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/info/whoami": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get information about current session", "operationId": "infoWhoami", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/info.WhoAmIResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/keyvisual/config": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get Key Visual Dynamic Config", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/config.KeyVisualConfig" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "put": { "security": [ { "JwtAuth": [] } ], "summary": "Set Key Visual Dynamic Config", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/config.KeyVisualConfig" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/config.KeyVisualConfig" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/keyvisual/heatmaps": { "get": { "security": [ { "JwtAuth": [] } ], "description": "Heatmaps in a given range to visualize TiKV usage", "summary": "Key Visual Heatmaps", "parameters": [ { "type": "string", "description": "The start of the key range", "name": "startkey", "in": "query" }, { "type": "string", "description": "The end of the key range", "name": "endkey", "in": "query" }, { "type": "integer", "description": "The start of the time range (Unix)", "name": "starttime", "in": "query" }, { "type": "integer", "description": "The end of the time range (Unix)", "name": "endtime", "in": "query" }, { "enum": [ "written_bytes", "read_bytes", "written_keys", "read_keys", "integration" ], "type": "string", "description": "Main types of data", "name": "type", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/matrix.Matrix" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/logs/download": { "get": { "produces": [ "application/x-tar", "application/zip" ], "summary": "Download logs", "parameters": [ { "type": "string", "description": "download token", "name": "token", "in": "query", "required": true } ], "responses": { "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/logs/download/acquire_token": { "get": { "security": [ { "JwtAuth": [] } ], "produces": [ "text/plain" ], "summary": "Generate a download token for downloading logs", "parameters": [ { "type": "array", "items": { "type": "string" }, "collectionFormat": "csv", "description": "task id", "name": "id", "in": "query" } ], "responses": { "200": { "description": "xxx", "schema": { "type": "string" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/logs/taskgroup": { "put": { "security": [ { "JwtAuth": [] } ], "summary": "Create and run a new log search task group", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/logsearch.CreateTaskGroupRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/logsearch.TaskGroupResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/logs/taskgroups": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "List all log search task groups", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/logsearch.TaskGroupModel" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/logs/taskgroups/{id}": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "List tasks in a log search task group", "parameters": [ { "type": "string", "description": "Task Group ID", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/logsearch.TaskGroupResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "delete": { "security": [ { "JwtAuth": [] } ], "summary": "Delete a log search task group", "parameters": [ { "type": "string", "description": "task group id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/rest.EmptyResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/logs/taskgroups/{id}/cancel": { "post": { "security": [ { "JwtAuth": [] } ], "summary": "Cancel running tasks in a log search task group", "parameters": [ { "type": "string", "description": "task group id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/rest.EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/logs/taskgroups/{id}/preview": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Preview a log search task group", "parameters": [ { "type": "string", "description": "task group id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/logsearch.PreviewModel" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/logs/taskgroups/{id}/retry": { "post": { "security": [ { "JwtAuth": [] } ], "summary": "Retry failed tasks in a log search task group", "parameters": [ { "type": "string", "description": "task group id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/rest.EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/metrics/prom_address": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get the Prometheus address cluster config", "operationId": "metricsGetPromAddress", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/metrics.GetPromAddressConfigResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "put": { "security": [ { "JwtAuth": [] } ], "summary": "Set or clear the customized Prometheus address", "operationId": "metricsSetCustomPromAddress", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/metrics.PutCustomPromAddressRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/metrics.PutCustomPromAddressResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/metrics/query": { "get": { "security": [ { "JwtAuth": [] } ], "description": "Query metrics in the given range", "summary": "Query metrics", "parameters": [ { "type": "integer", "name": "end_time_sec", "in": "query" }, { "type": "string", "name": "query", "in": "query" }, { "type": "integer", "name": "start_time_sec", "in": "query" }, { "type": "integer", "name": "step_sec", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/metrics.QueryResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/profiling/action_token": { "get": { "security": [ { "JwtAuth": [] } ], "description": "Get token with a given group ID or task ID and action type", "produces": [ "text/plain" ], "summary": "Get action token for download or view", "operationId": "getActionToken", "parameters": [ { "type": "string", "description": "group or task ID", "name": "id", "in": "query" }, { "type": "string", "description": "action", "name": "action", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/profiling/config": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get Profiling Dynamic Config", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/config.ProfilingConfig" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "put": { "security": [ { "JwtAuth": [] } ], "summary": "Set Profiling Dynamic Config", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/config.ProfilingConfig" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/config.ProfilingConfig" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/profiling/group/cancel/{groupId}": { "post": { "security": [ { "JwtAuth": [] } ], "description": "Cancel all profling tasks with a given group ID", "summary": "Cancel all tasks with a given group ID", "operationId": "cancelProfilingGroup", "parameters": [ { "type": "string", "description": "group ID", "name": "groupId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/rest.EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/profiling/group/delete/{groupId}": { "delete": { "security": [ { "JwtAuth": [] } ], "description": "Delete all finished profiling tasks with a given group ID", "summary": "Delete all tasks with a given group ID", "operationId": "deleteProfilingGroup", "parameters": [ { "type": "string", "description": "group ID", "name": "groupId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/rest.EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/profiling/group/detail/{groupId}": { "get": { "security": [ { "JwtAuth": [] } ], "description": "List all profiling tasks with a given group ID", "summary": "List all tasks with a given group ID", "operationId": "getProfilingGroupDetail", "parameters": [ { "type": "string", "description": "group ID", "name": "groupId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/profiling.GroupDetailResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/profiling/group/download": { "get": { "security": [ { "JwtAuth": [] } ], "description": "Download all finished profiling results of a task group", "produces": [ "application/x-gzip" ], "summary": "Download all results of a task group", "operationId": "downloadProfilingGroup", "parameters": [ { "type": "string", "description": "download token", "name": "token", "in": "query", "required": true } ], "responses": { "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/profiling/group/list": { "get": { "security": [ { "JwtAuth": [] } ], "description": "List all profiling groups", "summary": "List all profiling groups", "operationId": "getProfilingGroups", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/profiling.TaskGroupModel" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/profiling/group/start": { "post": { "security": [ { "JwtAuth": [] } ], "description": "Start a profiling task group", "summary": "Start profiling", "operationId": "startProfiling", "parameters": [ { "description": "profiling request", "name": "req", "in": "body", "required": true, "schema": { "$ref": "#/definitions/profiling.StartRequest" } } ], "responses": { "200": { "description": "task group", "schema": { "$ref": "#/definitions/profiling.TaskGroupModel" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/profiling/single/download": { "get": { "security": [ { "JwtAuth": [] } ], "description": "Download the finished profiling result of a task", "produces": [ "application/x-gzip" ], "summary": "Download the result of a task", "operationId": "downloadProfilingSingle", "parameters": [ { "type": "string", "description": "download token", "name": "token", "in": "query", "required": true } ], "responses": { "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/profiling/single/view": { "get": { "security": [ { "JwtAuth": [] } ], "description": "View the finished profiling result of a task", "produces": [ "text/html" ], "summary": "View the result of a task", "operationId": "viewProfilingSingle", "parameters": [ { "type": "string", "description": "download token", "name": "token", "in": "query", "required": true } ], "responses": { "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/query_editor/run": { "post": { "security": [ { "JwtAuth": [] } ], "summary": "Run statements", "operationId": "queryEditorRun", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/queryeditor.RunRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/queryeditor.RunResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/resource_manager/calibrate/actual": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get calibrate of Resource Groups by actual workload", "parameters": [ { "type": "integer", "name": "end_time", "in": "query" }, { "type": "integer", "name": "start_time", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/resourcemanager.CalibrateResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/resource_manager/calibrate/hardware": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get calibrate of Resource Groups by hardware deployment", "parameters": [ { "type": "string", "default": "\"tpcc\"", "description": "workload", "name": "workload", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/resourcemanager.CalibrateResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/resource_manager/config": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get Resource Control enable config", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/resourcemanager.GetConfigResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/resource_manager/information": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get Information of Resource Groups", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/resourcemanager.ResourceInfoRowDef" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/resource_manager/information/group_names": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "List all resource groups", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "type": "string" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/slow_query/available_fields": { "get": { "security": [ { "JwtAuth": [] } ], "description": "Get available field names by slowquery table columns", "summary": "Get available field names", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "type": "string" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/slow_query/detail": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get details of a slow query", "parameters": [ { "type": "string", "description": "TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well.", "name": "connect_id", "in": "query" }, { "type": "string", "name": "digest", "in": "query" }, { "type": "number", "name": "timestamp", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/slowquery.Model" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/slow_query/download": { "get": { "produces": [ "text/csv" ], "summary": "Download slow query statements", "parameters": [ { "type": "string", "description": "download token", "name": "token", "in": "query", "required": true } ], "responses": { "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/slow_query/download/token": { "post": { "security": [ { "JwtAuth": [] } ], "produces": [ "text/plain" ], "summary": "Generate a download token for exported slow query statements", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/slowquery.GetListRequest" } } ], "responses": { "200": { "description": "xxx", "schema": { "type": "string" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/slow_query/list": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "List all slow queries", "parameters": [ { "type": "integer", "name": "begin_time", "in": "query" }, { "type": "array", "items": { "type": "string" }, "collectionFormat": "multi", "name": "db", "in": "query" }, { "type": "boolean", "name": "desc", "in": "query" }, { "type": "string", "name": "digest", "in": "query" }, { "type": "integer", "name": "end_time", "in": "query" }, { "type": "string", "description": "example: \"Query,Digest\"", "name": "fields", "in": "query" }, { "type": "integer", "name": "limit", "in": "query" }, { "type": "string", "name": "orderBy", "in": "query" }, { "type": "array", "items": { "type": "string" }, "collectionFormat": "multi", "description": "for showing slow queries in the statement detail page", "name": "plans", "in": "query" }, { "type": "array", "items": { "type": "string" }, "collectionFormat": "multi", "name": "resource_group", "in": "query" }, { "type": "string", "name": "text", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/slowquery.Model" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/statements/available_fields": { "get": { "security": [ { "JwtAuth": [] } ], "description": "Get available field names by statements table columns", "summary": "Get available field names", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "type": "string" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/statements/config": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get statement configurations", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/statement.EditableConfig" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "post": { "security": [ { "JwtAuth": [] } ], "summary": "Update statement configurations", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/statement.EditableConfig" } } ], "responses": { "204": { "description": "No Content", "schema": { "type": "string" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/statements/download": { "get": { "produces": [ "text/csv" ], "summary": "Download statements", "parameters": [ { "type": "string", "description": "download token", "name": "token", "in": "query", "required": true } ], "responses": { "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/statements/download/token": { "post": { "security": [ { "JwtAuth": [] } ], "produces": [ "text/plain" ], "summary": "Generate a download token for exported statements", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/statement.GetStatementsRequest" } } ], "responses": { "200": { "description": "xxx", "schema": { "type": "string" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/statements/list": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get a list of statements", "parameters": [ { "type": "integer", "name": "begin_time", "in": "query" }, { "type": "integer", "name": "end_time", "in": "query" }, { "type": "string", "name": "fields", "in": "query" }, { "type": "array", "items": { "type": "string" }, "collectionFormat": "multi", "name": "resource_groups", "in": "query" }, { "type": "array", "items": { "type": "string" }, "collectionFormat": "multi", "name": "schemas", "in": "query" }, { "type": "array", "items": { "type": "string" }, "collectionFormat": "multi", "name": "stmt_types", "in": "query" }, { "type": "string", "name": "text", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/statement.Model" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/statements/plan/binding": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get the bound plan digest (if exists) of a statement", "parameters": [ { "type": "string", "description": "query template id", "name": "sql_digest", "in": "query", "required": true }, { "type": "integer", "description": "begin time", "name": "begin_time", "in": "query", "required": true }, { "type": "integer", "description": "end time", "name": "end_time", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/statement.Binding" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "post": { "security": [ { "JwtAuth": [] } ], "summary": "Create a binding for a statement and a plan", "parameters": [ { "type": "string", "description": "plan digest id", "name": "plan_digest", "in": "query", "required": true } ], "responses": { "200": { "description": "success", "schema": { "type": "string" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "delete": { "security": [ { "JwtAuth": [] } ], "summary": "Drop all manually created bindings for a statement", "parameters": [ { "type": "string", "description": "query template ID (a.k.a. sql digest)", "name": "sql_digest", "in": "query", "required": true } ], "responses": { "200": { "description": "success", "schema": { "type": "string" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/statements/plan/detail": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get details of a statement in an execution plan", "parameters": [ { "type": "integer", "name": "begin_time", "in": "query" }, { "type": "string", "name": "digest", "in": "query" }, { "type": "integer", "name": "end_time", "in": "query" }, { "type": "array", "items": { "type": "string" }, "collectionFormat": "multi", "name": "plans", "in": "query" }, { "type": "string", "name": "schema_name", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/statement.Model" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/statements/plans": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get execution plans of a statement", "parameters": [ { "type": "integer", "name": "begin_time", "in": "query" }, { "type": "string", "name": "digest", "in": "query" }, { "type": "integer", "name": "end_time", "in": "query" }, { "type": "string", "name": "schema_name", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/statement.Model" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/statements/stmt_types": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get all statement types", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "type": "string" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/alertmanager": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get AlertManager instance", "operationId": "getAlertManagerTopology", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/topology.AlertManagerInfo" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/alertmanager/{address}/count": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get current alert count from AlertManager", "operationId": "getAlertManagerCounts", "parameters": [ { "type": "string", "description": "ip:port", "name": "address", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "integer" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/grafana": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get Grafana instance", "operationId": "getGrafanaTopology", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/topology.GrafanaInfo" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/pd": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get all PD instances", "operationId": "getPDTopology", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/topology.PDInfo" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/scheduling": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get all Scheduling instances", "operationId": "getSchedulingTopology", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/topology.SchedulingInfo" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/store": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get all TiKV / TiFlash instances", "operationId": "getStoreTopology", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/clusterinfo.StoreTopologyResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/store_location": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get location labels of all TiKV / TiFlash instances", "operationId": "getStoreLocationTopology", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/topology.StoreLocation" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/ticdc": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get all TiCDC instances", "operationId": "getTiCDCTopology", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/topology.TiCDCInfo" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/tidb": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get all TiDB instances", "operationId": "getTiDBTopology", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/topology.TiDBInfo" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/tidb/{address}": { "delete": { "security": [ { "JwtAuth": [] } ], "summary": "Hide a TiDB instance", "parameters": [ { "type": "string", "description": "ip:port", "name": "address", "in": "path", "required": true } ], "responses": { "200": { "description": "delete ok" }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/tiproxy": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get all TiProxy instances", "operationId": "getTiProxyTopology", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/topology.TiProxyInfo" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topology/tso": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get all TSO instances", "operationId": "getTSOTopology", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/topology.TSOInfo" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topsql/config": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get Top SQL config", "responses": { "200": { "description": "ok", "schema": { "$ref": "#/definitions/topsql.EditableConfig" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "post": { "security": [ { "JwtAuth": [] } ], "summary": "Update Top SQL config", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/topsql.EditableConfig" } } ], "responses": { "204": { "description": "No Content", "schema": { "type": "string" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topsql/instances": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get available instances", "parameters": [ { "type": "string", "name": "data_source", "in": "query" }, { "type": "string", "name": "end", "in": "query" }, { "type": "string", "name": "start", "in": "query" } ], "responses": { "200": { "description": "ok", "schema": { "$ref": "#/definitions/topsql.InstanceResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topsql/summary": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get summaries", "parameters": [ { "type": "string", "name": "data_source", "in": "query" }, { "type": "string", "name": "end", "in": "query" }, { "type": "string", "name": "group_by", "in": "query" }, { "type": "string", "name": "instance", "in": "query" }, { "type": "string", "name": "instance_type", "in": "query" }, { "type": "string", "name": "order_by", "in": "query" }, { "type": "string", "name": "start", "in": "query" }, { "type": "string", "name": "top", "in": "query" }, { "type": "string", "name": "window", "in": "query" } ], "responses": { "200": { "description": "ok", "schema": { "$ref": "#/definitions/topsql.SummaryResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/topsql/tikv_network_io_collection": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get TiKV network IO collection config", "operationId": "topsqlGetTiKVNetworkIOCollection", "responses": { "200": { "description": "ok", "schema": { "$ref": "#/definitions/topsql.TikvNetworkIoCollectionConfig" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "post": { "security": [ { "JwtAuth": [] } ], "summary": "Update TiKV network IO collection config", "operationId": "topsqlUpdateTiKVNetworkIOCollection", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/topsql.TikvNetworkIoCollectionConfig" } } ], "responses": { "200": { "description": "ok", "schema": { "$ref": "#/definitions/topsql.UpdateTikvNetworkIoCollectionResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/user/login": { "post": { "summary": "Log in", "operationId": "userLogin", "parameters": [ { "description": "Credentials", "name": "message", "in": "body", "required": true, "schema": { "$ref": "#/definitions/user.AuthenticateForm" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/user.TokenResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/user/login_info": { "get": { "summary": "Get log in information, like supported authenticate types", "operationId": "userGetLoginInfo", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/user.GetLoginInfoResponse" } } } } }, "/user/share/code": { "post": { "security": [ { "JwtAuth": [] } ], "summary": "Share current session and generate a sharing code", "operationId": "userShareSession", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/code.ShareRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/code.ShareResponse" } } } } }, "/user/share/revoke": { "post": { "security": [ { "JwtAuth": [] } ], "summary": "Reset encryption key to revoke all authorized codes", "operationId": "userRevokeSession", "responses": { "200": { "description": "" } } } }, "/user/sign_out_info": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get sign out info", "operationId": "userGetSignOutInfo", "parameters": [ { "type": "string", "name": "redirect_url", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/user.SignOutInfo" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/user/sso/auth_url": { "get": { "summary": "Get SSO Auth URL", "operationId": "userSSOGetAuthURL", "parameters": [ { "type": "string", "name": "code_verifier", "in": "query" }, { "type": "string", "name": "redirect_url", "in": "query" }, { "type": "string", "name": "state", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } } } } }, "/user/sso/config": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "Get SSO config", "operationId": "userSSOGetConfig", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/config.SSOCoreConfig" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "put": { "security": [ { "JwtAuth": [] } ], "summary": "Set SSO config", "operationId": "userSSOSetConfig", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/sso.SetConfigRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/config.SSOCoreConfig" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/user/sso/impersonation": { "post": { "security": [ { "JwtAuth": [] } ], "summary": "Create an impersonation", "operationId": "userSSOCreateImpersonation", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/sso.CreateImpersonationRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/sso.SSOImpersonationModel" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } }, "/user/sso/impersonations/list": { "get": { "security": [ { "JwtAuth": [] } ], "summary": "List all impersonations", "operationId": "userSSOListImpersonations", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/sso.SSOImpersonationModel" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/rest.ErrorResponse" } } } } } }, "definitions": { "clusterinfo.ClusterStatistics": { "type": "object", "properties": { "probe_failure_hosts": { "type": "integer" }, "stats_by_instance_kind": { "type": "object", "additionalProperties": { "$ref": "#/definitions/clusterinfo.ClusterStatisticsPartial" } }, "total_stats": { "$ref": "#/definitions/clusterinfo.ClusterStatisticsPartial" }, "versions": { "type": "array", "items": { "type": "string" } } } }, "clusterinfo.ClusterStatisticsPartial": { "type": "object", "properties": { "number_of_hosts": { "type": "integer" }, "number_of_instances": { "type": "integer" }, "total_logical_cores": { "type": "integer" }, "total_memory_capacity_bytes": { "type": "integer" }, "total_physical_cores": { "type": "integer" } } }, "clusterinfo.GetHostsInfoResponse": { "type": "object", "properties": { "hosts": { "type": "array", "items": { "$ref": "#/definitions/hostinfo.Info" } }, "warning": { "$ref": "#/definitions/rest.ErrorResponse" } } }, "clusterinfo.StoreTopologyResponse": { "type": "object", "properties": { "tiflash": { "type": "array", "items": { "$ref": "#/definitions/topology.StoreInfo" } }, "tikv": { "type": "array", "items": { "$ref": "#/definitions/topology.StoreInfo" } } } }, "code.ShareRequest": { "type": "object", "properties": { "expire_in_sec": { "type": "integer" }, "revoke_write_priv": { "type": "boolean" } } }, "code.ShareResponse": { "type": "object", "properties": { "code": { "type": "string" } } }, "config.KeyVisualConfig": { "type": "object", "properties": { "auto_collection_disabled": { "type": "boolean" }, "policy": { "type": "string" }, "policy_kv_separator": { "type": "string" } } }, "config.ProfilingConfig": { "type": "object", "properties": { "auto_collection_duration_secs": { "type": "integer" }, "auto_collection_interval_secs": { "type": "integer" }, "auto_collection_targets": { "type": "array", "items": { "$ref": "#/definitions/model.RequestTargetNode" } } } }, "config.SSOCoreConfig": { "type": "object", "properties": { "client_id": { "type": "string" }, "client_secret": { "type": "string" }, "discovery_url": { "type": "string" }, "enabled": { "type": "boolean" }, "is_read_only": { "type": "boolean" }, "scopes": { "type": "string" } } }, "configuration.AllConfigItems": { "type": "object", "properties": { "errors": { "type": "array", "items": { "$ref": "#/definitions/rest.ErrorResponse" } }, "items": { "type": "object", "additionalProperties": { "type": "array", "items": { "$ref": "#/definitions/configuration.Item" } } } } }, "configuration.EditRequest": { "type": "object", "properties": { "id": { "type": "string" }, "kind": { "type": "string" }, "new_value": {} } }, "configuration.EditResponse": { "type": "object", "properties": { "warnings": { "type": "array", "items": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "configuration.Item": { "type": "object", "properties": { "id": { "type": "string" }, "is_editable": { "type": "boolean" }, "is_multi_value": { "description": "TODO: Support per-instance config", "type": "boolean" }, "value": { "description": "When multi value present, this contains one of the value" } } }, "conprof.Component": { "type": "object", "properties": { "ip": { "type": "string" }, "name": { "type": "string" }, "port": { "type": "integer" }, "status_port": { "type": "integer" } } }, "conprof.ComponentNum": { "type": "object", "properties": { "pd": { "type": "integer" }, "ticdc": { "type": "integer" }, "tidb": { "type": "integer" }, "tiflash": { "type": "integer" }, "tikv": { "type": "integer" } } }, "conprof.ContinuousProfilingConfig": { "type": "object", "properties": { "data_retention_seconds": { "type": "integer" }, "enable": { "type": "boolean" }, "interval_seconds": { "type": "integer" }, "profile_seconds": { "type": "integer" }, "timeout_seconds": { "type": "integer" } } }, "conprof.EstimateSizeRes": { "type": "object", "properties": { "instance_count": { "type": "integer" }, "profile_size": { "type": "integer" } } }, "conprof.GroupProfileDetail": { "type": "object", "properties": { "profile_duration_secs": { "type": "integer" }, "state": { "type": "string" }, "target_profiles": { "type": "array", "items": { "$ref": "#/definitions/conprof.ProfileDetail" } }, "ts": { "type": "integer" } } }, "conprof.GroupProfiles": { "type": "object", "properties": { "component_num": { "$ref": "#/definitions/conprof.ComponentNum" }, "profile_duration_secs": { "type": "integer" }, "state": { "type": "string" }, "ts": { "type": "integer" } } }, "conprof.NgMonitoringConfig": { "type": "object", "properties": { "continuous_profiling": { "$ref": "#/definitions/conprof.ContinuousProfilingConfig" } } }, "conprof.ProfileDetail": { "type": "object", "properties": { "error": { "type": "string" }, "profile_type": { "type": "string" }, "state": { "type": "string" }, "target": { "$ref": "#/definitions/conprof.Target" } } }, "conprof.Target": { "type": "object", "properties": { "address": { "type": "string" }, "component": { "type": "string" } } }, "deadlock.Model": { "type": "object", "properties": { "current_sql": { "type": "string" }, "id": { "type": "integer" }, "instance": { "type": "string" }, "key": { "type": "string" }, "key_info": { "type": "string" }, "occur_time": { "type": "string" }, "retryable": { "type": "boolean" }, "trx_holding_lock": { "type": "integer" }, "try_lock_trx_id": { "type": "integer" } } }, "decorator.LabelKey": { "type": "object", "required": [ "key", "labels" ], "properties": { "key": { "type": "string" }, "labels": { "type": "array", "items": { "type": "string" } } } }, "diagnose.GenDiagnosisReportRequest": { "type": "object", "properties": { "end_time": { "type": "integer" }, "kind": { "description": "values: config, error, performance", "type": "string" }, "start_time": { "type": "integer" } } }, "diagnose.GenerateMetricsRelationRequest": { "type": "object", "properties": { "end_time": { "type": "integer" }, "start_time": { "type": "integer" }, "type": { "type": "string" } } }, "diagnose.GenerateReportRequest": { "type": "object", "properties": { "compare_end_time": { "type": "integer" }, "compare_start_time": { "type": "integer" }, "end_time": { "type": "integer" }, "start_time": { "type": "integer" } } }, "diagnose.Report": { "type": "object", "properties": { "compare_end_time": { "type": "string" }, "compare_start_time": { "type": "string" }, "content": { "type": "string" }, "created_at": { "type": "string" }, "end_time": { "type": "string" }, "id": { "type": "string" }, "progress": { "description": "0~100", "type": "integer" }, "start_time": { "type": "string" } } }, "diagnose.TableDef": { "type": "object", "properties": { "category": { "description": "The category of the table, such as [TiDB]", "type": "array", "items": { "type": "string" } }, "column": { "type": "array", "items": { "type": "string" } }, "comment": { "type": "string" }, "rows": { "type": "array", "items": { "$ref": "#/definitions/diagnose.TableRowDef" } }, "title": { "type": "string" } } }, "diagnose.TableRowDef": { "type": "object", "properties": { "comment": { "type": "string" }, "sub_values": { "description": "SubValues need fold default.", "type": "array", "items": { "type": "array", "items": { "type": "string" } } }, "values": { "type": "array", "items": { "type": "string" } } } }, "endpoint.APIDefinition": { "type": "object", "properties": { "component": { "type": "string" }, "id": { "type": "string" }, "method": { "type": "string" }, "path": { "type": "string" }, "path_params": { "description": "e.g. /stats/dump/{db}/{table} -\u003e db, table", "type": "array", "items": { "$ref": "#/definitions/endpoint.APIParamDefinition" } }, "query_params": { "description": "e.g. /debug/pprof?seconds=1 -\u003e seconds", "type": "array", "items": { "$ref": "#/definitions/endpoint.APIParamDefinition" } } } }, "endpoint.APIParamDefinition": { "type": "object", "properties": { "name": { "type": "string" }, "required": { "type": "boolean" }, "ui_kind": { "type": "string" }, "ui_props": { "description": "varies by different ui kinds" } } }, "endpoint.RequestPayload": { "type": "object", "properties": { "api_id": { "type": "string" }, "host": { "type": "string" }, "param_values": { "type": "object", "additionalProperties": { "type": "string" } }, "port": { "type": "integer" } } }, "hostinfo.CPUInfo": { "type": "object", "properties": { "arch": { "type": "string" }, "logical_cores": { "type": "integer" }, "physical_cores": { "type": "integer" } } }, "hostinfo.CPUUsageInfo": { "type": "object", "properties": { "idle": { "type": "number" }, "system": { "type": "number" } } }, "hostinfo.Info": { "type": "object", "properties": { "cpu_info": { "$ref": "#/definitions/hostinfo.CPUInfo" }, "cpu_usage": { "$ref": "#/definitions/hostinfo.CPUUsageInfo" }, "host": { "type": "string" }, "instances": { "description": "Instances in the current host. The key is instance address", "type": "object", "additionalProperties": { "$ref": "#/definitions/hostinfo.InstanceInfo" } }, "memory_usage": { "$ref": "#/definitions/hostinfo.MemoryUsageInfo" }, "partitions": { "description": "Containing unused partitions. The key is path in lower case.\nNote: deviceName is not used as the key, since TiDB and TiKV may return different deviceName for the same device.", "type": "object", "additionalProperties": { "$ref": "#/definitions/hostinfo.PartitionInfo" } } } }, "hostinfo.InstanceInfo": { "type": "object", "properties": { "partition_path_lower": { "type": "string" }, "type": { "type": "string" } } }, "hostinfo.MemoryUsageInfo": { "type": "object", "properties": { "total": { "type": "integer" }, "used": { "type": "integer" } } }, "hostinfo.PartitionInfo": { "type": "object", "properties": { "free": { "type": "integer" }, "fstype": { "type": "string" }, "path": { "type": "string" }, "total": { "type": "integer" } } }, "info.InfoResponse": { "type": "object", "properties": { "enable_experimental": { "type": "boolean" }, "enable_telemetry": { "type": "boolean" }, "ngm_state": { "type": "string" }, "supported_features": { "type": "array", "items": { "type": "string" } }, "version": { "$ref": "#/definitions/version.Info" } } }, "info.WhoAmIResponse": { "type": "object", "properties": { "display_name": { "type": "string" }, "is_shareable": { "type": "boolean" }, "is_writeable": { "type": "boolean" } } }, "info.tableSchema": { "type": "object", "properties": { "table_id": { "type": "string" }, "table_name": { "type": "string" } } }, "logsearch.CreateTaskGroupRequest": { "type": "object", "required": [ "request", "targets" ], "properties": { "request": { "$ref": "#/definitions/logsearch.SearchLogRequest" }, "targets": { "type": "array", "items": { "$ref": "#/definitions/model.RequestTargetNode" } } } }, "logsearch.PreviewModel": { "type": "object", "properties": { "id": { "type": "integer" }, "level": { "type": "integer" }, "message": { "type": "string" }, "task_group_id": { "type": "integer" }, "task_id": { "type": "integer" }, "time": { "type": "integer" } } }, "logsearch.SearchLogRequest": { "type": "object", "properties": { "end_time": { "type": "integer" }, "min_level": { "type": "integer" }, "patterns": { "description": "We use a string array to represent multiple CNF pattern sceniaor like:\nSELECT * FROM t WHERE c LIKE '%s%' and c REGEXP '.*a.*' because\nGolang and Rust don't support perl-like (?=re1)(?=re2)", "type": "array", "items": { "type": "string" } }, "start_time": { "type": "integer" } } }, "logsearch.TaskGroupModel": { "type": "object", "properties": { "id": { "type": "integer" }, "log_store_dir": { "type": "string" }, "search_request": { "$ref": "#/definitions/logsearch.SearchLogRequest" }, "state": { "type": "integer" }, "target_stats": { "$ref": "#/definitions/model.RequestTargetStatistics" } } }, "logsearch.TaskGroupResponse": { "type": "object", "properties": { "task_group": { "$ref": "#/definitions/logsearch.TaskGroupModel" }, "tasks": { "type": "array", "items": { "$ref": "#/definitions/logsearch.TaskModel" } } } }, "logsearch.TaskModel": { "type": "object", "properties": { "error": { "type": "string" }, "id": { "type": "integer" }, "log_store_path": { "type": "string" }, "size": { "type": "integer" }, "slow_log_store_path": { "type": "string" }, "state": { "type": "integer" }, "target": { "$ref": "#/definitions/model.RequestTargetNode" }, "task_group_id": { "type": "integer" } } }, "matrix.Matrix": { "type": "object", "required": [ "data", "keyAxis", "timeAxis" ], "properties": { "data": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "array", "items": { "type": "integer" } } } }, "keyAxis": { "type": "array", "items": { "$ref": "#/definitions/decorator.LabelKey" } }, "timeAxis": { "type": "array", "items": { "type": "integer" } } } }, "metrics.GetPromAddressConfigResponse": { "type": "object", "properties": { "customized_addr": { "type": "string" }, "deployed_addr": { "type": "string" } } }, "metrics.PutCustomPromAddressRequest": { "type": "object", "properties": { "address": { "type": "string" } } }, "metrics.PutCustomPromAddressResponse": { "type": "object", "properties": { "normalized_address": { "type": "string" } } }, "metrics.QueryResponse": { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": true }, "status": { "type": "string" } } }, "model.RequestTargetNode": { "type": "object", "properties": { "display_name": { "type": "string", "example": "127.0.0.1:4000" }, "ip": { "type": "string", "example": "127.0.0.1" }, "kind": { "type": "string", "example": "tidb" }, "port": { "type": "integer", "example": 4000 } } }, "model.RequestTargetStatistics": { "type": "object", "properties": { "num_pd_nodes": { "type": "integer" }, "num_scheduling_nodes": { "type": "integer" }, "num_ticdc_nodes": { "type": "integer" }, "num_tidb_nodes": { "type": "integer" }, "num_tiflash_nodes": { "type": "integer" }, "num_tikv_nodes": { "type": "integer" }, "num_tiproxy_nodes": { "type": "integer" }, "num_tso_nodes": { "type": "integer" } } }, "profiling.GroupDetailResponse": { "type": "object", "properties": { "server_time": { "type": "integer" }, "task_group_status": { "$ref": "#/definitions/profiling.TaskGroupModel" }, "tasks_status": { "type": "array", "items": { "$ref": "#/definitions/profiling.TaskModel" } } } }, "profiling.StartRequest": { "type": "object", "properties": { "duration_secs": { "type": "integer" }, "requsted_profiling_types": { "type": "array", "items": { "type": "string" } }, "targets": { "type": "array", "items": { "$ref": "#/definitions/model.RequestTargetNode" } } } }, "profiling.TaskGroupModel": { "type": "object", "properties": { "id": { "type": "integer" }, "profile_duration_secs": { "type": "integer" }, "requsted_profiling_types": { "type": "array", "items": { "type": "string" } }, "started_at": { "type": "integer" }, "state": { "type": "integer" }, "target_stats": { "$ref": "#/definitions/model.RequestTargetStatistics" } } }, "profiling.TaskModel": { "type": "object", "properties": { "error": { "type": "string" }, "id": { "type": "integer" }, "profiling_type": { "type": "string" }, "raw_data_type": { "type": "string" }, "started_at": { "description": "The start running time, reset when retry. Used to estimate approximate profiling progress.", "type": "integer" }, "state": { "type": "integer" }, "target": { "$ref": "#/definitions/model.RequestTargetNode" }, "task_group_id": { "type": "integer" } } }, "queryeditor.RunRequest": { "type": "object", "properties": { "max_rows": { "type": "integer", "example": 1000 }, "statements": { "type": "string", "example": "show databases;" } } }, "queryeditor.RunResponse": { "type": "object", "properties": { "actual_rows": { "type": "integer" }, "column_names": { "type": "array", "items": { "type": "string" } }, "error_msg": { "type": "string" }, "execution_ms": { "type": "integer" }, "rows": { "type": "array", "items": { "type": "array", "items": {} } } } }, "resourcemanager.CalibrateResponse": { "type": "object", "properties": { "estimated_capacity": { "type": "integer" } } }, "resourcemanager.GetConfigResponse": { "type": "object", "properties": { "enable": { "type": "boolean" } } }, "resourcemanager.ResourceInfoRowDef": { "type": "object", "properties": { "burstable": { "type": "string" }, "name": { "type": "string" }, "priority": { "type": "string" }, "ru_per_sec": { "type": "string" } } }, "rest.EmptyResponse": { "type": "object" }, "rest.ErrorResponse": { "type": "object", "properties": { "code": { "type": "string" }, "error": { "type": "boolean" }, "full_text": { "type": "string" }, "message": { "type": "string" } } }, "slowquery.GetListRequest": { "type": "object", "properties": { "begin_time": { "type": "integer" }, "db": { "type": "array", "items": { "type": "string" } }, "desc": { "type": "boolean" }, "digest": { "type": "string" }, "end_time": { "type": "integer" }, "fields": { "description": "example: \"Query,Digest\"", "type": "string" }, "limit": { "type": "integer" }, "orderBy": { "type": "string" }, "plans": { "description": "for showing slow queries in the statement detail page", "type": "array", "items": { "type": "string" } }, "resource_group": { "type": "array", "items": { "type": "string" } }, "text": { "type": "string" } } }, "slowquery.Model": { "type": "object", "properties": { "backoff_time": { "type": "number" }, "backoff_types": { "type": "string" }, "binary_plan": { "type": "string" }, "binary_plan_json": { "description": "Computed fields", "type": "string" }, "binary_plan_text": { "description": "binary plan plain text", "type": "string" }, "commit_backoff_time": { "type": "number" }, "commit_time": { "type": "number" }, "compile_time": { "type": "number" }, "connection_id": { "description": "TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well.", "type": "string" }, "cop_proc_addr": { "type": "string" }, "cop_proc_avg": { "type": "number" }, "cop_proc_max": { "type": "number" }, "cop_proc_p90": { "type": "number" }, "cop_time": { "type": "number" }, "cop_wait_addr": { "type": "string" }, "cop_wait_avg": { "type": "number" }, "cop_wait_max": { "type": "number" }, "cop_wait_p90": { "type": "number" }, "db": { "type": "string" }, "digest": { "type": "string" }, "disk_max": { "type": "integer" }, "exec_retry_time": { "type": "number" }, "get_commit_ts_time": { "type": "number" }, "host": { "type": "string" }, "ia_remote_read_segment_size": { "description": "IA remote read", "type": "integer" }, "ia_remote_read_segment_wait_time": { "type": "number" }, "index_names": { "type": "string" }, "instance": { "type": "string" }, "is_internal": { "description": "Basic", "type": "integer" }, "local_latch_wait_time": { "type": "number" }, "lock_keys_time": { "type": "number" }, "mem_arbitration": { "type": "number" }, "memory_max": { "type": "integer" }, "optimize_time": { "type": "number" }, "parse_time": { "type": "number" }, "plan": { "description": "deprecated, replaced by BinaryPlanText", "type": "string" }, "plan_from_binding": { "type": "integer" }, "plan_from_cache": { "type": "integer" }, "prepared": { "type": "integer" }, "preproc_subqueries_time": { "type": "number" }, "prev_stmt": { "description": "Detail", "type": "string" }, "prewrite_region": { "type": "integer" }, "prewrite_time": { "type": "number" }, "process_keys": { "type": "integer" }, "process_time": { "description": "Time", "type": "number" }, "query": { "type": "string" }, "query_time": { "description": "latency", "type": "number" }, "request_count": { "description": "Coprocessor", "type": "integer" }, "resolve_lock_time": { "type": "number" }, "resource_group": { "type": "string" }, "rewrite_time": { "type": "number" }, "rocksdb_block_cache_hit_count": { "type": "integer" }, "rocksdb_block_read_byte": { "type": "integer" }, "rocksdb_block_read_count": { "type": "integer" }, "rocksdb_delete_skipped_count": { "description": "RocksDB", "type": "integer" }, "rocksdb_key_skipped_count": { "type": "integer" }, "ru": { "description": "Resource Control", "type": "number" }, "stats": { "type": "string" }, "success": { "type": "integer" }, "time_queued_by_rc": { "type": "number" }, "timestamp": { "description": "finish time", "type": "number" }, "total_keys": { "type": "integer" }, "txn_retry": { "type": "integer" }, "txn_start_ts": { "description": "TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well.", "type": "string" }, "unpacked_bytes_received_tiflash_cross_zone": { "type": "integer" }, "unpacked_bytes_received_tiflash_total": { "type": "integer" }, "unpacked_bytes_received_tikv_cross_zone": { "type": "integer" }, "unpacked_bytes_received_tikv_total": { "type": "integer" }, "unpacked_bytes_sent_tiflash_cross_zone": { "type": "integer" }, "unpacked_bytes_sent_tiflash_total": { "type": "integer" }, "unpacked_bytes_sent_tikv_cross_zone": { "type": "integer" }, "unpacked_bytes_sent_tikv_total": { "description": "Network fields", "type": "integer" }, "user": { "description": "Connection", "type": "string" }, "wait_prewrite_binlog_time": { "type": "number" }, "wait_time": { "type": "number" }, "wait_ts": { "type": "number" }, "warnings": { "type": "array", "items": { "type": "integer" } }, "write_keys": { "description": "Transaction", "type": "integer" }, "write_size": { "type": "integer" }, "write_sql_response_total": { "type": "number" } } }, "sso.CreateImpersonationRequest": { "type": "object", "properties": { "password": { "type": "string" }, "sql_user": { "type": "string" } } }, "sso.SSOImpersonationModel": { "type": "object", "properties": { "last_impersonate_status": { "type": "string" }, "sql_user": { "type": "string" } } }, "sso.SetConfigRequest": { "type": "object", "properties": { "config": { "$ref": "#/definitions/config.SSOCoreConfig" } } }, "statement.Binding": { "type": "object", "properties": { "plan_digest": { "type": "string" }, "source": { "type": "string", "enum": [ "manual", "history", "capture", "evolve" ], "example": "manual" }, "status": { "type": "string", "enum": [ "enabled", "using", "disabled", "deleted", "invalid", "rejected", "pending verify" ], "example": "enabled" } } }, "statement.EditableConfig": { "type": "object", "properties": { "enable": { "type": "boolean" }, "history_size": { "type": "integer" }, "internal_query": { "type": "boolean" }, "max_size": { "type": "integer" }, "refresh_interval": { "type": "integer" } } }, "statement.GetStatementsRequest": { "type": "object", "properties": { "begin_time": { "type": "integer" }, "end_time": { "type": "integer" }, "fields": { "type": "string" }, "resource_groups": { "type": "array", "items": { "type": "string" } }, "schemas": { "type": "array", "items": { "type": "string" } }, "stmt_types": { "type": "array", "items": { "type": "string" } }, "text": { "type": "string" } } }, "statement.Model": { "type": "object", "properties": { "avg_affected_rows": { "type": "integer" }, "avg_backoff_time": { "description": "avg total back off time per sql", "type": "integer" }, "avg_commit_backoff_time": { "type": "integer" }, "avg_commit_time": { "type": "integer" }, "avg_compile_latency": { "type": "integer" }, "avg_cop_process_time": { "description": "avg process time per copr task", "type": "integer" }, "avg_cop_wait_time": { "description": "avg wait time per copr task", "type": "integer" }, "avg_disk": { "type": "integer" }, "avg_get_commit_ts_time": { "type": "integer" }, "avg_ia_read_segment_count": { "type": "number" }, "avg_ia_remote_read_segment_size": { "type": "number" }, "avg_ia_remote_read_segment_wait_time": { "type": "number" }, "avg_latency": { "type": "integer" }, "avg_local_latch_wait_time": { "type": "integer" }, "avg_mem": { "type": "integer" }, "avg_mem_arbitration": { "type": "number" }, "avg_parse_latency": { "type": "integer" }, "avg_prewrite_regions": { "type": "integer" }, "avg_prewrite_time": { "type": "integer" }, "avg_process_time": { "description": "avg total process time per sql", "type": "integer" }, "avg_processed_keys": { "type": "integer" }, "avg_resolve_lock_time": { "type": "integer" }, "avg_rocksdb_block_cache_hit_count": { "type": "integer" }, "avg_rocksdb_block_read_byte": { "type": "integer" }, "avg_rocksdb_block_read_count": { "type": "integer" }, "avg_rocksdb_delete_skipped_count": { "type": "integer" }, "avg_rocksdb_key_skipped_count": { "type": "integer" }, "avg_ru": { "type": "number" }, "avg_time_queued_by_rc": { "type": "number" }, "avg_total_keys": { "type": "integer" }, "avg_txn_retry": { "type": "integer" }, "avg_wait_time": { "description": "avg total wait time per sql", "type": "integer" }, "avg_write_keys": { "type": "integer" }, "avg_write_size": { "type": "integer" }, "binary_plan": { "type": "string" }, "binary_plan_json": { "type": "string" }, "binary_plan_text": { "type": "string" }, "digest": { "type": "string" }, "digest_text": { "type": "string" }, "exec_count": { "type": "integer" }, "first_seen": { "type": "integer" }, "index_names": { "type": "string" }, "last_seen": { "type": "integer" }, "max_backoff_time": { "description": "max back off time per sql", "type": "integer" }, "max_commit_backoff_time": { "type": "integer" }, "max_commit_time": { "type": "integer" }, "max_compile_latency": { "type": "integer" }, "max_cop_process_time": { "description": "max process time per copr task", "type": "integer" }, "max_cop_wait_time": { "description": "max wait time per copr task", "type": "integer" }, "max_disk": { "type": "integer" }, "max_get_commit_ts_time": { "type": "integer" }, "max_ia_read_segment_count": { "type": "integer" }, "max_ia_remote_read_segment_size": { "type": "integer" }, "max_ia_remote_read_segment_wait_time": { "type": "number" }, "max_latency": { "type": "integer" }, "max_local_latch_wait_time": { "type": "integer" }, "max_mem": { "type": "integer" }, "max_mem_arbitration": { "type": "number" }, "max_parse_latency": { "type": "integer" }, "max_prewrite_regions": { "type": "integer" }, "max_prewrite_time": { "type": "integer" }, "max_process_time": { "description": "max process time per sql", "type": "integer" }, "max_processed_keys": { "type": "integer" }, "max_resolve_lock_time": { "type": "integer" }, "max_rocksdb_block_cache_hit_count": { "type": "integer" }, "max_rocksdb_block_read_byte": { "type": "integer" }, "max_rocksdb_block_read_count": { "type": "integer" }, "max_rocksdb_delete_skipped_count": { "description": "RocksDB", "type": "integer" }, "max_rocksdb_key_skipped_count": { "type": "integer" }, "max_ru": { "type": "number" }, "max_time_queued_by_rc": { "type": "number" }, "max_total_keys": { "type": "integer" }, "max_txn_retry": { "type": "integer" }, "max_wait_time": { "description": "max wait time per sql", "type": "integer" }, "max_write_keys": { "type": "integer" }, "max_write_size": { "type": "integer" }, "min_latency": { "type": "integer" }, "plan": { "description": "deprecated, replaced by BinaryPlanText", "type": "string" }, "plan_cache_hits": { "type": "integer" }, "plan_can_be_bound": { "type": "boolean" }, "plan_count": { "type": "integer" }, "plan_digest": { "type": "string" }, "plan_hint": { "type": "string" }, "prev_sample_text": { "type": "string" }, "query_sample_text": { "type": "string" }, "related_schemas": { "description": "Computed fields", "type": "string" }, "resource_group": { "description": "Resource Control", "type": "string" }, "sample_user": { "type": "string" }, "schema_name": { "type": "string" }, "stmt_type": { "type": "string" }, "sum_backoff_times": { "type": "integer" }, "sum_cop_task_num": { "type": "integer" }, "sum_errors": { "type": "integer" }, "sum_ia_read_segment_count": { "description": "IA remote read segment metrics", "type": "integer" }, "sum_ia_remote_read_segment_size": { "type": "integer" }, "sum_ia_remote_read_segment_wait_time": { "type": "number" }, "sum_latency": { "type": "integer" }, "sum_ru": { "type": "number" }, "sum_unpacked_bytes_received_tiflash_cross_zone": { "type": "integer" }, "sum_unpacked_bytes_received_tiflash_total": { "type": "integer" }, "sum_unpacked_bytes_received_tikv_cross_zone": { "type": "integer" }, "sum_unpacked_bytes_received_tikv_total": { "type": "integer" }, "sum_unpacked_bytes_sent_tiflash_cross_zone": { "type": "integer" }, "sum_unpacked_bytes_sent_tiflash_total": { "type": "integer" }, "sum_unpacked_bytes_sent_tikv_cross_zone": { "type": "integer" }, "sum_unpacked_bytes_sent_tikv_total": { "description": "Network Fields", "type": "integer" }, "sum_warnings": { "type": "integer" }, "summary_begin_time": { "type": "integer" }, "summary_end_time": { "type": "integer" }, "table_names": { "type": "string" } } }, "topology.AlertManagerInfo": { "type": "object", "properties": { "ip": { "type": "string" }, "port": { "type": "integer" } } }, "topology.GrafanaInfo": { "type": "object", "properties": { "ip": { "type": "string" }, "port": { "type": "integer" } } }, "topology.PDInfo": { "type": "object", "properties": { "deploy_path": { "type": "string" }, "git_hash": { "type": "string" }, "ip": { "type": "string" }, "port": { "type": "integer" }, "start_timestamp": { "description": "Ts = 0 means unknown", "type": "integer" }, "status": { "type": "integer" }, "version": { "type": "string" } } }, "topology.SchedulingInfo": { "type": "object", "properties": { "deploy_path": { "type": "string" }, "git_hash": { "type": "string" }, "ip": { "type": "string" }, "port": { "type": "integer" }, "start_timestamp": { "type": "integer" }, "status": { "type": "integer" }, "version": { "type": "string" } } }, "topology.StoreInfo": { "type": "object", "properties": { "deploy_path": { "type": "string" }, "git_hash": { "type": "string" }, "ip": { "type": "string" }, "labels": { "type": "object", "additionalProperties": { "type": "string" } }, "port": { "type": "integer" }, "start_timestamp": { "type": "integer" }, "status": { "type": "integer" }, "status_port": { "type": "integer" }, "version": { "type": "string" } } }, "topology.StoreLabels": { "type": "object", "properties": { "address": { "type": "string" }, "labels": { "type": "object", "additionalProperties": { "type": "string" } } } }, "topology.StoreLocation": { "type": "object", "properties": { "location_labels": { "type": "array", "items": { "type": "string" } }, "stores": { "type": "array", "items": { "$ref": "#/definitions/topology.StoreLabels" } } } }, "topology.TSOInfo": { "type": "object", "properties": { "deploy_path": { "type": "string" }, "git_hash": { "type": "string" }, "ip": { "type": "string" }, "port": { "type": "integer" }, "start_timestamp": { "type": "integer" }, "status": { "type": "integer" }, "version": { "type": "string" } } }, "topology.TiCDCInfo": { "type": "object", "properties": { "cluster_name": { "type": "string" }, "deploy_path": { "type": "string" }, "git_hash": { "type": "string" }, "ip": { "type": "string" }, "port": { "type": "integer" }, "start_timestamp": { "type": "integer" }, "status": { "type": "integer" }, "status_port": { "type": "integer" }, "version": { "type": "string" } } }, "topology.TiDBInfo": { "type": "object", "properties": { "deploy_path": { "type": "string" }, "git_hash": { "type": "string" }, "ip": { "type": "string" }, "port": { "type": "integer" }, "start_timestamp": { "type": "integer" }, "status": { "type": "integer" }, "status_port": { "type": "integer" }, "version": { "type": "string" } } }, "topology.TiProxyInfo": { "type": "object", "properties": { "deploy_path": { "type": "string" }, "git_hash": { "type": "string" }, "ip": { "type": "string" }, "port": { "type": "integer" }, "start_timestamp": { "type": "integer" }, "status": { "type": "integer" }, "status_port": { "type": "integer" }, "version": { "type": "string" } } }, "topsql.EditableConfig": { "type": "object", "properties": { "enable": { "type": "boolean" } } }, "topsql.InstanceItem": { "type": "object", "properties": { "instance": { "type": "string" }, "instance_type": { "type": "string" } } }, "topsql.InstanceResponse": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/topsql.InstanceItem" } } } }, "topsql.SummaryByItem": { "type": "object", "properties": { "cpu_time_ms": { "type": "array", "items": { "type": "integer" } }, "cpu_time_ms_sum": { "type": "integer" }, "logical_io_bytes": { "type": "array", "items": { "type": "integer" } }, "logical_io_bytes_sum": { "type": "integer" }, "network_bytes": { "type": "array", "items": { "type": "integer" } }, "network_bytes_sum": { "type": "integer" }, "text": { "type": "string" }, "timestamp_sec": { "type": "array", "items": { "type": "integer" } } } }, "topsql.SummaryItem": { "type": "object", "properties": { "cpu_time_ms": { "type": "integer" }, "duration_per_exec_ms": { "type": "number" }, "exec_count_per_sec": { "type": "number" }, "is_other": { "type": "boolean" }, "logical_io_bytes": { "type": "integer" }, "network_bytes": { "type": "integer" }, "plans": { "type": "array", "items": { "$ref": "#/definitions/topsql.SummaryPlanItem" } }, "scan_indexes_per_sec": { "type": "number" }, "scan_records_per_sec": { "type": "number" }, "sql_digest": { "type": "string" }, "sql_text": { "type": "string" } } }, "topsql.SummaryPlanItem": { "type": "object", "properties": { "cpu_time_ms": { "type": "array", "items": { "type": "integer" } }, "duration_per_exec_ms": { "type": "number" }, "exec_count_per_sec": { "type": "number" }, "logical_io_bytes": { "type": "array", "items": { "type": "integer" } }, "network_bytes": { "type": "array", "items": { "type": "integer" } }, "plan_digest": { "type": "string" }, "plan_text": { "type": "string" }, "scan_indexes_per_sec": { "type": "number" }, "scan_records_per_sec": { "type": "number" }, "timestamp_sec": { "type": "array", "items": { "type": "integer" } } } }, "topsql.SummaryResponse": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/topsql.SummaryItem" } }, "data_by": { "type": "array", "items": { "$ref": "#/definitions/topsql.SummaryByItem" } } } }, "topsql.TikvNetworkIoCollectionConfig": { "type": "object", "properties": { "enable": { "type": "boolean" }, "is_multi_value": { "type": "boolean" } } }, "topsql.UpdateTikvNetworkIoCollectionResponse": { "type": "object", "properties": { "warnings": { "type": "array", "items": { "$ref": "#/definitions/rest.ErrorResponse" } } } }, "user.AuthenticateForm": { "type": "object", "properties": { "extra": { "description": "FIXME: Use strong type", "type": "string" }, "password": { "type": "string" }, "type": { "type": "integer", "example": 0 }, "username": { "description": "Does not present for AuthTypeSharingCode", "type": "string", "example": "root" } } }, "user.GetLoginInfoResponse": { "type": "object", "properties": { "sql_auth_public_key": { "type": "string" }, "supported_auth_types": { "type": "array", "items": { "type": "integer" } } } }, "user.SignOutInfo": { "type": "object", "properties": { "end_session_url": { "type": "string" } } }, "user.TokenResponse": { "type": "object", "properties": { "expire": { "type": "string" }, "token": { "type": "string" } } }, "version.Info": { "type": "object", "properties": { "build_git_hash": { "type": "string" }, "build_time": { "type": "string" }, "internal_version": { "type": "string" }, "pd_version": { "type": "string" }, "standalone": { "type": "string" } } } }, "securityDefinitions": { "JwtAuth": { "type": "apiKey", "name": "Authorization", "in": "header" } } } ================================================ FILE: ui/packages/tidb-dashboard-client/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", "declaration": true } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/.gitignore ================================================ /public/speedscope ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/builder.js ================================================ const path = require('path') const fs = require('fs-extra') const chalk = require('chalk') const { watch } = require('chokidar') const { build } = require('esbuild') const postCssPlugin = require('@baurine/esbuild-plugin-postcss3') const autoprefixer = require('autoprefixer') const { yamlPlugin } = require('esbuild-plugin-yaml') const { lessModifyVars, lessGlobalVars } = require('../../less-vars') const isDev = process.env.NODE_ENV !== 'production' // load env const dotenv = require('dotenv') const envFile = isDev ? './.env.development' : './.env.production' dotenv.config({ path: path.resolve(process.cwd(), envFile) }) if (isDev && fs.pathExistsSync(path.resolve(process.cwd(), '.env.local'))) { dotenv.config({ path: '.env.local', override: true }) } const outDir = 'dist' const targetVariantDashboardPath = process.env.TARGET_VARIANT_DASHBOARD_PATH function genDefine() { const define = {} for (const k in process.env) { if (k.startsWith('REACT_APP_')) { let envVal = process.env[k] // Example: REACT_APP_VERSION=$npm_package_version // Expect output: REACT_APP_VERSION=0.1.0 if (envVal.startsWith('$')) { envVal = process.env[envVal.substring(1)] } define[`process.env.${k}`] = JSON.stringify(envVal) } } return define } // customized plugin: log time const logTime = (_options = {}) => ({ name: 'logTime', setup(build) { let time build.onStart(() => { time = new Date() console.log(`Build started`) }) build.onEnd(() => { console.log(`Build ended: ${chalk.yellow(`${new Date() - time}ms`)}`) }) } }) const esbuildParams = { color: true, entryPoints: { dashboardApp: 'src/dashboardApp/index.ts', diagnoseReport: 'src/diagnoseReportApp/index.tsx' }, outdir: outDir, minify: !isDev, format: 'esm', bundle: true, sourcemap: true, logLevel: 'error', incremental: true, // splitting: true, platform: 'browser', plugins: [ postCssPlugin.default({ lessOptions: { modifyVars: lessModifyVars, globalVars: lessGlobalVars, javascriptEnabled: true }, enableCache: true, plugins: [autoprefixer] }), yamlPlugin(), logTime() ], define: genDefine(), inject: ['./process-shim.js'] // fix runtime crash } function buildHtml(inputFilename, outputFilename) { let result = fs.readFileSync(inputFilename).toString() // replace TIME_PLACE_HOLDER const nowTime = new Date().valueOf() result = result.replace(new RegExp(`%TIME_PLACE_HOLDER%`, 'g'), nowTime) fs.writeFileSync(outputFilename, result) } function handleAssets() { fs.copySync('./public', `./${outDir}`) buildHtml('./public/index.html', `./${outDir}/index.html`) } function copyAssets() { if (!fs.existsSync(targetVariantDashboardPath)) { console.log( `target variant dashboard path ${targetVariantDashboardPath} doesn't exist, ignore` ) return } fs.removeSync(targetVariantDashboardPath) fs.copySync(`./${outDir}`, targetVariantDashboardPath) console.log('copy dashboard to target variant') } async function main() { fs.removeSync(`./${outDir}`) const builder = await build(esbuildParams) handleAssets() function rebuild() { builder .rebuild() .then(() => { copyAssets() }) .catch((err) => console.log(err)) } if (isDev) { copyAssets() watch(`src/**/*`, { ignoreInitial: true }).on('all', () => { rebuild() }) watch('public/**/*', { ignoreInitial: true }).on('all', () => { handleAssets() copyAssets() }) // watch "node_modules/@pingcap/tidb-dashboard-lib/dist/**/*" triggers too many rebuild // so we just watch index.js to refine the experience watch('node_modules/@pingcap/tidb-dashboard-lib/dist/index.js', { ignoreInitial: true }).on('all', () => { rebuild() }) } else { process.exit(0) } } main() ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/gulpfile.js ================================================ const { task, series, parallel } = require('gulp') const shell = require('gulp-shell') task( 'speedscope:copy', shell.task( 'mkdir -p public/speedscope && cp node_modules/@duorou_xu/speedscope/dist/release/* public/speedscope/' ) ) task('tsc:watch', shell.task('tsc --watch')) task('tsc:check', shell.task('tsc')) // https://www.npmjs.com/package/eslint-watch task('lint:watch', shell.task('esw --watch --cache --ext .tsx,.ts .')) task('lint:check', shell.task('esw --cache --ext tsx,ts .')) task('esbuild:dev', shell.task('NODE_ENV=development node builder.js')) task('esbuild:build', shell.task('NODE_ENV=production node builder.js')) task( 'dev', series('speedscope:copy', parallel('tsc:watch', 'lint:watch', 'esbuild:dev')) ) task( 'build', series( 'speedscope:copy', parallel('tsc:check', 'lint:check', 'esbuild:build') ) ) ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/package.json ================================================ { "name": "@pingcap/tidb-dashboard-for-clinic-cloud", "version": "0.0.56", "main": "dist/dashboardApp.js", "module": "dist/dashboardApp.js", "files": [ "dist/*.js", "dist/*.css", "dist/*.map", "dist/speedscope/*.js", "dist/speedscope/*.css", "dist/speedscope/*.map", "dist/speedscope/*.png", "dist/speedscope/*.txt", "package.json", "README.md" ], "scripts": { "dev": "gulp dev", "build": "gulp build" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@ant-design/icons": "^4.7.0", "@duorou_xu/speedscope": "1.14.1", "@fortawesome/fontawesome-free": "^6.1.1", "@g07cha/flexbox-react": "^5.0.0", "@pingcap/tidb-dashboard-client": "workspace:^1.0.0", "@pingcap/tidb-dashboard-lib": "workspace:^1.0.0", "ahooks": "^3.1.9", "antd": "^4.18.7", "axios": "^1.12.0", "bulma": "^0.9.4", "classnames": "^2.3.1", "compare-versions": "^5.0.1", "eventemitter2": "^6.4.5", "i18next": "^23.7.11", "nprogress": "^0.2.0", "rc-animate": "^3.1.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-i18next": "^11.15.4", "react-markdown": "^8.0.3", "react-router-dom": "6", "react-spring": "^8.0.27", "react-use": "^15.3.3", "single-spa": "^5.9.4", "single-spa-react": "^4.6.1" }, "devDependencies": { "@baurine/esbuild-plugin-babel": "^0.3.0", "@baurine/esbuild-plugin-postcss3": "^0.4.3", "@cypress/code-coverage": "^3.9.12", "@cypress/skip-test": "^2.6.1", "@types/node": "^16.9.1", "@types/react": "^17.0.20", "@types/react-dom": "^17.0.9", "autoprefixer": "^10.4.2", "babel-plugin-istanbul": "^6.1.1", "chalk": "4.1.2", "chokidar": "^3.5.2", "clipboardy": "2.3.0", "cypress": "8.5.0", "cypress-image-snapshot": "^4.0.1", "cypress-real-events": "^1.7.0", "dayjs": "^1.10.8", "dotenv": "^16.0.1", "esbuild": "^0.14.23", "esbuild-plugin-svgr": "^1.0.0", "esbuild-plugin-yaml": "^0.0.1", "eslint-plugin-cypress": "^2.12.1", "eslint-watch": "^8.0.0", "fs-extra": "^10.0.0", "gulp": "^4.0.2", "gulp-shell": "^0.8.0", "http-proxy-middleware": "^2.0.6", "live-server": "^1.2.1", "neat-csv": "5.1.0", "typescript": "^4.7.3" } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/process-shim.js ================================================ export let process = { // cwd: () => '', env: {} // to avoid `process.env` undefined in runtime } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/public/diagnose-report/index.html ================================================
================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/public/index.html ================================================

It may take a bit long time to load for the first time, due to download some js files.

================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/public/ngm.html ================================================

It may take a bit long time to load for the first time, due to download some js files.

================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/ClusterInfo/context.ts ================================================ import { IClusterInfoDataSource, IClusterInfoContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' class DataSource implements IClusterInfoDataSource { clusterInfoGetHostsInfo(options?: ReqConfig) { return client.getInstance().clusterInfoGetHostsInfo(options) } getStoreLocationTopology(options?: ReqConfig) { return client.getInstance().getStoreLocationTopology(options) } getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } getTiCDCTopology(options?: ReqConfig) { return client.getInstance().getTiCDCTopology(options) } getTiProxyTopology(options?: ReqConfig) { return client.getInstance().getTiProxyTopology(options) } getTSOTopology(options?: ReqConfig) { return client.getInstance().getTSOTopology(options) } getSchedulingTopology(options?: ReqConfig) { return client.getInstance().getSchedulingTopology(options) } topologyTidbAddressDelete(address: string, options?: ReqConfig) { return client.getInstance().topologyTidbAddressDelete({ address }, options) } clusterInfoGetStatistics(options?: ReqConfig) { return client.getInstance().clusterInfoGetStatistics(options) } } const ds = new DataSource() export const ctx: IClusterInfoContext = { ds, cfg: { apiPathBase: client.getBasePath() } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/ClusterInfo/index.tsx ================================================ import React from 'react' import { ClusterInfoApp, ClusterInfoProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/ClusterInfo/meta.ts ================================================ import { ClusterOutlined } from '@ant-design/icons' export default { id: 'cluster_info', routerPrefix: '/cluster_info', icon: ClusterOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Configuration/context.ts ================================================ import { IConfigurationDataSource, IConfigurationContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { ConfigurationEditRequest } from '~/client' class DataSource implements IConfigurationDataSource { configurationEdit(request: ConfigurationEditRequest, options?: ReqConfig) { return client.getInstance().configurationEdit({ request }, options) } configurationGetAll(options?: ReqConfig) { return client.getInstance().configurationGetAll(options) } } const ds = new DataSource() export const ctx: IConfigurationContext = { ds } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Configuration/index.tsx ================================================ import React from 'react' import { ConfigurationApp, ConfigurationProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Configuration/meta.ts ================================================ import { ToolOutlined } from '@ant-design/icons' export default { id: 'configuration', routerPrefix: '/configuration', icon: ToolOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/ContinuousProfiling/context.ts ================================================ import { IConProfilingDataSource, IConProfilingContext, ReqConfig, IConProfilingConfig } from '@pingcap/tidb-dashboard-lib' import client, { ConprofNgMonitoringConfig } from '~/client' import publicPathBase from '~/utils/publicPathPrefix' class DataSource implements IConProfilingDataSource { private headers: {} = {} constructor(cfg: Partial) { this.headers = cfg.deployType === 'nextgen-host' ? { 'x-cluster-id': cfg.clusterId, 'x-deploy-type': 'premium' } : {} } continuousProfilingActionTokenGet(q: string, options?: ReqConfig) { return client .getInstance() .continuousProfilingActionTokenGet( { q }, { headers: this.headers, ...options } ) } continuousProfilingComponentsGet(options?: ReqConfig) { return client .getInstance() .continuousProfilingComponentsGet({ headers: this.headers, ...options }) } continuousProfilingConfigGet(options?: ReqConfig) { return client .getInstance() .continuousProfilingConfigGet({ headers: this.headers, ...options }) } continuousProfilingConfigPost( request: ConprofNgMonitoringConfig, options?: ReqConfig ) { return client .getInstance() .continuousProfilingConfigPost( { request }, { headers: this.headers, ...options } ) } continuousProfilingDownloadGet(ts: number, options?: ReqConfig) { return client .getInstance() .continuousProfilingDownloadGet( { ts }, { headers: this.headers, ...options } ) } continuousProfilingEstimateSizeGet(options?: ReqConfig) { return client .getInstance() .continuousProfilingEstimateSizeGet({ headers: this.headers, ...options }) } continuousProfilingGroupProfileDetailGet(ts: number, options?: ReqConfig) { return client .getInstance() .continuousProfilingGroupProfileDetailGet( { ts }, { headers: this.headers, ...options } ) } continuousProfilingGroupProfilesGet( beginTime?: number, endTime?: number, options?: ReqConfig ) { return client .getInstance() .continuousProfilingGroupProfilesGet( { beginTime, endTime }, { headers: this.headers, ...options } ) } continuousProfilingSingleProfileViewGet( address?: string, component?: string, profileType?: string, ts?: number, options?: ReqConfig ) { return client .getInstance() .continuousProfilingSingleProfileViewGet( { address, component, profileType, ts }, { headers: this.headers, ...options } ) } getTiDBTopology(options?: ReqConfig) { return client .getInstance() .getTiDBTopology({ headers: this.headers, ...options }) } getStoreTopology(options?: ReqConfig) { return client .getInstance() .getStoreTopology({ headers: this.headers, ...options }) } getPDTopology(options?: ReqConfig) { return client .getInstance() .getPDTopology({ headers: this.headers, ...options }) } } export const ctx: ( cfg: Partial ) => IConProfilingContext = (cfg) => ({ ds: new DataSource(cfg), cfg: { apiPathBase: client.getBasePath(), publicPathBase, ...cfg } }) ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/ContinuousProfiling/index.tsx ================================================ import React from 'react' import { ConProfilingApp, ConProfilingProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' import { getGlobalConfig } from '~/utils/globalConfig' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/ContinuousProfiling/meta.ts ================================================ import { AimOutlined } from '@ant-design/icons' export default { id: 'conprof', routerPrefix: '/continuous_profiling', icon: AimOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Deadlock/context.ts ================================================ import { IDeadlockDataSource, IDeadlockContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' class DataSource implements IDeadlockDataSource { deadlockListGet(options?: ReqConfig) { return client.getInstance().deadlockListGet(options) } } const ds = new DataSource() export const ctx: IDeadlockContext = { ds } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Deadlock/index.tsx ================================================ import React from 'react' import { DeadlockApp, DeadlockProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Deadlock/meta.ts ================================================ import { SyncOutlined } from '@ant-design/icons' export default { id: 'deadlock', routerPrefix: '/deadlock', icon: SyncOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/DebugAPI/context.ts ================================================ import { IDebugAPIDataSource, IDebugAPIContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { EndpointRequestPayload } from '~/client' class DataSource implements IDebugAPIDataSource { debugAPIGetEndpoints(options?: ReqConfig) { return client.getInstance().debugAPIGetEndpoints(options) } debugAPIRequestEndpoint(req: EndpointRequestPayload, options?: ReqConfig) { return client.getInstance().debugAPIRequestEndpoint( { req: { ...req, // To compatible with the old tidb-dashboard backend api before 5.4.0 // By PR https://github.com/pingcap/tidb-dashboard/pull/1103 (release to v2021.12.30.1 and PD 5.4.0) // It changes `id` to `api_id`, `params` to `param_values` id: req.api_id, params: req.param_values } as any }, options ) } infoListDatabases(options?: ReqConfig) { return client.getInstance().infoListDatabases(options) } infoListTables(databaseName?: string, options?: ReqConfig) { return client.getInstance().infoListTables({ databaseName }, options) } getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } getTiProxyTopology(options?: ReqConfig) { return client.getInstance().getTiProxyTopology(options) } } const ds = new DataSource() export const ctx: IDebugAPIContext = { ds, cfg: { apiPathBase: client.getBasePath() } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/DebugAPI/index.tsx ================================================ import React from 'react' import { DebugAPIApp, DebugAPIProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/DebugAPI/meta.ts ================================================ import { ApiOutlined } from '@ant-design/icons' export default { id: 'debug_api', routerPrefix: '/debug_api', icon: ApiOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Diagnose/context.ts ================================================ import { IDiagnoseDataSource, IDiagnoseContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { DiagnoseGenDiagnosisReportRequest } from '~/client' class DataSource implements IDiagnoseDataSource { diagnoseDiagnosisPost( request: DiagnoseGenDiagnosisReportRequest, options?: ReqConfig ) { return client.getInstance().diagnoseDiagnosisPost({ request }, options) } } const ds = new DataSource() export const ctx: IDiagnoseContext = { ds } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Diagnose/index.tsx ================================================ import React from 'react' import { DiagnoseApp, DiagnoseProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Diagnose/meta.ts ================================================ import { SafetyCertificateOutlined } from '@ant-design/icons' export default { id: 'diagnose', routerPrefix: '/diagnose', icon: SafetyCertificateOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/InstanceProfiling/context.ts ================================================ import { IInstanceProfilingDataSource, IInstanceProfilingContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { ProfilingStartRequest } from '~/client' import publicPathBase from '~/utils/publicPathPrefix' class DataSource implements IInstanceProfilingDataSource { getActionToken(id?: string, action?: string, options?: ReqConfig) { return client.getInstance().getActionToken({ id, action }, options) } getProfilingGroupDetail(groupId: string, options?: ReqConfig) { return client.getInstance().getProfilingGroupDetail({ groupId }, options) } getProfilingGroups(options?: ReqConfig) { return client.getInstance().getProfilingGroups(options) } startProfiling(req: ProfilingStartRequest, options?: ReqConfig) { return client.getInstance().startProfiling({ req }, options) } continuousProfilingConfigGet(options?: ReqConfig) { return client.getInstance().continuousProfilingConfigGet(options) } getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } getTiCDCTopology(options?: ReqConfig) { return client.getInstance().getTiCDCTopology(options) } getTiProxyTopology(options?: ReqConfig) { return client.getInstance().getTiProxyTopology(options) } getTSOTopology(options?: ReqConfig) { return client.getInstance().getTSOTopology(options) } getSchedulingTopology(options?: ReqConfig) { return client.getInstance().getSchedulingTopology(options) } } const ds = new DataSource() export const ctx: IInstanceProfilingContext = { ds, cfg: { apiPathBase: client.getBasePath(), publicPathBase } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/InstanceProfiling/index.tsx ================================================ import React from 'react' import { InstanceProfilingApp, InstanceProfilingProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/InstanceProfiling/meta.ts ================================================ import { AimOutlined } from '@ant-design/icons' export default { id: 'instance_profiling', routerPrefix: '/instance_profiling', icon: AimOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/KeyViz/context.ts ================================================ import { IKeyVizDataSource, IKeyVizContext, ReqConfig, IKeyVizConfig } from '@pingcap/tidb-dashboard-lib' import client, { ConfigKeyVisualConfig } from '~/client' class DataSource implements IKeyVizDataSource { keyvisualConfigGet(options?: ReqConfig) { return client.getInstance().keyvisualConfigGet(options) } keyvisualConfigPut(request: ConfigKeyVisualConfig, options?: ReqConfig) { return client.getInstance().keyvisualConfigPut({ request }, options) } keyvisualHeatmapsGet( startkey?: string, endkey?: string, starttime?: number, endtime?: number, type?: | 'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration', options?: ReqConfig ) { return client.getInstance().keyvisualHeatmapsGet( { startkey, endkey, starttime, type }, options ) } } const ds = new DataSource() export function ctx(cfg: Partial): IKeyVizContext { return { ds, cfg: { showHelp: true, showSetting: true, ...cfg } } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/KeyViz/index.tsx ================================================ import React from 'react' import { KeyVizApp, KeyVizProvider } from '@pingcap/tidb-dashboard-lib' import { getGlobalConfig } from '~/utils/globalConfig' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/KeyViz/meta.ts ================================================ import { EyeOutlined } from '@ant-design/icons' export default { id: 'keyviz', routerPrefix: '/keyviz', icon: EyeOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Monitoring/context.ts ================================================ import { IMonitoringDataSource, IMonitoringContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' import { getMonitoringItems } from './metricsQueries' class DataSource implements IMonitoringDataSource { metricsQueryGet(params: { endTimeSec?: number query?: string startTimeSec?: number stepSec?: number }) { return client .getInstance() .metricsQueryGet(params, { handleError: 'custom' } as ReqConfig) .then((res) => res.data) } } const RECENT_SECONDS = [ 5 * 60, 15 * 60, 30 * 60, 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60, 2 * 24 * 60 * 60 ] const ds = new DataSource() export const ctx: IMonitoringContext = { ds, cfg: { getMetricsQueries: (pdVersion: string | undefined) => getMonitoringItems(pdVersion), timeRangeSelector: { recent_seconds: RECENT_SECONDS, customAbsoluteRangePicker: true } } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Monitoring/index.tsx ================================================ import React from 'react' import { MonitoringApp, MonitoringProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Monitoring/meta.ts ================================================ import { LineChartOutlined } from '@ant-design/icons' export default { id: 'monitoring', routerPrefix: '/monitoring', icon: LineChartOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Monitoring/metricsQueries.ts ================================================ import { ColorType, TransformNullValue, MetricsQueryType } from '@pingcap/tidb-dashboard-lib' import { compare } from 'compare-versions' function transformColorBySQLType(legendLabel: string) { switch (legendLabel) { case 'Select': return ColorType.BLUE_3 case 'Commit': return ColorType.GREEN_2 case 'Insert': return ColorType.GREEN_3 case 'Update': return ColorType.GREEN_4 case 'general': return ColorType.PINK default: return undefined } } function transformColorByExecTimeOverview(legendLabel: string) { switch (legendLabel) { case 'tso_wait': return ColorType.RED_5 case 'Commit': return ColorType.GREEN_4 case 'Prewrite': return ColorType.GREEN_3 case 'PessimisticLock': return ColorType.RED_4 case 'Get': return ColorType.BLUE_3 case 'BatchGet': return ColorType.BLUE_4 case 'Cop': return ColorType.BLUE_1 case 'ScanLock': case 'Scan': return ColorType.PURPLE case 'execute time': return ColorType.YELLOW default: return undefined } } const getMonitoringItems = ( pdVersion: string | undefined ): MetricsQueryType[] => { function loadTiKVStoragePromql() { const PDVersion = pdVersion?.replace('v', '') if (PDVersion && PDVersion !== 'N/A' && compare(PDVersion, '5.4.1', '<')) { return 'sum(tikv_engine_size_bytes) by (instance)' } return 'sum(tikv_store_size_bytes{type="used"}) by (instance)' } const monitoringItems: MetricsQueryType[] = [ { category: 'database_time', metrics: [ { title: 'Database Time by SQL Types', queries: [ { promql: `sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval]))`, name: 'database time', color: ColorType.YELLOW, type: 'line' }, { promql: `sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval])) by (sql_type)`, name: '{sql_type}', color: (seriesName: string) => transformColorBySQLType(seriesName), type: 'bar_stacked' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Database Time by SQL Phase', queries: [ { promql: `sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval]))`, name: 'database time', color: ColorType.YELLOW, type: 'line' }, { promql: `sum(rate(tidb_session_parse_duration_seconds_sum{sql_type="general"}[$__rate_interval]))`, name: 'parse', color: ColorType.RED_2, type: 'bar_stacked' }, { promql: `sum(rate(tidb_session_compile_duration_seconds_sum{sql_type="general"}[$__rate_interval]))`, name: 'compile', color: ColorType.ORANGE, type: 'bar_stacked' }, { promql: `sum(rate(tidb_session_execute_duration_seconds_sum{sql_type="general"}[$__rate_interval]))`, name: 'execute', color: ColorType.GREEN_3, type: 'bar_stacked' }, { promql: `sum(rate(tidb_server_get_token_duration_seconds_sum[$__rate_interval]))/1000000`, name: 'get token', color: ColorType.RED_3, type: 'bar_stacked' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'SQL Execute Time Overview', queries: [ { promql: 'sum(rate(tidb_tikvclient_request_seconds_sum{store!="0"}[$__rate_interval])) by (type)', name: '{type}', color: (seriesName: string) => transformColorByExecTimeOverview(seriesName), type: 'bar_stacked' }, { promql: 'sum(rate(pd_client_cmd_handle_cmds_duration_seconds_sum{type="wait"}[$__rate_interval]))', name: 'tso_wait', color: ColorType.RED_5, type: 'bar_stacked' } ], unit: 's' } ] }, { category: 'application_connection', metrics: [ { title: 'Connection Count', queries: [ { promql: 'sum(tidb_server_connections)', name: 'Total', type: 'line' }, { promql: 'sum(tidb_server_tokens)', name: 'active connections', type: 'line' } ], unit: 'short', nullValue: TransformNullValue.AS_ZERO }, { title: 'Disconnection', queries: [ { promql: 'sum(rate(tidb_server_disconnection_total[$__rate_interval])) by (instance, result)', name: '{instance}-{result}', type: 'area_stack' } ], unit: 'short', nullValue: TransformNullValue.AS_ZERO } ] }, { category: 'sql_count', metrics: [ { title: 'Query Per Second', queries: [ { promql: 'sum(rate(tidb_executor_statement_total[$__rate_interval]))', name: 'Total', type: 'line' }, { promql: 'sum(rate(tidb_executor_statement_total[$__rate_interval])) by (type)', name: '{type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'short' }, { title: 'Failed Queries', queries: [ { promql: 'sum(rate(tidb_server_execute_error_total[$__rate_interval]))', name: '{type} @ {instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'short' }, { title: 'Command Per Second', queries: [ { promql: 'sum(rate(tidb_server_query_total[$__rate_interval])) by (type)', name: '{type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'short' }, { title: 'Queries Using Plan Cache OPS', queries: [ { promql: 'sum(rate(tidb_server_plan_cache_total[$__rate_interval]))', name: 'avg - hit', type: 'line' }, { promql: 'sum(rate(tidb_server_plan_cache_miss_total[$__rate_interval]))', name: 'avg - miss', type: 'line' } ], unit: 'short', nullValue: TransformNullValue.AS_ZERO } ] }, { category: 'latency_break_down', metrics: [ { title: 'Query Duration', queries: [ { promql: 'sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval])) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!="internal"}[$__rate_interval]))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{sql_type!="internal"}[$__rate_interval])) by (le))', name: '99', type: 'line' }, { promql: 'sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval])) by (sql_type) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!="internal"}[$__rate_interval])) by (sql_type)', name: 'avg-{sql_type}', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket[$__rate_interval])) by (le,sql_type))', name: '99-{sql_type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average Idle Connection Duration', queries: [ { promql: `(sum(rate(tidb_server_conn_idle_duration_seconds_sum{in_txn='1'}[$__rate_interval])) / sum(rate(tidb_server_conn_idle_duration_seconds_count{in_txn='1'}[$__rate_interval])))`, name: 'avg-in-txn', type: 'line' }, { promql: `(sum(rate(tidb_server_conn_idle_duration_seconds_sum{in_txn='0'}[$__rate_interval])) / sum(rate(tidb_server_conn_idle_duration_seconds_count{in_txn='0'}[$__rate_interval])))`, name: 'avg-not-in-txn', type: 'line' } ], unit: 's', nullValue: TransformNullValue.AS_ZERO }, { title: 'Get Token Duration', queries: [ { promql: 'sum(rate(tidb_server_get_token_duration_seconds_sum[$__rate_interval])) / sum(rate(tidb_server_get_token_duration_seconds_count[$__rate_interval]))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_server_get_token_duration_seconds_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'µs' }, { title: 'Parse Duration', queries: [ { promql: '(sum(rate(tidb_session_parse_duration_seconds_sum{sql_type="general"}[$__rate_interval])) / sum(rate(tidb_session_parse_duration_seconds_count{sql_type="general"}[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_session_parse_duration_seconds_bucket{sql_type="general"}[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Compile Duration', queries: [ { promql: '(sum(rate(tidb_session_compile_duration_seconds_sum{sql_type="general"}[$__rate_interval])) / sum(rate(tidb_session_compile_duration_seconds_count{sql_type="general"}[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_session_compile_duration_seconds_bucket{sql_type="general"}[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Execute Duration', queries: [ { promql: '(sum(rate(tidb_session_execute_duration_seconds_sum{sql_type="general"}[$__rate_interval])) / sum(rate(tidb_session_execute_duration_seconds_count{sql_type="general"}[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_session_execute_duration_seconds_bucket{sql_type="general"}[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' } ] }, { category: 'transaction', metrics: [ { title: 'Transaction Per Second', queries: [ { promql: 'sum(rate(tidb_session_transaction_duration_seconds_count{scope=~"general"}[$__rate_interval])) by (type, txn_mode)', name: '{type}-{txn_mode}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'short' }, { title: 'Transaction Duration', queries: [ { promql: 'sum(rate(tidb_session_transaction_duration_seconds_sum{scope=~"general"}[$__rate_interval])) by (txn_mode)/ sum(rate(tidb_session_transaction_duration_seconds_count{scope=~"general"}[$__rate_interval])) by (txn_mode)', name: 'avg-{txn_mode}', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_session_transaction_duration_seconds_bucket[$__rate_interval])) by (le, txn_mode))', name: '99-{txn_mode}', type: 'line' } ], unit: 's' } ] }, { category: 'core_path_duration', metrics: [ { title: 'Avg TiDB KV Request Duration', queries: [ { promql: 'sum(rate(tidb_tikvclient_request_seconds_sum{store!="0"}[$__rate_interval])) by (type)/ sum(rate(tidb_tikvclient_request_seconds_count{store!="0"}[$__rate_interval])) by (type)', name: '{type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Avg TiKV GRPC Duration', queries: [ { promql: 'sum(rate(tikv_grpc_msg_duration_seconds_sum{store!="0"}[$__rate_interval])) by (type)/ sum(rate(tikv_grpc_msg_duration_seconds_count{store!="0"}[$__rate_interval])) by (type)', name: '{type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 PD TSO Wait/RPC Duration', queries: [ { promql: '(sum(rate(pd_client_cmd_handle_cmds_duration_seconds_sum{type="wait"}[$__rate_interval])) / sum(rate(pd_client_cmd_handle_cmds_duration_seconds_count{type="wait"}[$__rate_interval])))', name: 'wait - avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(pd_client_cmd_handle_cmds_duration_seconds_bucket{type="wait"}[$__rate_interval])) by (le))', name: 'wait - 99', type: 'line' }, { promql: '(sum(rate(pd_client_request_handle_requests_duration_seconds_sum{type="tso"}[$__rate_interval])) / sum(rate(pd_client_request_handle_requests_duration_seconds_count{type="tso"}[$__rate_interval])))', name: 'rpc - avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(pd_client_request_handle_requests_duration_seconds_bucket{type="tso"}[$__rate_interval])) by (le))', name: 'rpc - 99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Storage Async Write Duration', queries: [ { promql: 'sum(rate(tikv_storage_engine_async_request_duration_seconds_sum{type="write"}[$__rate_interval])) / sum(rate(tikv_storage_engine_async_request_duration_seconds_count{type="write"}[$__rate_interval]))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_storage_engine_async_request_duration_seconds_bucket{type="write"}[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Store Duration', queries: [ { promql: 'sum(rate(tikv_raftstore_store_duration_secs_sum[$__rate_interval])) / sum(rate(tikv_raftstore_store_duration_secs_count[$__rate_interval]))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_raftstore_store_duration_secs_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Apply Duration', queries: [ { promql: '(sum(rate(tikv_raftstore_apply_duration_secs_sum[$__rate_interval])) / sum(rate(tikv_raftstore_apply_duration_secs_count[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_raftstore_apply_duration_secs_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Append Log Duration', queries: [ { promql: '(sum(rate(tikv_raftstore_append_log_duration_seconds_sum[$__rate_interval])) / sum(rate(tikv_raftstore_append_log_duration_seconds_count[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_raftstore_append_log_duration_seconds_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Commit Log Duration', queries: [ { promql: '(sum(rate(tikv_raftstore_commit_log_duration_seconds_sum[$__rate_interval])) / sum(rate(tikv_raftstore_commit_log_duration_seconds_count[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_raftstore_commit_log_duration_seconds_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Apply Log Duration', queries: [ { promql: '(sum(rate(tikv_raftstore_apply_log_duration_seconds_sum[$__rate_interval])) / sum(rate(tikv_raftstore_apply_log_duration_seconds_count[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_raftstore_apply_log_duration_seconds_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' } ] }, { category: 'server', metrics: [ { title: 'TiDB Uptime', queries: [ { promql: '(time() - process_start_time_seconds{component="tidb"})', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'TiDB CPU Usage', queries: [ { promql: 'irate(process_cpu_seconds_total{component="tidb"}[$__rate_interval])', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'percentunit' }, { title: 'TiDB Memory Usage', queries: [ { promql: 'process_resident_memory_bytes{component="tidb"}', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'bytes' }, { title: 'TiKV Uptime', queries: [ { promql: '(time() - process_start_time_seconds{component="tikv"})', name: '{instance}', type: 'line' } ], unit: 's' }, { title: 'TiKV CPU Usage', queries: [ { promql: 'sum(rate(tikv_thread_cpu_seconds_total[$__rate_interval])) by (instance)', name: '{instance}', type: 'line' } ], unit: 'percentunit' }, { title: 'TiKV Memory Usage', queries: [ { promql: 'avg(process_resident_memory_bytes{component="tikv"}) by (instance)', name: '{instance}', type: 'line' } ], unit: 'bytes' }, { title: 'TiKV IO MBps', queries: [ { promql: 'sum(rate(tikv_engine_flow_bytes{db="kv", type="wal_file_bytes"}[$__rate_interval])) by (instance) + (sum(rate(tikv_engine_flow_bytes{db="raft", type="wal_file_bytes"}[$__rate_interval])) by (instance) or (0 * sum(rate(raft_engine_write_size_sum[$__rate_interval])) by (instance))) + (sum(rate(raft_engine_write_size_sum[$__rate_interval])) by (instance) or (0 * sum(rate(tikv_engine_flow_bytes{db="raft", type="wal_file_bytes"}[$__rate_interval])) by (instance)))', name: '{instance}-write', type: 'line' }, { promql: 'sum(rate(tikv_engine_flow_bytes{db="kv", type=~"bytes_read|iter_bytes_read"}[$__rate_interval])) by (instance)', name: '{instance}-read', type: 'line' } ], unit: 'Bps' }, { title: 'TiKV Storage Usage', queries: [ { promql: loadTiKVStoragePromql(), name: '{instance}', type: 'area_stack' } ], unit: 'bytes' }, { title: 'TiFlash Uptime', queries: [ { promql: 'tiflash_system_asynchronous_metric_Uptime', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'TiFlash CPU Usage', queries: [ { promql: 'rate(tiflash_proxy_process_cpu_seconds_total{component="tiflash"}[$__rate_interval])', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'percentunit' }, { title: 'TiFlash Memory', queries: [ { promql: 'tiflash_proxy_process_resident_memory_bytes{component="tiflash"}', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'bytes' }, { title: 'TiFlash IO MBps', queries: [ { promql: 'sum(rate(tiflash_system_profile_event_WriteBufferFromFileDescriptorWriteBytes[$__rate_interval])) by (instance) + sum(rate(tiflash_system_profile_event_PSMWriteBytes[$__rate_interval])) by (instance) + sum(rate(tiflash_system_profile_event_WriteBufferAIOWriteBytes[$__rate_interval])) by (instance)', name: '{instance}-write', type: 'line' }, { promql: 'sum(rate(tiflash_system_profile_event_ReadBufferFromFileDescriptorReadBytes[$__rate_interval])) by (instance) + sum(rate(tiflash_system_profile_event_PSMReadBytes[$__rate_interval])) by (instance) + sum(rate(tiflash_system_profile_event_ReadBufferAIOReadBytes[$__rate_interval])) by (instance)', name: '{instance}-read', type: 'line' } ], unit: 'Bps' }, { title: 'TiFlash Storage Usage', queries: [ { promql: 'sum(tiflash_system_current_metric_StoreSizeUsed) by (instance)', name: '{instance}', type: 'area_stack' } ], unit: 'bytes' } ] } ] return monitoringItems } export { getMonitoringItems } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/OptimizerTrace/context.ts ================================================ import { IOptimizerTraceDataSource, IOptimizerTraceContext // ReqConfig } from '@pingcap/tidb-dashboard-lib' // import client from '~/client' class DataSource implements IOptimizerTraceDataSource {} const ds = new DataSource() export const ctx: IOptimizerTraceContext = { ds } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/OptimizerTrace/index.tsx ================================================ import React from 'react' import { OptimizerTraceApp, OptimizerTraceProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/OptimizerTrace/meta.ts ================================================ export default { id: 'optimizer_trace', routerPrefix: '/optimizer_trace', reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Overview/context.ts ================================================ import { IOverviewDataSource, IOverviewContext, IOverviewConfig, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' import { overviewMetrics } from './metricsQueries' class DataSource implements IOverviewDataSource { getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } getTiCDCTopology(options?: ReqConfig) { return client.getInstance().getTiCDCTopology(options) } getTiProxyTopology(options?: ReqConfig) { return client.getInstance().getTiProxyTopology(options) } getTSOTopology(options?: ReqConfig) { return client.getInstance().getTSOTopology(options) } getSchedulingTopology(options?: ReqConfig) { return client.getInstance().getSchedulingTopology(options) } metricsQueryGet(params: { endTimeSec?: number query?: string startTimeSec?: number stepSec?: number }) { return client .getInstance() .metricsQueryGet(params, { handleError: 'custom' } as ReqConfig) .then((res) => res.data) } getGrafanaTopology(options?: ReqConfig) { return client.getInstance().getGrafanaTopology(options) } getAlertManagerTopology(options?: ReqConfig) { return client.getInstance().getAlertManagerTopology(options) } getAlertManagerCounts(address: string, options?: ReqConfig) { return client.getInstance().getAlertManagerCounts({ address }, options) } } const ds = new DataSource() const RECENT_SECONDS = [ 5 * 60, 15 * 60, 30 * 60, 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60, 2 * 24 * 60 * 60 ] export const ctx: (cfg: Partial) => IOverviewContext = ( cfg ) => ({ ds, cfg: { apiPathBase: client.getBasePath(), metricsQueries: overviewMetrics, timeRangeSelector: { recent_seconds: RECENT_SECONDS, customAbsoluteRangePicker: true }, showMetrics: false, ...cfg } }) ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Overview/index.tsx ================================================ import React from 'react' import { OverviewApp, OverviewProvider } from '@pingcap/tidb-dashboard-lib' import { getGlobalConfig } from '~/utils/globalConfig' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Overview/meta.ts ================================================ import { AppstoreOutlined } from '@ant-design/icons' export default { id: 'overview', routerPrefix: '/overview', icon: AppstoreOutlined, isDefaultRouter: true, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Overview/metricsQueries.ts ================================================ import { TransformNullValue, OverviewMetricsQueryType } from '@pingcap/tidb-dashboard-lib' const overviewMetrics: OverviewMetricsQueryType[] = [ { title: 'total_requests', queries: [ { promql: 'sum(rate(tidb_executor_statement_total[$__rate_interval]))', name: 'Total', type: 'line' }, { promql: 'sum(rate(tidb_executor_statement_total[$__rate_interval])) by (type)', name: '{type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'short' }, { title: 'latency', queries: [ { promql: 'sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval])) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!="internal"}[$__rate_interval]))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{sql_type!="internal"}[$__rate_interval])) by (le))', name: '99', type: 'line' }, { promql: 'sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval])) by (sql_type) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!="internal"}[$__rate_interval])) by (sql_type)', name: 'avg-{sql_type}', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{sql_type!="internal"}[$__rate_interval])) by (le,sql_type))', name: '99-{sql_type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'cpu', queries: [ { promql: 'irate(process_cpu_seconds_total{component="tidb"}[$__rate_interval])', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'percentunit' }, { title: 'memory', queries: [ { promql: 'process_resident_memory_bytes{component="tidb"}', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'bytes' }, { title: 'io', queries: [ { promql: 'sum(rate(tikv_engine_flow_bytes{db="kv", type="wal_file_bytes"}[$__rate_interval])) by (instance) + (sum(rate(tikv_engine_flow_bytes{db="raft", type="wal_file_bytes"}[$__rate_interval])) by (instance) or (0 * sum(rate(raft_engine_write_size_sum[$__rate_interval])) by (instance))) + (sum(rate(raft_engine_write_size_sum[$__rate_interval])) by (instance) or (0 * sum(rate(tikv_engine_flow_bytes{db="raft", type="wal_file_bytes"}[$__rate_interval])) by (instance)))', name: '{instance}-write', type: 'line' }, { promql: 'sum(rate(tikv_engine_flow_bytes{db="kv", type=~"bytes_read|iter_bytes_read"}[$__rate_interval])) by (instance)', name: '{instance}-read', type: 'line' } ], unit: 'Bps' } ] export { overviewMetrics } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/QueryEditor/context.ts ================================================ import { IQueryEditorDataSource, IQueryEditorContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { QueryeditorRunRequest } from '~/client' class DataSource implements IQueryEditorDataSource { queryEditorRun(request: QueryeditorRunRequest, options?: ReqConfig) { return client.getInstance().queryEditorRun({ request }, options) } } const ds = new DataSource() export const ctx: IQueryEditorContext = { ds } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/QueryEditor/index.tsx ================================================ import React from 'react' import { QueryEditorApp, QueryEditorProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/QueryEditor/meta.ts ================================================ import { ConsoleSqlOutlined } from '@ant-design/icons' export default { id: 'query_editor', routerPrefix: '/query_editor', icon: ConsoleSqlOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/ResourceManager/context-impl.ts ================================================ import { IResourceManagerDataSource, IResourceManagerContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import { AxiosPromise } from 'axios' import client, { ResourcemanagerCalibrateResponse, ResourcemanagerGetConfigResponse, ResourcemanagerResourceInfoRowDef } from '~/client' class DataSource implements IResourceManagerDataSource { getConfig( options?: ReqConfig ): AxiosPromise { return client.getInstance().resourceManagerConfigGet(options) } getInformation( options?: ReqConfig ): AxiosPromise { return client.getInstance().resourceManagerInformationGet(options) } getCalibrateByHardware( params: { workload: string }, options?: ReqConfig | undefined ): AxiosPromise { return client .getInstance() .resourceManagerCalibrateHardwareGet(params, options) } getCalibrateByActual( params: { startTime: number; endTime: number }, options?: ReqConfig | undefined ): AxiosPromise { return client .getInstance() .resourceManagerCalibrateActualGet(params, options) } metricsQueryGet(params: { endTimeSec?: number query?: string startTimeSec?: number stepSec?: number }) { return client .getInstance() .metricsQueryGet(params, { handleError: 'custom' } as ReqConfig) .then((res) => res.data) } } export const getResourceManagerContext: () => IResourceManagerContext = () => { return { ds: new DataSource(), cfg: {} } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/ResourceManager/index.tsx ================================================ import React from 'react' import { ResourceManagerApp, ResourceManagerProvider } from '@pingcap/tidb-dashboard-lib' import { getResourceManagerContext } from './context-impl' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/ResourceManager/meta.ts ================================================ import { HddOutlined } from '@ant-design/icons' export default { id: 'resource_manager', routerPrefix: '/resource_manager', icon: HddOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SearchLogs/context.ts ================================================ import { ISearchLogsDataSource, ISearchLogsContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { LogsearchCreateTaskGroupRequest } from '~/client' class DataSource implements ISearchLogsDataSource { logsDownloadAcquireTokenGet(id?: Array, options?: ReqConfig) { return client.getInstance().logsDownloadAcquireTokenGet({ id }, options) } // logsDownloadGet(token: string, options?: ReqConfig) { // return client.getInstance().logsDownloadGet({ token }, options) // } logsTaskgroupPut( request: LogsearchCreateTaskGroupRequest, options?: ReqConfig ) { return client.getInstance().logsTaskgroupPut({ request }, options) } logsTaskgroupsGet(options?: ReqConfig) { return client.getInstance().logsTaskgroupsGet(options) } logsTaskgroupsIdCancelPost(id: string, options?: ReqConfig) { return client.getInstance().logsTaskgroupsIdCancelPost({ id }, options) } logsTaskgroupsIdDelete(id: string, options?: ReqConfig) { return client.getInstance().logsTaskgroupsIdDelete({ id }, options) } logsTaskgroupsIdGet(id: string, options?: ReqConfig) { return client.getInstance().logsTaskgroupsIdGet({ id }, options) } logsTaskgroupsIdPreviewGet(id: string, options?: ReqConfig) { return client.getInstance().logsTaskgroupsIdPreviewGet({ id }, options) } logsTaskgroupsIdRetryPost(id: string, options?: ReqConfig) { return client.getInstance().logsTaskgroupsIdRetryPost({ id }, options) } getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } getTiCDCTopology(options?: ReqConfig) { return client.getInstance().getTiCDCTopology(options) } getTiProxyTopology(options?: ReqConfig) { return client.getInstance().getTiProxyTopology(options) } getTSOTopology(options?: ReqConfig) { return client.getInstance().getTSOTopology(options) } getSchedulingTopology(options?: ReqConfig) { return client.getInstance().getSchedulingTopology(options) } } const ds = new DataSource() export const ctx: ISearchLogsContext = { ds, cfg: { apiPathBase: client.getBasePath() } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SearchLogs/index.tsx ================================================ import React from 'react' import { SearchLogsApp, SearchLogsProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SearchLogs/meta.ts ================================================ import { FileSearchOutlined } from '@ant-design/icons' export default { id: 'search_logs', routerPrefix: '/search_logs', icon: FileSearchOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SlowQuery/context.ts ================================================ import { ISlowQueryDataSource, ISlowQueryContext, ISlowQueryConfig, ISlowQueryEvent, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { SlowqueryModel } from '~/client' const debugHeaders = { // 'x-cluster-id': '1379661944646413143', // 'x-org-id': '1372813089209061633', // 'x-project-id': '1372813089454525346', // 'x-provider': 'aws', // 'x-region': 'us-east-1', // 'x-env': 'prod' } class DataSource implements ISlowQueryDataSource { constructor(public cache: SlowqueryModel[]) {} getDatabaseList(beginTime: number, endTime: number, options?: ReqConfig) { // get database list from PD if (beginTime === 0) { return client.getInstance().infoListDatabases(options) } // get database list from s3 return client .getAxiosInstance() .get( `/slow_query/databases?begin_time=${beginTime}&end_time=${endTime}`, { headers: debugHeaders } ) } infoListResourceGroupNames(options?: ReqConfig) { return client.getInstance().resourceManagerInformationGroupNamesGet(options) } slowQueryAvailableFieldsGet(options?: ReqConfig) { return client.getInstance().slowQueryAvailableFieldsGet(options) } slowQueryListGet( beginTime?: number, db?: Array, desc?: boolean, digest?: string, endTime?: number, fields?: string, limit?: number, orderBy?: string, plans?: Array, resourceGroup?: Array, text?: string, showInternal?: boolean, options?: ReqConfig ) { const localVarQueryParameter = {} as any if (beginTime !== undefined) { localVarQueryParameter['begin_time'] = beginTime } if (db) { localVarQueryParameter['db'] = db } if (desc !== undefined) { localVarQueryParameter['desc'] = desc } if (digest !== undefined) { localVarQueryParameter['digest'] = digest } if (endTime !== undefined) { localVarQueryParameter['end_time'] = endTime } if (fields !== undefined) { localVarQueryParameter['fields'] = fields } if (limit !== undefined) { localVarQueryParameter['limit'] = limit } if (orderBy !== undefined) { localVarQueryParameter['orderBy'] = orderBy } if (plans) { localVarQueryParameter['plans'] = plans } if (resourceGroup) { localVarQueryParameter['resource_group'] = resourceGroup } if (text !== undefined) { localVarQueryParameter['text'] = text } if (showInternal !== undefined) { localVarQueryParameter['show_internal'] = showInternal } const searchParams = new URLSearchParams() for (const field in localVarQueryParameter) { const value = localVarQueryParameter[field] if (Array.isArray(value)) { searchParams.delete(field) for (const item of value) { searchParams.append(field, item) } } else { searchParams.set(field, value) } } const searchString = searchParams.toString() return client.getAxiosInstance().get(`/slow_query/list?${searchString}`) } slowQueryDetailGet( connectId?: string, digest?: string, timestamp?: number, options?: ReqConfig ) { // to make this.cache as small as possible const cachedItem = this.cache.pop() if (cachedItem) { return Promise.resolve({ data: cachedItem, status: 200, statusText: 'ok', headers: {}, config: {} } as any) } else { return client.getInstance().slowQueryDetailGet( { connectId, digest, timestamp }, options ) } } slowQueryDownloadTokenPost(request: any, options?: ReqConfig) { return client.getInstance().slowQueryDownloadTokenPost({ request }, options) } slowQueryAnalyze(start: number, end: number) { return client .getAxiosInstance() .get(`/slow_query/analyze?begin_time=${start}&end_time=${end}`) } slowQueryDownloadDBFile(begin_time: number, end_time: number) { return client .getAxiosInstance() .get(`/slow_query/files?begin_time=${begin_time}&end_time=${end_time}`, { responseType: 'blob', headers: { Accept: 'application/octet-stream' } }) } promqlQuery(query: string, time: number, timeout: string) { return client .getAxiosInstance() .get( `/slow_query/vm_query?query=${query}&time=${time}&timeout=${timeout}` ) .then((res) => res.data) } promqlQueryRange(query: string, start: number, end: number, step: string) { return client .getAxiosInstance() .get( `/slow_query/vm_query_range?query=${query}&start=${start}&end=${end}&step=${step}` ) .then((res) => res.data) } } class EventHandler implements ISlowQueryEvent { constructor( public listApiReturnDetail: boolean, public cache: SlowqueryModel[] ) {} selectSlowQueryItem(item: any) { if (this.listApiReturnDetail === true) { this.cache.push(item) } } } export const ctx: (cfg: Partial) => ISlowQueryContext = ( cfg ) => { const slowQueryCache: SlowqueryModel[] = [] return { ds: new DataSource(slowQueryCache), event: new EventHandler(cfg.listApiReturnDetail ?? false, slowQueryCache), cfg: { apiPathBase: client.getBasePath(), enableExport: true, showDBFilter: true, showDigestFilter: false, showResourceGroupFilter: true, showDownloadSlowQueryDBFile: true, showInternalFilter: true, showRuV2: true, ...cfg } } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SlowQuery/index.tsx ================================================ import React from 'react' import { SlowQueryApp, SlowQueryProvider } from '@pingcap/tidb-dashboard-lib' import { getGlobalConfig } from '~/utils/globalConfig' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SlowQuery/meta.ts ================================================ import { RocketOutlined } from '@ant-design/icons' export default { id: 'slow_query', routerPrefix: '/slow_query', icon: RocketOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Statement/context.ts ================================================ import { IStatementDataSource, IStatementContext, ReqConfig, IStatementConfig } from '@pingcap/tidb-dashboard-lib' import client, { StatementEditableConfig, StatementGetStatementsRequest } from '~/client' class DataSource implements IStatementDataSource { getDatabaseList( beginTime: number, endTime: number, options?: ReqConfig | undefined ) { return client.getInstance().infoListDatabases(options) } infoListResourceGroupNames(options?: ReqConfig) { return client.getInstance().resourceManagerInformationGroupNamesGet(options) } statementsAvailableFieldsGet(options?: ReqConfig) { return client.getInstance().statementsAvailableFieldsGet(options) } statementsConfigGet(options?: ReqConfig) { return client.getInstance().statementsConfigGet(options) } statementsConfigPost(request: StatementEditableConfig, options?: ReqConfig) { return client.getInstance().statementsConfigPost({ request }, options) } statementsDownloadGet(token: string, options?: ReqConfig) { return client.getInstance().statementsDownloadGet({ token }, options) } statementsDownloadTokenPost( request: StatementGetStatementsRequest, options?: ReqConfig ) { return client .getInstance() .statementsDownloadTokenPost({ request }, options) } statementsListGet( beginTime?: number, endTime?: number, fields?: string, schemas?: Array, resourceGroups?: Array, stmtTypes?: Array, text?: string, options?: ReqConfig ) { return client.getInstance().statementsListGet( { beginTime, endTime, fields, schemas, resourceGroups, stmtTypes, text }, options ) } statementsPlanDetailGet( beginTime?: number, digest?: string, endTime?: number, plans?: Array, schemaName?: string, options?: ReqConfig ) { return client.getInstance().statementsPlanDetailGet( { beginTime, digest, endTime, plans, schemaName }, options ) } statementsPlansGet( beginTime?: number, digest?: string, endTime?: number, schemaName?: string, options?: ReqConfig ) { return client.getInstance().statementsPlansGet( { beginTime, digest, endTime, schemaName }, options ) } statementsStmtTypesGet(options?: ReqConfig) { return client.getInstance().statementsStmtTypesGet(options) } statementsTimeRangesGet(options?: ReqConfig) { return client.getAxiosInstance().get('/statements/time_ranges', options) } // slow query slowQueryAvailableFieldsGet(options?: ReqConfig) { return client.getInstance().slowQueryAvailableFieldsGet(options) } slowQueryListGet( beginTime?: number, db?: Array, desc?: boolean, digest?: string, endTime?: number, fields?: string, limit?: number, orderBy?: string, plans?: Array, resourceGroup?: Array, text?: string, showInternal?: boolean, options?: ReqConfig ) { return client.getInstance().slowQueryListGet( { beginTime, db, desc, digest, endTime, fields, limit, orderBy, plans, resourceGroup, text }, options ) } slowQueryDetailGet( connectId?: string, digest?: string, timestamp?: number, options?: ReqConfig ) { return client.getInstance().slowQueryDetailGet( { connectId, digest, timestamp }, options ) } slowQueryDownloadTokenPost(request: any, options?: ReqConfig) { return client.getInstance().slowQueryDownloadTokenPost({ request }, options) } } export const ctx: (cfg: Partial) => IStatementContext = ( cfg ) => { return { ds: new DataSource(), cfg: { apiPathBase: client.getBasePath(), showRuV2: true, ...cfg } } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Statement/index.tsx ================================================ import React from 'react' import { StatementApp, StatementProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' import { getGlobalConfig } from '~/utils/globalConfig' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Statement/meta.ts ================================================ import { ThunderboltOutlined } from '@ant-design/icons' export default { id: 'statement', routerPrefix: '/statement', icon: ThunderboltOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SystemReport/context.ts ================================================ import { ISystemReportDataSource, ISystemReportContext, ReqConfig, ISystemReportConfig } from '@pingcap/tidb-dashboard-lib' import client, { DiagnoseGenerateReportRequest, DiagnoseGenerateMetricsRelationRequest } from '~/client' import publicPathBase from '~/utils/publicPathPrefix' export type DsExtra = { orgId: string clusterId: string } class DataSource implements ISystemReportDataSource { diagnoseReportsGet(options?: ReqConfig) { return client.getInstance().diagnoseReportsGet(options) } diagnoseReportsPost( request: DiagnoseGenerateReportRequest, options?: ReqConfig ) { return client.getInstance().diagnoseReportsPost({ request }, options) } diagnoseGenerateMetricsRelationship( request: DiagnoseGenerateMetricsRelationRequest, options?: ReqConfig ) { return client .getInstance() .diagnoseGenerateMetricsRelationship({ request }, options) } diagnoseReportsIdStatusGet(id: string, options?: ReqConfig) { return client.getInstance().diagnoseReportsIdStatusGet({ id }, options) } } class SystemReportConfig implements ISystemReportConfig { constructor(public extra: DsExtra) {} public apiPathBase = client.getBasePath() public publicPathBase = publicPathBase public fullReportLink(reportId: string): string { const { orgId, clusterId } = this.extra return `${publicPathBase}/diagnose-report/?orgId=${orgId}&clusterId=${clusterId}&reportId=${reportId}` } } export const ctx: (extra: DsExtra) => ISystemReportContext = (extra) => ({ ds: new DataSource(), cfg: new SystemReportConfig(extra) }) ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SystemReport/index.tsx ================================================ import React, { useMemo } from 'react' import { SystemReportApp, SystemReportProvider } from '@pingcap/tidb-dashboard-lib' import { ctx, DsExtra } from './context' function getDsExtra(): DsExtra { const searchParams = new URLSearchParams(window.location.search) return { orgId: searchParams.get('orgId')!, clusterId: searchParams.get('clusterId')! } } export default function () { const dsExtra = useMemo(() => getDsExtra(), []) return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SystemReport/meta.ts ================================================ import { SnippetsOutlined } from '@ant-design/icons' export default { id: 'system_report', routerPrefix: '/system_report', icon: SnippetsOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSQL/context.ts ================================================ import { ITopSQLDataSource, ITopSQLContext, ITopSQLConfig, TopsqlTikvNetworkIoCollectionConfig, TopsqlTikvNetworkIoCollectionUpdateResponse, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { TopsqlEditableConfig } from '~/client' class DataSource implements ITopSQLDataSource { topsqlConfigGet(options?: ReqConfig) { return client.getInstance().topsqlConfigGet(options) } topsqlConfigPost(request: TopsqlEditableConfig, options?: ReqConfig) { return client.getInstance().topsqlConfigPost({ request }, options) } topsqlTikvNetworkIoCollectionGet(options?: ReqConfig) { // Cloud TopSQL does not expose TiKV multi-dimensional collection settings. // Return a fixed disabled state to keep interface compatibility. return Promise.resolve({ data: { enable: false, is_multi_value: false } as TopsqlTikvNetworkIoCollectionConfig } as any) } topsqlTikvNetworkIoCollectionPost( request: TopsqlTikvNetworkIoCollectionConfig, options?: ReqConfig ) { // Cloud TopSQL does not expose TiKV multi-dimensional collection settings. // Keep no-op behavior for compatibility if called unexpectedly. return Promise.resolve({ data: { warnings: [] } as TopsqlTikvNetworkIoCollectionUpdateResponse } as any) } topsqlInstancesGet( end?: string, start?: string, dataSource?: string, options?: ReqConfig ) { const requestParameters: any = { start, end } if (dataSource !== undefined) { requestParameters.dataSource = dataSource } return client.getInstance().topsqlInstancesGet(requestParameters, options) } topsqlSummaryGet( end?: string, groupBy?: string, instance?: string, instanceType?: string, orderBy?: string, start?: string, top?: string, window?: string, dataSource?: string, options?: ReqConfig ) { return client.getInstance().topsqlSummaryGet( { end, groupBy, instance, instanceType, orderBy, start, top, window, dataSource }, options ) } } const ds = new DataSource() export const ctx: (cfg: Partial) => ITopSQLContext = (cfg) => ({ ds, cfg: { checkNgm: true, showSetting: true, ...cfg } }) ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSQL/index.tsx ================================================ import React from 'react' import { TopSQLApp, TopSQLProvider } from '@pingcap/tidb-dashboard-lib' import { getGlobalConfig } from '~/utils/globalConfig' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSQL/meta.ts ================================================ import { BarChartOutlined } from '@ant-design/icons' export default { id: 'topsql', routerPrefix: '/topsql', icon: BarChartOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSlowQuery/context-provider.tsx ================================================ import React, { useMemo } from 'react' import { TopSlowQueryContext, TopSlowQueryCtxValue } from '@pingcap/tidb-dashboard-lib' import { getGlobalConfig } from '~/utils/globalConfig' import client from '~/client' const debugHeaders = { // 'x-cluster-id': '1379661944646413143', // 'x-org-id': '1372813089209061633', // 'x-project-id': '1372813089454525346', // 'x-provider': 'aws', // 'x-region': 'us-east-1', // 'x-env': 'prod' } export function TopSlowQueryProvider(props: { children: React.ReactNode }) { const ctxValue = useMemo(() => { return { api: { getAvailableTimeWindows: async ({ from, to, duration }: { from: number to: number duration: number }) => { const hours = duration / 3600 return client .getAxiosInstance() .get( `/slow_query/stats/time_windows?begin_time=${from}&end_time=${to}&hours=${hours}`, { headers: debugHeaders } ) .then((res) => res.data) }, getMetrics: async (params: { start: number; end: number }) => { const hours = (params.end - params.start) / 3600 return client .getAxiosInstance() .get( `/slow_query/stats/metric?begin_time=${params.start}&hours=${hours}&metric_name=count_per_minute`, { headers: debugHeaders } ) .then((res) => res.data) }, getDatabaseList: async (params: { start: number; end: number }) => { const hours = (params.end - params.start) / 3600 return client .getAxiosInstance() .get( `/slow_query/stats/databases?begin_time=${params.start}&hours=${hours}`, { headers: debugHeaders } ) .then((res) => res.data) }, getTopSlowQueries: async (params: { start: number end: number order: string dbs: string[] internal: string stmtKinds: string[] }) => { const hours = (params.end - params.start) / 3600 const p = new URLSearchParams() p.append('begin_time', params.start + '') p.append('hours', hours + '') p.append('order_by', params.order) p.append('limit', '10') params.dbs.forEach((d) => p.append('databases', d)) params.stmtKinds.forEach((d) => p.append('statement_types', d)) return client .getAxiosInstance() .get(`/slow_query/stats?${p.toString()}`, { headers: debugHeaders }) .then((res) => res.data) } }, cfg: getGlobalConfig().appsConfig?.topSlowQuery || {} } }, []) return ( {props.children} ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSlowQuery/index.tsx ================================================ import React from 'react' import { TopSlowQueryApp } from '@pingcap/tidb-dashboard-lib' import { TopSlowQueryProvider } from './context-provider' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSlowQuery/meta.ts ================================================ import { BarChartOutlined } from '@ant-design/icons' export default { id: 'top_slowquery', routerPrefix: '/top_slowquery', icon: BarChartOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSlowQuery/sample-data/slowqueries.json ================================================ [ { "sql_digest": "9a4170a059f3113e196bdac9c156d6cecf3ccbef8eb238b878d04e61aa7d9741", "sql_text": "start transaction;", "count": 8559, "sum_latency": 21234.817677733, "max_latency": 4.577684212, "avg_latency": 2.4809928353467696, "sum_memory": 22654976, "max_memory": 520192, "avg_memory": 2646.91856525295, "sum_disk": 0, "max_disk": 0, "avg_disk": 0 }, { "sql_digest": "9505cacb7c710ed17125fcc6cb3669e8ddca6c8cd8af6a31f6b3cd64604c3098", "sql_text": "commit;", "count": 6973, "sum_latency": 19676.377774166976, "max_latency": 9.5105206, "avg_latency": 2.821795177709304, "sum_memory": 83042304, "max_memory": 2088960, "avg_memory": 11909.12146852144, "sum_disk": 0, "max_disk": 0, "avg_disk": 0 }, { "sql_digest": "6d07d8784dc1f26e88f9498494ea658077d8f24354ea9840386ffa5b05bb96e3", "sql_text": "select * from `dh_audit_record` where `user_id` = ? and status in ( ... ) and `charge_time` \u003c ? order by `charge_time` asc , `id` asc;", "count": 2358, "sum_latency": 5021.569481481002, "max_latency": 3.45770166, "avg_latency": 2.1295884145381687, "sum_memory": 73377498, "max_memory": 63388, "avg_memory": 31118.531806615778, "sum_disk": 0, "max_disk": 0, "avg_disk": 0 }, { "sql_digest": "3067b3ecccb86eceaeb26d43420b0414ad2378bfe9d90cc01da6ba1b0619609e", "sql_text": "select `dh_active_category_new` . `id` , `dh_active_category_new` . `category_name` , `dh_active_category_new` . `category_type` , `dh_active_category_new` . `sort_number` , `dh_active_category_new` . `category_status` , `dh_active_category_new` . `active_no` , `dh_active_category_new` . `translate_names` , `dh_active_category_new` . `content` , `dh_active_category_new` . `update_time` , `dh_active_category_new` . `op_user` from `dh_active_category_new` order by `sort_number` asc;", "count": 1779, "sum_latency": 4512.599653008, "max_latency": 4.171903397, "avg_latency": 2.536593396856661, "sum_memory": 9178920, "max_memory": 12288, "avg_memory": 5159.595278246205, "sum_disk": 0, "max_disk": 0, "avg_disk": 0 }, { "sql_digest": "c2d9a6d6ea4c41fcf811146999a6113be6d2db02cf9e2ba63c5d54f5d7d89c90", "sql_text": "select `id` from `dh_game_record` where `game_record_id` = ? limit ?;", "count": 1074, "sum_latency": 2279.700366508, "max_latency": 3.455839219, "avg_latency": 2.1226260395791434, "sum_memory": 0, "max_memory": 0, "avg_memory": 0, "sum_disk": 0, "max_disk": 0, "avg_disk": 0 }, { "sql_digest": "d58c4a60c4657c30d588c86a7d85a4894e9cffdd12076a61d1b3b2b318904c3a", "sql_text": "select * from `dh_finance_details` where `message_status` = ? and `create_time` \u003c ? order by `create_time` desc limit ?;", "count": 830, "sum_latency": 3391.784238015004, "max_latency": 12.926423231, "avg_latency": 4.086487033753017, "sum_memory": 21707732, "max_memory": 61440, "avg_memory": 26153.893975903615, "sum_disk": 0, "max_disk": 0, "avg_disk": 0 }, { "sql_digest": "77f10c86e21cb52c123ba0c7b3f0642136051351f4bc59a637290ffdc20b3678", "sql_text": "select `useridx` , sum ( if ( `direct` = ? and `member_is_agent` = ... ) ) as `direct_agents` , sum ( if ( `direct` = ? and `member_is_agent` = ... ) ) as `direct_accounts` , sum ( if ( `direct` = ? and `member_is_agent` = ... ) ) as `other_agents` , sum ( if ( `direct` = ? and `member_is_agent` = ... ) ) as `other_accounts` from `dh_promote_myteam_detail` where `useridx` = ?;", "count": 751, "sum_latency": 926.2322493650005, "max_latency": 3.791190985, "avg_latency": 1.2333318899667116, "sum_memory": 85776781, "max_memory": 433917, "avg_memory": 114216.75233022636, "sum_disk": 0, "max_disk": 0, "avg_disk": 0 }, { "sql_digest": "8b04b2a8bb0777143b5de1111ffdb142180bec5c58941568b2a2450aead8956d", "sql_text": "select count ( ? ) as `days` from `dh_user_day_report` where `user_idx` = ? and `valid_bet` \u003e ?;", "count": 746, "sum_latency": 1915.1985941270002, "max_latency": 4.038490419, "avg_latency": 2.567290340652815, "sum_memory": 17516890, "max_memory": 67432, "avg_memory": 23481.085790884717, "sum_disk": 0, "max_disk": 0, "avg_disk": 0 }, { "sql_digest": "3adcc90f3cc8cdfba25f8b1fdad067d2d6e7c4b3079836004aa6c2d330d13683", "sql_text": "select `ifnull` ( sum ( `deposit` ) , ? ) as `total_deposit` from `dh_user_day_report` where `user_idx` = ?;", "count": 582, "sum_latency": 1478.318133973, "max_latency": 3.483108902, "avg_latency": 2.540065522290378, "sum_memory": 16735193, "max_memory": 60872, "avg_memory": 28754.62714776632, "sum_disk": 0, "max_disk": 0, "avg_disk": 0 }, { "sql_digest": "daa1f705cac4db15a1a54fece6a5148fd1180d3ac422d8e5e1c1db7c3f9ae6f3", "sql_text": "select `agent_id` , `agent_mode` , `settle_type` , `profit_agent_mode` , `commission_type` , `commission_start_time` , `commission_end_time` from `dh_agentmode` limit ?;", "count": 386, "sum_latency": 971.2267713840008, "max_latency": 3.185514788, "avg_latency": 2.5161315320829036, "sum_memory": 13527988, "max_memory": 74808, "avg_memory": 35046.60103626943, "sum_disk": 0, "max_disk": 0, "avg_disk": 0 } ] ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/UserProfile/context.ts ================================================ import { IUserProfileDataSource, IUserProfileContext, ReqConfig, IUserProfileEvent } from '@pingcap/tidb-dashboard-lib' import client, { SsoCreateImpersonationRequest, SsoSetConfigRequest, CodeShareRequest, MetricsPutCustomPromAddressRequest } from '~/client' class DataSource implements IUserProfileDataSource { userGetSignOutInfo(redirectUrl?: string, options?: ReqConfig) { return client.getInstance().userGetSignOutInfo({ redirectUrl }, options) } userSSOCreateImpersonation( request: SsoCreateImpersonationRequest, options?: ReqConfig ) { return client.getInstance().userSSOCreateImpersonation({ request }, options) } userSSOGetConfig(options?: ReqConfig) { return client.getInstance().userSSOGetConfig(options) } userSSOListImpersonations(options?: ReqConfig) { return client.getInstance().userSSOListImpersonations(options) } userSSOSetConfig(request: SsoSetConfigRequest, options?: ReqConfig) { return client.getInstance().userSSOSetConfig({ request }, options) } userShareSession(request: CodeShareRequest, options?: ReqConfig) { return client.getInstance().userShareSession({ request }, options) } userRevokeSession(options?: ReqConfig) { return client.getInstance().userRevokeSession(options) } metricsGetPromAddress(options?: ReqConfig) { return client.getInstance().metricsGetPromAddress(options) } metricsSetCustomPromAddress( request: MetricsPutCustomPromAddressRequest, options?: ReqConfig ) { return client .getInstance() .metricsSetCustomPromAddress({ request }, options) } } class EventHandler implements IUserProfileEvent { logOut(): void {} } export const ctx: IUserProfileContext = { ds: new DataSource(), event: new EventHandler() } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/UserProfile/index.tsx ================================================ import React from 'react' import { UserProfileApp, UserProfileProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/UserProfile/meta.ts ================================================ import { UserOutlined } from '@ant-design/icons' export default { id: 'user_profile', routerPrefix: '/user_profile', icon: UserOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/client/index.tsx ================================================ import React from 'react' import i18next from 'i18next' import axios, { AxiosInstance } from 'axios' import { message, Modal, notification } from 'antd' import { routing, i18n } from '@pingcap/tidb-dashboard-lib' import { Configuration, DefaultApi as DashboardApi } from '@pingcap/tidb-dashboard-client' import { ClientOptions, ClusterInfo } from '~/utils/globalConfig' import translations from './translations' export * from '@pingcap/tidb-dashboard-client' ////////////////////////////// const client = { init( apiBasePath: string, apiInstance: DashboardApi, axiosInstance: AxiosInstance ) { this.apiBasePath = apiBasePath this.apiInstance = apiInstance this.axiosInstance = axiosInstance }, getInstance(): DashboardApi { return this.apiInstance }, getBasePath(): string { return this.apiBasePath }, getAxiosInstance(): AxiosInstance { return this.axiosInstance } } export default client ////////////////////////////// type HandleError = 'default' | 'custom' function applyErrorHandlerInterceptor(instance: AxiosInstance) { instance.interceptors.response.use(undefined, async function (err) { const { response, config } = err const handleError = config.handleError as HandleError const method = (config.method as string).toLowerCase() let errCode: string let content: string if (err.message === 'Network Error') { errCode = 'common.network' } else { errCode = response?.data?.code } if (i18next.exists(`error.${errCode ?? ''}`)) { // If there is a translation for the code, use the translation. // TODO: Better to display error details somewhere. content = i18next.t(`error.${errCode}`) } else { content = String( response?.data?.message || err.message || 'Internal error' ) } err.message = content err.errCode = errCode if (errCode === 'common.unauthenticated' || response?.status === 401) { // Handle unauthorized error in a unified way if (!routing.isLocationMatch('/') && !routing.isSignInPage()) { message.error({ content, key: errCode ?? '401' }) } // Remember the current url before redirecting to login page, // to support redirect back after login. localStorage.setItem('clinic.login.from', window.location.href) setTimeout(() => { window.location.href = window.location.origin }, 2000) err.handled = true } else if (handleError === 'default') { if (method === 'get') { const fullUrl = config.url as string const API = fullUrl.replace(client.getBasePath(), '').split('?')[0] notification.error({ key: API, message: i18next.t('error.title'), description: ( API: {API}
{content}
) }) } else if (['post', 'put', 'delete', 'patch'].includes(method)) { Modal.error({ title: i18next.t('error.title'), content: content, zIndex: 2000 // higher than popover }) } err.handled = true } return Promise.reject(err) }) } function initAxios(clientOptions: ClientOptions, clusterInfo: ClusterInfo) { const { apiToken } = clientOptions const { provider, region, orgId, projectId, clusterId, deployType, env } = clusterInfo let headers = {} // for clinic headers['x-csrf-token'] = apiToken // for tidb cloud // headers['authorization'] = `Bearer ${apiToken}` if (provider) { headers['x-provider'] = provider } if (region) { headers['x-region'] = region } if (orgId) { headers['x-org-id'] = orgId } if (projectId) { headers['x-project-id'] = projectId } if (clusterId) { headers['x-cluster-id'] = clusterId } if (deployType) { headers['x-deploy-type'] = deployType } if (env) { headers['x-env'] = env } const instance = axios.create({ baseURL: clientOptions.apiPathBase, headers }) applyErrorHandlerInterceptor(instance) return instance } export function setupClient( clientOptions: ClientOptions, clusterInfo: ClusterInfo ) { i18n.addTranslations(translations) const axiosInstance = initAxios(clientOptions, clusterInfo) const dashboardApi = new DashboardApi( new Configuration({ baseOptions: { handleError: 'default' } }), // basePath, it's set in the axiosInstance, so we pass empty string to dashboard Api // if basePath and baseURL are both relative path // the final api path will be the value that combined by dashboardApi basePath and axiosInstance baseURL // if we use undefined for this param, dashboardApi basePath will be the default value `/dashboard/api` '', axiosInstance ) client.init(clientOptions.apiPathBase, dashboardApi, axiosInstance) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/client/translations/en.yaml ================================================ error: title: Error common: network: Network connection error unauthenticated: Please sign in again (session is expired) forbidden: The current user is not authorized to perform this action api: user: signin: invalid_code: Authorization Code is invalid or expired insufficient_priv: The user does not have sufficient privileges to access {{distro.tidb}} Dashboard. slow_query: export_no_data: No slow queires can be exported statement: export_no_data: No statements can be exported continuous_profiling: ng_monitoring_not_ready: | To use or learn more about "Continuous Profiling" feature, please search for "Continuous Profiling" in the {{distro.tidb}} official docs for more information. If it doesn't resove the issue, please contact the product's technical support. feature_not_supported: The cluster of this version doesn't support or can't use this feature, please contact with technical support to get more information. tidb: no_alive_tidb: No alive {{distro.tidb}} instance pd_access_failed: Failed to access {{distro.pd}} node tidb_conn_failed: Failed to connect to {{distro.tidb}} tidb_auth_failed: '{{distro.tidb}} authentication failed' ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/client/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/client/translations/zh.yaml ================================================ error: title: 错误 common: network: 网络连接失败 unauthenticated: 会话已过期,请重新登录 forbidden: 当前用户没有权限进行该操作 api: user: signin: invalid_code: 授权码无效或已过期 insufficient_priv: 该用户缺少足够的权限访问 {{distro.tidb}} Dashboard。 slow_query: export_no_data: 没有可导出的慢查询日志 statement: export_no_data: 没有可导出的语句 continuous_profiling: ng_monitoring_not_ready: | 想使用或深入了解“持续性能分析”功能,请在 {{distro.tidb}} 官方文档搜索“持续性能分析”查看更多内容。 若未能解决问题,请联系本产品技术支持。 feature_not_supported: 当前版本的集群不支持或无法使用该功能,请联系技术支持了解详细情况。 tidb: no_alive_tidb: 没有正在运行的 {{distro.tidb}} 实例 pd_access_failed: 无法访问 {{distro.pd}} 节点 tidb_conn_failed: 无法连接到 {{distro.tidb}} tidb_auth_failed: '{{distro.tidb}} 登录验证失败' ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/index.ts ================================================ import '../styles/style.less' import '@pingcap/tidb-dashboard-lib/dist/index.css' import { start } from './main' import '../styles/override.less' export default start ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/layout/main/Sider/Banner.module.less ================================================ @import 'antd/es/style/themes/default.less'; .banner { background: @primary-color; color: #fff; cursor: pointer; overflow: hidden; user-select: none; position: relative; margin-bottom: 20px; flex-shrink: 0; } .bannerLeft { padding: 20px 16px 20px 24px; } .bannerRight { position: absolute; top: 0; height: 100%; transition: background-color 0.2s @ease-out; display: flex; } .banner:hover .bannerRight { background: lighten(@primary-color, 5%); } .bannerLogo { margin: 5px 0; } .bannerContent { margin-left: 15px; } .bannerTitle { font-size: 1rem; } .bannerVersion { font-size: 0.9rem; opacity: 0.7; } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/layout/main/Sider/Banner.tsx ================================================ import React, { useMemo, useRef } from 'react' import { CaretRightOutlined, CaretLeftOutlined } from '@ant-design/icons' import { useSize } from 'ahooks' import Flexbox from '@g07cha/flexbox-react' import { useSpring, animated } from 'react-spring' import { useTranslation, TFunction } from 'react-i18next' import { InfoInfoResponse } from '~/client' import { // store store } from '@pingcap/tidb-dashboard-lib' import { lightLogoSvg } from '~/utils/distro/assetsRes' import styles from './Banner.module.less' const toggleWidth = 40 const toggleHeight = 50 function parseVersion(i: InfoInfoResponse, t: TFunction) { if (!i.version) { return null } if (i.version.standalone !== 'No') { // For Standalone == Yes / Unknown, display internal version if (i.version.internal_version === 'nightly') { let vPrefix = i.version.internal_version if (i.version.build_git_hash) { vPrefix += `-${i.version.build_git_hash.substr(0, 8)}` } // e.g. nightly-xxxxxxxx return vPrefix } if (i.version.internal_version) { // e.g. v2020.07.01.1 if (i.version.internal_version.startsWith('v')) { return i.version.internal_version } else { return `v${i.version.internal_version}` } } return null } if (i.version.pd_version) { // e.g. PD v4.0.1 return `${t('distro.pd')} ${i.version.pd_version}` } } export default function ToggleBanner({ fullWidth, collapsedWidth, collapsed, onToggle }) { const { t } = useTranslation() const bannerRef = useRef(null) const bannerSize = useSize(bannerRef) const transBanner = useSpring({ opacity: collapsed ? 0 : 1, height: collapsed ? toggleHeight : bannerSize?.height ?? 0 }) const transButton = useSpring({ left: collapsed ? 0 : fullWidth - toggleWidth, width: collapsed ? collapsedWidth : toggleWidth }) const appInfo = store.useState((s) => s.appInfo) const version = useMemo(() => { if (appInfo) { return parseVersion(appInfo, t) } return null }, [appInfo, t]) return (
{t('distro.tidb')} Dashboard
{version || 'Version unknown'}
{collapsed ? ( ) : ( )}
) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/layout/main/Sider/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; @sider-background: rgb(246, 246, 246); @sider-highlight-height: 36px; @sider-ribbon-height: 30px; .sider { position: fixed; left: 0; top: 0; height: 100%; z-index: 1; background: @sider-background; overflow-x: hidden; overflow-y: auto; transition: none; user-select: none; :global { /* cancel text animations */ .ant-menu-item .ant-menu-item-icon, .ant-menu-submenu-title .ant-menu-item-icon, .ant-menu-item .anticon, .ant-menu-submenu-title .anticon { transition-duration: 0.1s; } .ant-menu-item .ant-menu-item-icon + span, .ant-menu-submenu-title .ant-menu-item-icon + span, .ant-menu-item .anticon + span, .ant-menu-submenu-title .anticon + span { transition-duration: 0.1s; } .ant-menu-item, .ant-menu-submenu-title { transition-duration: 0.1s; } .ant-menu-title-content { transition-duration: 0.1s; } .ant-layout-sider-children { display: flex; flex-direction: column; padding-bottom: 20px; .ant-menu.ant-menu-inline-collapsed { width: @menu-collapsed-width; // 80px } } .ant-menu { border-right: 0; background: none; } .ant-menu-submenu-selected { color: #000; } .ant-menu-item { background: none !important; &::after { left: 0; top: 0; height: @sider-ribbon-height; margin-top: -(@sider-ribbon-height / 2); top: 50%; border: 0px; width: 5px; border-radius: 0 5px 5px 0; background: @primary-color; } &::before { content: ''; position: absolute; left: 0; right: 20px; height: @sider-highlight-height; top: 50%; margin-top: -(@sider-highlight-height / 2); border-radius: 0 (@sider-highlight-height / 2) (@sider-highlight-height / 2) 0; z-index: -1; } &:hover::before { background: rgba(lighten(@primary-color, 20%), 0.2); } a { color: #666; transition-duration: 0s; &:hover { color: #000; } } &.ant-menu-item-selected { &::before { background: rgba(darken(@sider-background, 30%), 0.15); } a { color: #000; } } } .ant-menu-submenu-title { background: none !important; } .ant-menu-inline-collapsed .ant-menu-item { &::before { right: 10px; left: 10px; border-radius: (@sider-highlight-height / 2); } } } } :global { .ant-menu-inline-collapsed-tooltip { a { color: @text-color-dark; // hsla(0, 0%, 100%, 0.85) } .anticon { display: none; } } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/layout/main/Sider/index.tsx ================================================ import React, { useState, useMemo } from 'react' import { ExperimentOutlined, BugOutlined, AimOutlined // PullRequestOutlined } from '@ant-design/icons' import { Layout, Menu } from 'antd' import { Link } from 'react-router-dom' import { useEventListener } from 'ahooks' import { useTranslation } from 'react-i18next' import { useSpring, animated } from 'react-spring' import Banner from './Banner' import styles from './index.module.less' import { store, useIsFeatureSupport } from '@pingcap/tidb-dashboard-lib' function useAppMenuItem( registry, appId, enable: boolean = true, title?: string, hideIcon?: boolean ) { const { t } = useTranslation() const app = registry.apps[appId] if (!enable || !app) { return null } return ( {!hideIcon && app.icon ? : null} {title ? title : t(`${appId}.nav_title`, appId)} ) } function useActiveAppId(registry) { const [appId, set] = useState('') useEventListener( 'single-spa:routing-event', () => { const activeApp = registry.getActiveApp() if (activeApp) { set(activeApp.id) } }, { target: window } ) return appId } function Sider({ registry, fullWidth, defaultCollapsed, collapsed, collapsedWidth, onToggle, animationDelay }) { const { t } = useTranslation() const activeAppId = useActiveAppId(registry) const whoAmI = store.useState((s) => s.whoAmI) const appInfo = store.useState((s) => s.appInfo) const supportConProf = useIsFeatureSupport('conprof') const profilingSubMenuItems = [ useAppMenuItem(registry, 'instance_profiling', true, '', true), useAppMenuItem(registry, 'conprof', supportConProf, '', true) ] const profilingSubMenu = ( {t('profiling.nav_title')} } > {profilingSubMenuItems} ) const debugSubMenuItems = [ profilingSubMenu, useAppMenuItem(registry, 'debug_api') ] const debugSubMenu = ( {t('nav.sider.debug')} } > {debugSubMenuItems} ) // const conflictSubMenuItems = [useAppMenuItem(registry, 'deadlock')] // const conflictSubMenu = ( // // // {t('nav.sider.conflict')} // // } // > // {conflictSubMenuItems} // // ) const experimentalSubMenuItems = [ useAppMenuItem(registry, 'query_editor'), useAppMenuItem(registry, 'configuration') ] const experimentalSubMenu = ( {t('nav.sider.experimental')} } > {experimentalSubMenuItems} ) const supportTopSQL = useIsFeatureSupport('topsql') const supportResourceManager = useIsFeatureSupport('resource_manager') const menuItems = [ useAppMenuItem(registry, 'overview'), useAppMenuItem(registry, 'cluster_info'), // topSQL useAppMenuItem(registry, 'topsql', supportTopSQL), useAppMenuItem(registry, 'statement'), useAppMenuItem(registry, 'slow_query'), useAppMenuItem(registry, 'keyviz'), useAppMenuItem(registry, 'system_report'), // warning: "diagnose" app doesn't release yet // useAppMenuItem(registry, 'diagnose'), useAppMenuItem(registry, 'monitoring'), useAppMenuItem(registry, 'search_logs'), useAppMenuItem(registry, 'resource_manager', supportResourceManager), // useAppMenuItem(registry, '__APP_NAME__'), // NOTE: Don't remove above comment line, it is a placeholder for code generator debugSubMenu // conflictSubMenu ] if (appInfo?.enable_experimental) { menuItems.push(experimentalSubMenu) } let displayName = whoAmI?.display_name || '...' const extraMenuItems = [ useAppMenuItem(registry, 'dashboard_settings'), useAppMenuItem(registry, 'user_profile', true, displayName) ] const transSider = useSpring({ width: collapsed ? collapsedWidth : fullWidth }) const defaultOpenKeys = useMemo(() => { if (defaultCollapsed) { return [] } else { return ['debug', 'experimental', 'profiling'] } }, [defaultCollapsed]) return ( {menuItems} {extraMenuItems} ) } export default Sider ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/layout/main/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .container { height: 100vh; } .content { position: relative; z-index: 3; background: #fff; min-height: 100vh; &:before, &:after { // Handle margin collapse content: ' '; display: table; } } .contentBack { position: fixed; z-index: 2; background: #fff; top: 0; height: 100%; right: 0; box-shadow: 0 0 20px rgba(#000, 0.1); } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/layout/main/index.tsx ================================================ import React, { useState, useCallback, useEffect } from 'react' // import { Root } from '@lib/components' import { HashRouter as Router } from 'react-router-dom' import { useSpring, animated } from 'react-spring' // import { useVersionedLocalStorageState } from '@lib/utils/useVersionedLocalStorageState' import { Root, useVersionedLocalStorageState } from '@pingcap/tidb-dashboard-lib' import Sider from './Sider' import styles from './index.module.less' const siderWidth = 260 const siderCollapsedWidth = 80 const collapsedContentOffset = siderCollapsedWidth - siderWidth const contentOffsetTrigger = collapsedContentOffset * 0.99 function triggerResizeEvent() { const event = document.createEvent('HTMLEvents') event.initEvent('resize', true, false) window.dispatchEvent(event) } const useContentLeftOffset = (collapsed) => { const [offset, setOffset] = useState(siderWidth) const onAnimationStart = useCallback(() => { if (!collapsed) { setOffset(siderWidth) } }, [collapsed]) const onAnimationFrame = useCallback( ({ x }) => { if (collapsed && x < contentOffsetTrigger) { setOffset(siderCollapsedWidth) } }, [collapsed] ) useEffect(triggerResizeEvent, [offset]) return { contentLeftOffset: offset, onAnimationStart, onAnimationFrame } } export default function App({ registry }) { const [collapsed, setCollapsed] = useVersionedLocalStorageState( 'layout.sider.collapsed', { defaultValue: false } ) const [defaultCollapsed] = useState(collapsed) const { contentLeftOffset, onAnimationStart, onAnimationFrame } = useContentLeftOffset(collapsed) const transContentBack = useSpring({ x: collapsed ? collapsedContentOffset : 0, onStart: onAnimationStart, onFrame: onAnimationFrame }) const transContainer = useSpring({ opacity: 1, from: { opacity: 0 }, delay: 100 }) const handleToggle = useCallback(() => { setCollapsed((c) => !c) }, [setCollapsed]) const { appOptions } = registry return ( {!appOptions.hideNav && ( <> `translate3d(${x}px, 0, 0)` ) }} > )}
) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/layout/translations/en.yaml ================================================ signin: message: error: 'Sign in failed: {{ msg }}' success: Sign in successfully access_doc: Help access_doc_link: https://docs.pingcap.com/tidb/stable/dashboard-user form: username: Username username_tooltip: Sign in user can be customized in TiDB 5.3 or later versions password: Password button: Sign In tidb_auth: title: SQL User Sign In switch: title: SQL User description: I know the username and password to connect to the database code_auth: title: Authorization Code Sign In switch: title: Authorization Code description: I was invited by others with an authorization code code: Code sso: button: Sign In via Company Account (SSO) switch: title: SSO description: I want to sign in use my company account use_alternative: Use Alternative Authentication alternative: title: Select Authentication nav: user: signout: Sign Out sider: debug: Advanced Debugging conflict: Conflict Diagnosing experimental: Experimental Features health_check: failed_notification_title: System Health Check Failed ngm_not_started: A required component `NgMonitoring` is not started in this cluster. Some features may not work. help_text: Help help_url: https://docs.pingcap.com/tidb/dev/dashboard-faq#a-required-component-ngmonitoring-is-not-started-error-is-shown ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/layout/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/layout/translations/zh.yaml ================================================ signin: message: error: '登录失败: {{ msg }}' success: 登录成功 access_doc: 帮助 access_doc_link: https://docs.pingcap.com/zh/tidb/stable/dashboard-user form: username: 用户名 username_tooltip: 升级到 TiDB 5.3 及更高版本后可自定义登录用户 password: 密码 button: 登录 tidb_auth: title: SQL 用户登录 switch: title: SQL 用户 description: 我知道数据库的登录用户名和密码 code_auth: title: 授权码登录 switch: title: 授权码 description: 其他人通过授权码邀请我使用 code: 授权码 sso: button: 使用公司账号 SSO 登录 switch: title: SSO description: 使用公司账号登录 use_alternative: 使用其他登录方式 alternative: title: 选择登录方式 nav: user: signout: 登出 sider: debug: 高级调试 conflict: 冲突诊断 experimental: 实验性功能 health_check: failed_notification_title: 系统健康检查失败 ngm_not_started: 集群中未启动必要组件 `NgMonitoring`,部分功能将不可用。 help_text: 帮助 help_url: https://docs.pingcap.com/zh/tidb/dev/dashboard-faq#界面提示-集群中未启动必要组件-ngmonitoring ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/main.tsx ================================================ import React from 'react' import * as singleSpa from 'single-spa' import i18next from 'i18next' import { Modal, notification } from 'antd' import NProgress from 'nprogress' import './nprogress.less' import { routing, i18n, // telemetry telemetry, // store NgmState, // distro distro, isDistro } from '@pingcap/tidb-dashboard-lib' import { InfoInfoResponse, setupClient } from '~/client' import { mustLoadAppInfo, reloadWhoAmI } from '~/utils/store' import { AppOptions, defAppOptions, GlobalConfig, setGlobalConfig } from '~/utils/globalConfig' import AppRegistry from '~/utils/registry' import AppOverview from '~/apps/Overview/meta' import AppMonitoring from '~/apps/Monitoring/meta' import AppClusterInfo from '~/apps/ClusterInfo/meta' import AppTopSQL from '~/apps/TopSQL/meta' import AppSlowQuery from '~/apps/SlowQuery/meta' import AppStatement from '~/apps/Statement/meta' import AppKeyViz from '~/apps/KeyViz/meta' import AppSystemReport from '~/apps/SystemReport/meta' import AppSearchLogs from '~/apps/SearchLogs/meta' import AppInstanceProfiling from '~/apps/InstanceProfiling/meta' import AppConProfiling from '~/apps/ContinuousProfiling/meta' import AppDebugAPI from '~/apps/DebugAPI/meta' import AppQueryEditor from '~/apps/QueryEditor/meta' import AppConfiguration from '~/apps/Configuration/meta' import AppUserProfile from '~/apps/UserProfile/meta' import AppDiagnose from '~/apps/Diagnose/meta' import AppOptimizerTrace from '~/apps/OptimizerTrace/meta' import AppDeadlock from '~/apps/Deadlock/meta' import AppResourceManager from '~/apps/ResourceManager/meta' import AppTopSlowQuery from '~/apps/TopSlowQuery/meta' import LayoutMain from './layout/main' import translations from './layout/translations' // for update distro strings resource import '~/utils/distro/stringsRes' function removeSpinner() { const spinner = document.getElementById('dashboard_page_spinner') if (spinner) { spinner.remove() } } async function webPageStart(appOptions: AppOptions) { i18n.addTranslations(translations) i18next.changeLanguage(appOptions.lang) let info: InfoInfoResponse if (!appOptions.skipLoadAppInfo) { try { info = await mustLoadAppInfo() if (!appOptions.skipNgmCheck && info?.ngm_state === NgmState.NotStarted) { notification.error({ key: 'ngm_not_started', message: i18next.t('health_check.failed_notification_title'), description: ( {i18next.t('health_check.ngm_not_started')} {!isDistro() && ( <> {' '} {i18next.t('health_check.help_text')} )} ), duration: null }) } } catch (e) { Modal.error({ title: `Failed to connect to server`, content: '' + e, okText: 'Reload', onOk: () => window.location.reload() }) removeSpinner() return } } telemetry.init( process.env.REACT_APP_MIXPANEL_HOST, process.env.REACT_APP_MIXPANEL_TOKEN ) // if (info?.enable_telemetry) { // } // mixpanel telemetry.enable( `tidb-dashboard-for-clinic-cloud-${process.env.REACT_APP_VERSION}` ) let preRoute = '' window.addEventListener('single-spa:routing-event', () => { const curRoute = routing.getPathInLocationHash() if (curRoute !== preRoute) { telemetry.trackRouteChange(curRoute) preRoute = curRoute } }) const registry = new AppRegistry(appOptions) if (!appOptions.hidePageLoadProgress) { NProgress.configure({ showSpinner: false }) window.addEventListener('single-spa:before-routing-event', () => { NProgress.set(0.2) }) window.addEventListener('single-spa:routing-event', () => { NProgress.done(true) }) } singleSpa.registerApplication( 'layout', AppRegistry.newReactSpaApp(() => LayoutMain, 'root'), () => { return !routing.isSignInPage() }, { registry } ) registry .register(AppUserProfile) .register(AppOverview) .register(AppClusterInfo) .register(AppKeyViz) .register(AppTopSQL) .register(AppStatement) .register(AppSystemReport) .register(AppSlowQuery) .register(AppDiagnose) .register(AppMonitoring) .register(AppSearchLogs) .register(AppInstanceProfiling) .register(AppConProfiling) .register(AppQueryEditor) .register(AppConfiguration) .register(AppDebugAPI) .register(AppOptimizerTrace) .register(AppDeadlock) .register(AppResourceManager) .register(AppTopSlowQuery) if (!appOptions.skipReloadWhoAmI) { try { const ok = await reloadWhoAmI() if (routing.isLocationMatch('/') && ok) { singleSpa.navigateToUrl('#' + registry.getDefaultRouter()) } } catch (e) { // If there are auth errors, redirection will happen any way. So we continue. } } window.addEventListener('single-spa:first-mount', () => { removeSpinner() }) singleSpa.start() } export function start(globalConfig: GlobalConfig) { document.title = `${distro().tidb} Dashboard` setGlobalConfig(globalConfig) setupClient(globalConfig.clientOptions, globalConfig.clusterInfo) webPageStart({ ...defAppOptions, ...globalConfig.appOptions }) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/dashboardApp/nprogress.less ================================================ @progress-color: #ffc53d; #nprogress { pointer-events: none; } #nprogress .bar { background: @progress-color; position: fixed; z-index: 1031; top: 0; left: 0; width: 100%; height: 2px; } /* Fancy blur effect */ #nprogress .peg { display: block; position: absolute; right: 0px; width: 100px; height: 100%; box-shadow: 0 0 10px @progress-color, 0 0 5px @progress-color; opacity: 1; transform: rotate(3deg) translate(0px, -4px); } /* Remove these to get rid of the spinner */ #nprogress .spinner { display: block; position: fixed; z-index: 1031; top: 15px; right: 15px; } #nprogress .spinner-icon { width: 18px; height: 18px; box-sizing: border-box; border: solid 2px transparent; border-top-color: @progress-color; border-left-color: @progress-color; border-radius: 50%; animation: nprogress-spinner 400ms linear infinite; } .nprogress-custom-parent { overflow: hidden; position: relative; } .nprogress-custom-parent #nprogress .spinner, .nprogress-custom-parent #nprogress .bar { position: absolute; } @keyframes nprogress-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/diagnoseReportApp/components/DiagnosisReport.tsx ================================================ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { i18n } from '@pingcap/tidb-dashboard-lib' import DiagnosisTable from './DiagnosisTable' import { ExpandContext, TableDef } from '../types' function LangDropdown() { const { i18n: i18next } = useTranslation() return (
) } type Props = { diagnosisTables: TableDef[] } function TablesNavMenu({ diagnosisTables }: Props) { const { t } = useTranslation() return (
{diagnosisTables.map((item) => (

{item.category[0] && t(`diagnosis.tables.category.${item.category[0]}`)}

{t(`diagnosis.tables.title.${item.title}`)}
))}
) } export default function DiagnosisReport({ diagnosisTables }: Props) { const [expandAll, setExpandAll] = useState(false) const { t } = useTranslation() return (

{t('diagnosis.title')}

{diagnosisTables.map((item, idx) => ( ))}
) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/diagnoseReportApp/components/DiagnosisTable.tsx ================================================ import React, { useContext, useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import ReactMarkdown from 'react-markdown' import { distro } from '@pingcap/tidb-dashboard-lib' import { TableDef, ExpandContext, TableRowDef } from '../types' const lowerDistro = Object.keys(distro).reduce((accu, cur) => { if (typeof distro[cur] === 'string') { accu[cur] = distro[cur].toLowerCase() } return accu }, {}) const distroRegs = Object.keys(lowerDistro).reduce((accu, cur) => { accu[cur] = new RegExp(cur, 'ig') return accu }, {}) function replaceDistro(oriStr: string): string { let retStr = oriStr Object.keys(lowerDistro).forEach((key) => { retStr = retStr.replace(distroRegs[key], lowerDistro[key]) }) return retStr } function DiagnosisRow({ row }: { row: TableRowDef }) { const outsideExpand = useContext(ExpandContext) const [internalExpand, setInternalExpand] = useState(false) const { t, i18n } = useTranslation() // when outsideExpand changes, reset the internalExpand to the same as outsideExpand useEffect(() => { setInternalExpand(outsideExpand) }, [outsideExpand]) function showRowName(rowName: string) { const i18nKey = `diagnosis.tables.table.name.${rowName}` if (i18n.exists(i18nKey)) { return t(i18nKey) } return replaceDistro(rowName) } function showOthers(val: string | number) { if (typeof val === 'string') { return replaceDistro(val) } return val } return ( <> {(row.values || []).map((val, valIdx) => ( {valIdx === 0 ? showRowName(val) : showOthers(val)} {valIdx === 0 && t(`diagnosis.tables.table.comment.${val}`, '') !== '' && (

{t(`diagnosis.tables.table.comment.${val}`)}

)} {valIdx === 0 && (row.sub_values || []).length > 0 && ( <>     setInternalExpand(!internalExpand)} > {internalExpand ? t('diagnosis.fold') : t('diagnosis.expand')} )} ))} {(row.sub_values || []).map((subVals, subValsIdx) => ( {subVals.map((subVal, subValIdx) => ( {subValIdx === 0 && '|-- '} {showOthers(subVal)} ))} ))} ) } type Props = { diagnosis: TableDef } export default function DiagnosisTable({ diagnosis }: Props) { const { category, title, column, rows } = diagnosis const { t } = useTranslation() return (
{(category || []).map((c, idx) => (

{c && t(`diagnosis.tables.category.${c}`)}

))}

{t(`diagnosis.tables.title.${title}`)}

{t(`diagnosis.tables.comment.${title}`, '')} {column.map((col, colIdx) => ( ))} {(rows || []).map((row, rowIdx) => ( ))}
{col}
) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/diagnoseReportApp/index.css ================================================ .report-container { margin-bottom: 16px; } tr.subvalues { background-color: lightcyan; } tr.subvalues.fold { display: none; } .subvalues-toggle { display: inline-block; width: 60px; cursor: pointer; color: #2160c4; } .actions { padding: 8px 0; background-color: white; position: sticky; top: 0; z-index: 2; } .table-header-row { position: sticky; top: 55px; z-index: 1; background-color: #888; color: white !important; } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/diagnoseReportApp/index.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom' import 'bulma/css/bulma.css' import '@fortawesome/fontawesome-free/js/all.js' import { i18n, distro } from '@pingcap/tidb-dashboard-lib' import DiagnosisReport from './components/DiagnosisReport' import translations from './translations' // for update distro strings resource import '~/utils/distro/stringsRes' import './index.css' function refineDiagnosisData() { const diagnosisData = window.__diagnosis_data__ || [] let preCategory = '' diagnosisData.forEach((d) => { if (d.category.join('') === preCategory) { d.category = [] } else { preCategory = d.category.join('') } }) return diagnosisData } i18n.addTranslations(translations) document.title = `${distro().tidb} Dashboard Diagnosis Report` function main() { ReactDOM.render( , document.getElementById('root') ) } main() window.addEventListener('dashboard:diagnose_report_event', function (event) { main() }) ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/diagnoseReportApp/react-app-env.d.ts ================================================ /// // https://stackoverflow.com/questions/12709074/how-do-you-explicitly-set-a-new-property-on-window-in-typescript interface Window { __diagnosis_data__: any } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/diagnoseReportApp/translations/en.yaml ================================================ diagnosis: title: '{{distro.tidb}} Cluster System Report' expand_all: Expand All fold_all: Collapse All expand: Expand fold: Collapse all_tables: Report Overview tables: category: header: Basic Info diagnose: Diagnose load: Load Info overview: Component Info TiDB: '{{distro.tidb}} Component' PD: '{{distro.pd}} Component' TiKV: '{{distro.tikv}} Component' config: Configuration Info error: Error Info title: compare_diagnose: Diagnostics Comparison compare_report_time_range: Comparison Time Range top_10_slow_query_in_time_range_t1: Top 10 Slow Queries in Time Range t1 top_10_slow_query_in_time_range_t2: top 10 Slow Queries in Time Range t2 top_10_slow_query_group_by_digest_in_time_range_t1: Top 10 Slow Queries Group by Digest in Time Range t1 top_10_slow_query_group_by_digest_in_time_range_t2: Top 10 slow queries group by digest in Time Range t2 slow_query_with_diff_plan in_in_time_range_t1: Slow Queries with Different Plan in Time Range t1 slow_query_with_diff_plan in_in_time_range_t2: Slow queries with Different Plan in Time Range t2 diagnose_in_time_range_t1: Diagnostics in Time Range t1 diagnose_in_time_range_t2: Diagnostics in Time Range t2 max_diff_item: Maximum Different Item slow_query_t2: Slow Queries In Time Range t2 generate_report_error: Report Generation Error report_time_range: Report Time Range diagnose: Diagnosis Result total_time_consume: Time Consumed by Each Component total_error: Errors Occurred in Each Component time_consume: Time Consumed tidb_time_consume: Time Consumed by {{distro.tidb}} Component transaction: '{{distro.tidb}} Transaction' tidb_connection_count: '{{distro.tidb}} Server Connections' statistics_info: Statistics Info ddl_owner: DDL Owner scheduler_initial_config: Scheduler Initial Config scheduler_change_config: Scheduler Config Change History tidb_gc_initial_config: '{{distro.tidb}} GC Initial Config' tidb_gc_change_config: '{{distro.tidb}} GC Config Change History' tikv_rocksdb_initial_config: '{{distro.tikv}} RocksDB Initial Config' tikv_rocksdb_change_config: '{{distro.tikv}} RocksDB Config Change History' tikv_raftstore_initial_config: '{{distro.tikv}} RaftStore Initial Config' tikv_raftstore_change_config: '{{distro.tikv}} RaftStore Config Change History' pd_time_consume: Time Consumed by {{distro.pd}} Component balance_leader_region: Scheduled Leader/Region Count approximate_region_size: Approximate Region Size tikv_engine_size: '{{distro.tikv}} Engine Size' tikv_time_consume: Time Consumed by {{distro.tikv}} Component scheduler_info: Scheduler Info gc_info: GC Info task_info: Task Info snapshot_info: Snapshot Info coprocessor_info: Coprocessor Info raft_info: Raft Info tikv_error: '{{distro.tikv}} Error' tidb_current_config: "{{distro.tidb}}'s Current Config" pd_current_config: "{{distro.pd}}'s Current Config" tikv_current_config: "{{distro.tikv}}'s Current Config" node_load_info: Server Load Info process_cpu_usage: Instance CPU Usage process_memory_usage: Instance Memory Usage tidb/pd_goroutines_count: '{{distro.tidb}}/{{distro.pd}} Goroutines Count' tikv_thread_cpu_usage: '{{distro.tikv}} Thread CPU Usage' store_status: Store Status cluster_status: Cluster Status etcd_status: etcd Status cluster_info: Cluster Topology Info cache_hit: Cache Hit cluster_hardware: Cluster Hardware Info rocksdb_time_consume: Time Consumed by RocksDB top_10_slow_query: Top 10 Slow Queries top_10_slow_query_group_by_digest: Top 10 Slow Queries Group By Digest slow_query_with_diff_plan: Slow Queries with Different Plan comment: compare_diagnose: Automatically diagnose the cluster problem by comparing with the reference time. max_diff_item: The maximum different metrics between two time ranges. diagnose: Automatically diagnose the cluster problem and record the problem in the table below. total_time_consume: This table contains the event time consumed in {{distro.tidb}}/{{distro.tikv}}/{{distro.pd}}. METRIC_NAME is the event name; LABEL is the event label, such as instance, event type, etc; TIME_RATIO is the TOTAL_TIME of this event divided by the TOTAL_TIME of the events whose TIME_RATIO is 1; TOTAL_TIME is the total time cost of this event; TOTAL_COUNT is the total count of this event; P999 is the max time of 0.999 quantile; P99 is the max time of 0.99 quantile; P90 is the max time of 0.90 quantile; P80 is the max time of 0.80 quantile. total_error: This table contains the total count of each error event. METRIC_NAME is the error event name; LABEL is the event label, such as instance, event type, etc; TOTAL_COUNT is the total count of this event. tidb_time_consume: This table contains the event time consumed in {{distro.tidb}}. METRIC_NAME is the event name; LABEL is the event label, such as instance, event type, etc; TIME_RATIO is the TOTAL_TIME of this event divided by the TOTAL_TIME of the events whose TIME_RATIO is 1; TOTAL_TIME is the total time cost of this event; TOTAL_COUNT is the total count of this event; P999 is the max time of 0.999 quantile; P99 is the max time of 0.99 quantile; P90 is the max time of 0.90 quantile; P80 is the max time of 0.80 quantile. transaction: This table contains the {{distro.tidb}} transaction statistics information. METRIC_NAME is the object name; LABEL is the object label, such as instance, event type, etc; TOTAL_VALUE is the total size/value of this object; TOTAL_COUNT is the total count of this object; P999 is the max size/value of 0.999 quantile; P99 is the max size/value of 0.99 quantile; P90 is the max size/value of 0.90 quantile; P80 is the max size/value of 0.80 quantile. tidb_connection_count: The number of connections of each {{distro.tidb}} server. ddl_owner: This table contains the DDL Owner info. Note that if no DDL request has been executed, the Owner info maybe null below, but it doesn't indicate that no DDL Owner exists. scheduler_initial_config: The initial config value of {{distro.pd}} scheduler. The initial time is the start time of this report. scheduler_change_config: The config change history of {{distro.pd}} scheduler. APPROXIMATE_CHANGE_TIME is the minimum start effective time. tidb_gc_initial_config: The initial config value of {{distro.tidb}} GC. The initial time is the start time of this report. tidb_gc_change_config: The config change history of {{distro.tidb}} GC. APPROXIMATE_CHANGE_TIME is the minimum start effective time. tikv_rocksdb_initial_config: The initial config value of {{distro.tikv}} RocksDB. The initial time is the start time of this report. tikv_rocksdb_change_config: The config change history of {{distro.tikv}} RocksDB. APPROXIMATE_CHANGE_TIME is the minimum start effective time. tikv_raftstore_initial_config: The initial config value of {{distro.tikv}} RaftStore. The initial time is the start time of this report. tikv_raftstore_change_config: The config change history of {{distro.tikv}} RaftStore. APPROXIMATE_CHANGE_TIME is the minimum start effective time. pd_time_consume: This table contains the event time consumed in {{distro.pd}}. METRIC_NAME is the event name; LABEL is the event label, such as instance, event type, etc; TIME_RATIO is the TOTAL_TIME of this event divided by the TOTAL_TIME of the events whose TIME_RATIO is 1; TOTAL_TIME is the total time cost of this event; TOTAL_COUNT is the total count of this event; P999 is the max time of 0.999 quantile; P99 is the max time of 0.99 quantile; P90 is the max time of 0.90 quantile; P80 is the max time of 0.80 quantile. tikv_time_consume: This table contains the event time consumed in {{distro.tikv}}. METRIC_NAME is the event name; LABEL is the event label, such as instance, event type, etc; TIME_RATIO is the TOTAL_TIME of this event divided by the TOTAL_TIME of the events whose TIME_RATIO is 1; TOTAL_TIME is the total time cost of this event; TOTAL_COUNT is the total count of this event; P999 is the max time of 0.999 quantile; P99 is the max time of 0.99 quantile; P90 is the max time of 0.90 quantile; P80 is the max time of 0.80 quantile. table: name: tidb_transaction: Transaction tidb_kv_request: KV request tidb_slow_query: Slow query tidb_ddl_handle_job: DDL job tidb_ddl_batch_add_index: Batch add index tidb_load_schema: Schema load tidb_meta_operation: '{{distro.tidb}} meta operation' tidb_auto_id_request: '{{distro.tidb}} auto ID request' tidb_statistics_auto_analyze: '{{distro.tidb}} auto analyze' tidb_gc: '{{distro.tidb}} GC' pd_client_cmd: '{{distro.pd}} client cmd' pd_handle_request: '{{distro.pd}} request' pd_handle_transactions: etcd transactions tikv_cop_request: Coprocessor request tikv_cop_handle: Coprocessor handling request tikv_handle_snapshot: Snapshot handling tikv_send_snapshot: Snapshot sending tikv_commit_log: Raft commit log tidb_transaction_retry_num: '{{distro.tidb}} transaction retry' tidb_txn_region_num: Transaction Region count tidb_txn_kv_write_num: Transaction KV write count tidb_txn_kv_write_size: Transaction KV write size tidb_load_safepoint_total_num: Safepoint load tikv_scheduler_stage_total_num: Scheduler stage tikv_worker_handled_tasks_total_num: '{{distro.tikv}} worker handled tasks' tikv_worker_pending_tasks_total_num: '{{distro.tikv}} worker pending tasks' tikv_futurepool_handled_tasks_total_num: future_pool handled tasks tikv_futurepool_pending_tasks_total_num: future_pool pending tasks tikv_snapshot_kv_count: Snapshot KV tikv_snapshot_size: Snapshot size tikv_cop_scan_keys_num: '{{distro.tikv}} Coprocessor scan keys' tikv_cop_total_response_total_size: '{{distro.tikv}} Coprocessor response' tikv_cop_scan_num: '{{distro.tikv}} Coprocessor scan' tikv_raft_sent_messages_total_num: Raft sent messages tikv_flush_messages_total_num: Raft flush messages tikv_receive_messages_total_num: Raft receive messages tikv_raft_dropped_messages_total: Raft dropped messages tikv_raft_proposals_total_num: Raft proposals tikv_grpc_error_total_count: gRPC errors tikv_critical_error_total_count: '{{distro.tikv}} critical errors' tikv_coprocessor_request_error_total_count: Coprocessor request errors node_disk_write_latency: Disk write latency node_disk_read_latency: Disk read latency sched_worker: Scheduler worker tikv_memtable_hit: memtable hit tikv_block_all_cache_hit: All block cache hit tikv_block_index_cache_hit: Index block cache hit tikv_block_filter_cache_hit: Filter block cache hit tikv_block_data_cache_hit: Data block cache hit tikv_block_bloom_prefix_cache_hit: Bloom prefix block cache hit comment: tidb_query: The time cost of SQL queries. The label is [sql_type]. tidb_get_token(us): The time cost of a session getting token to execute the SQL query. The label is [instance]. tidb_parse: The time cost of parsing SQL queries. The label is [sql_type]. tidb_compile: The time cost of building the query plan. The label is [sql_type]. tidb_execute: The time cost of executing the SQL query, which does not include the time to get the results of the query. The label is [sql_type]. tidb_distsql_execution: The time cost of distsql execution. The label is [type]. tidb_cop: The processing time of KV storage Coprocessor. The label is [instance]. tidb_transaction: The time cost of a transaction executing durations, including retry. The label is [sql_type]. tidb_transaction_local_latch_wait: The time cost of waiting for local latch. The label is [instance]. tidb_kv_backoff: The time cost of {{distro.tidb}} transaction latch waiting for key value storage. The label is [type]. tidb_kv_request: The time cost of KV requests durations. The label is [type]. tidb_slow_query: The time cost of {{distro.tidb}} slow queries. The label is [instance]. tidb_slow_query_cop_process: The total Coprocessor processing time of {{distro.tidb}} slow queries. The label is [instance]. tidb_slow_query_cop_wait: The total Coprocessor waiting time of {{distro.tidb}} slow queries. The label is [instance]. tidb_ddl_handle_job: The time cost of processing {{distro.tidb}} DDL jobs. The label is [type]. tidb_ddl_worker: The time cost of DDL worker handling jobs. The label is [action]. tidb_ddl_update_self_version: The time cost of updating {{distro.tidb}} schema syncer version. The label is [result]. tidb_owner_handle_syncer: The time cost of {{distro.tidb}} DDL owner operations on etcd. The label is [type]. tidb_ddl_batch_add_index: The time cost of {{distro.tidb}} batch adding index. The label is [type]. tidb_ddl_deploy_syncer: The time cost of {{distro.tidb}} DDL schema syncer statistics, including init, start, watch, and clear. The label is [type]. tidb_load_schema: The time cost of {{distro.tidb}} loading schema. The label is [type]. tidb_meta_operation: The time cost of {{distro.tidb}} meta operations, including get/set schema and DDL jobs. The label is [instance]. tidb_auto_id_request: The time cost of handling requests for {{distro.tidb}} auto ID. The label is [type]. tidb_statistics_auto_analyze: The time cost of {{distro.tidb}} auto analyze. The label is [type]. tidb_gc: The time cost of KV storage garbage collection. The label is [instance]. tidb_gc_push_task: The time cost of KV storage range worker processing one task. The label is [instance]. tidb_batch_client_unavailable: The time cost of KV storage batch processing unavailable. The label is [type]. tidb_batch_client_wait: The time cost of {{distro.tidb}} KV storage batch processing client requests that are waiting. The label is [instance]. pd_start_tso_wait: The time cost of waiting for the start timestamp oracle. The label is [instance]. pd_tso_rpc: The time cost from sending TSO request to receiving the response. The label is [instance]. pd_tso_wait: The time cost from the client starting to wait for the timestamp to receiving the timestamp. The label is [instance]. pd_client_cmd: The time cost of {{distro.pd}} client command. The label is [type]. pd_handle_request: The time cost of {{distro.pd}} handling request. The label is [type]. pd_grpc_completed_commands: The time cost of {{distro.pd}} completing each kind of gRPC commands. The label is [grpc_method]. pd_operator_finish: The time cost of {{distro.pd}} completing each kind of scheduling commands. The label is [type]. pd_operator_step_finish: The time cost of {{distro.pd}} completing operating steps. The label is [type]. pd_handle_transactions: The time cost of {{distro.pd}} handling etcd transactions. The label is [result]. pd_region_heartbeat: The time cost of heartbeats in each {{distro.tikv}} instance. The label is [address]. etcd_wal_fsync: The time cost of etcd writing WAL into the persistent storage. The label is [instance]. pd_peer_round_trip: The latency of the network. The label is [To]. tikv_grpc_message: The time cost of handling {{distro.tikv}} gRPC messages. The label is [type]. tikv_cop_request: The time cost of Coprocessor handling read requests. The label is [req]. tikv_cop_handle: The time cost of handling Coprocessor requests. The label is [req]. tikv_cop_wait: The time cost of Coprocessor requests that wait for being handled. The label is [req]. tikv_scheduler_command: The time cost of executing commit command. The label is [type]. tikv_scheduler_latch_wait: The waiting time of {{distro.tikv}} latch in commit command. The label is [type]. tikv_handle_snapshot: The time cost of handling snapshots. The label is [type]. tikv_send_snapshot: The time cost of sending snapshots. The label is [instance]. tikv_storage_async_request: The time cost of processing asynchronous snapshot requests. The label is [type]. tikv_raft_append_log: The time cost of Raft appends log. The label is [instance]. tikv_raft_apply_log: The time cost of Raft apply log. The label is [instance]. tikv_raft_apply_wait: The time cost of Raft apply wait. The label is [instance]. tikv_raft_process: The time cost of peer processes in Raft. The label is [instance]. tikv_raft_propose_wait: The waiting time of each proposal. The label is [type]. tikv_raft_store_events: The time cost of raftstore events. The label is [type]. tikv_commit_log: The time cost of Raft commits log. The label is [instance]. tikv_check_split: The time cost of running split check. The label is [instance]. tikv_ingest_sst: The time cost of ingesting SST files. The label is [instance]. tikv_gc_tasks: The time cost of executing GC tasks. The label is [task]. tikv_pd_request: The time cost of {{distro.tikv}} sending requests to {{distro.pd}}. The label is [type]. tikv_lock_manager_deadlock_detect: tikv_lock_manager_waiter_lifetime: tikv_backup_range: tikv_backup: tidb_transaction_retry_num: '{{distro.tidb}} transaction retry count. The label is [instance].' tidb_transaction_statement_num: The total number of {{distro.tidb}} statements within a transaction. Internal means the internal transaction of {{distro.tidb}}. The label is [sql_type]. tidb_txn_region_num: The number of Regions that each transaction operates. The label is [instance]. tidb_txn_kv_write_num: The number of KV writes per transaction execution. The label is [instance]. tidb_txn_kv_write_size: The KV write size per transaction execution. The label is [instance]. tidb_load_safepoint_total_num: The total count of safe point loading. The label is [instance]. tidb_lock_resolver_total_num: The total count of lock resolve. The label is [instance]. pseudo_estimation_total_count: The total count of {{distro.tidb}} Optimizer using pseudo estimation. The label is [instance, type]. dump_feedback_total_count: The total count of operations that {{distro.tidb}} dumping statistics back to KV storage. The label is [instance, type]. store_query_feedback_total_count: The total count of {{distro.tidb}} store querying feedback. The label is [instance, type]. update_stats_total_count: The total count of {{distro.tidb}} updating statistics using feed back. The label is [instance]. balance-leader-in: balance-leader-in is the total count of Leader moving into the {{distro.tikv}} store. The label is [address]. balance-leader-out: balance-leader-out is the total count of Leader moving out of the {{distro.tikv}} store. The label is [address]. balance-region-in: balance-region-in is the total count of Regions moving into the {{distro.tikv}} store. The label is [address]. balance-region-out: balance-region-in is the total count of Regions moving into the {{distro.tikv}} store. The label is [address]. Approximate Region size: The approximate Region size. The label is [instance]. store size: The storage size. The label is [instance, type]. tikv_scheduler_keys_read: The number of keys read by a command. The label is [instance, type]. tikv_scheduler_keys_written: The number of keys written by a command. The label is [instance, type]. tikv_scheduler_scan_details_total_num: The keys scan details of each CF when executing a command. The label is [instance,req,tag]. tikv_scheduler_stage_total_num: The total number of scheduler states. The label is [instance,type,stage]. tikv_gc_keys_total_num: The total number of keys in CF affected during GC. The label is [instance,cf,tag]. tidb_gc_worker_action_total_num: The total count of KV storage garbage collection. The label is [instance,type]. tikv_worker_handled_tasks_total_num: The total number of tasks handled by worker. The label is [instance,name]. tikv_worker_pending_tasks_total_num: The total number of pending and running tasks of worker. The label is [instance,name]. tikv_futurepool_handled_tasks_total_num: The total number of tasks handled by future_pool. The label is [instance,name]. tikv_futurepool_pending_tasks_total_num: The total number of pending and running tasks of future_pool. The label is [instance,name]. tikv_snapshot_kv_count: tikv_snapshot_kv_count. The label is [instance]. tikv_snapshot_size: The number of KV pairs within a snapshot. The label is [instance]. tikv_snapshot_state_total_count: tikv_snapshot_size. The label is [instance,type]. tikv_cop_scan_keys_num: The total number of {{distro.tikv}} Coprocessor scan keys. The label is [instance,req]. tikv_cop_total_response_total_size: '{{distro.tikv}} coprocessor response total size. The label is [instance].' tikv_cop_scan_num: The total number of {{distro.tikv}} coprocessor scan operations. The label is [instance,req,tag,cf]. tikv_raft_sent_messages_total_num: The total number of sent Raft messages. The label is [instance,type]. tikv_flush_messages_total_num: The total number of flushed Raft messages. The label is [instance]. tikv_receive_messages_total_num: The total number of received Raft messages. The label is [instance]. tikv_raft_dropped_messages_total: The total number of dropped Raft messages. The label is [instance,type]. tikv_raft_proposals_total_num: The total number of raft proposals. The label is [instance,type]. tikv_grpc_error_total_count: The total number of the gRPC message failures. The label is [instance,type]. tikv_critical_error_total_count: The total number of the {{distro.tikv}} critical errors. The label is [instance,type]. tikv_scheduler_is_busy_total_count: The total number of Scheduler Busy events that make the {{distro.tikv}} instance temporarily unavailable. The label is [instance,db,type,stage]. tikv_channel_full_total_count: The total number of channel full errors, which will make the {{distro.tikv}} instance temporarily unavailable. The label is [instance,db,type]. tikv_coprocessor_request_error_total_count: The total number of Coprocessor errors. The label is [instance,reason]. tikv_engine_write_stall: Indicates occurrences of Write Stall events that make the {{distro.tikv}} instance temporarily unavailable. The label is [instance,db]. tikv_server_report_failures_total_count: The total number of reported failure messages. The label is [instance]. tikv_storage_async_request_error: The total number of storage request errors. The label is [instance,status,type]. tikv_lock_manager_detect_error_total_count: The total number of {{distro.tikv}} lock manager detect error. The label is [instance,type]. tikv_backup_errors_total_count: The total number of {{distro.tikv}} lock manager detected errors. The label is [instance,error]. node_disk_write_latency: The disk write latency in each node. The label is [instance,device]. node_disk_read_latency: The disk read latency in each node. The label is [instance,device]. grpc: The CPU utilization of each {{distro.tikv}} gRPC. The label is [instance]. raftstore: The CPU utilization of {{distro.tikv}} raftstore thread. The label is [instance]. Async apply: The CPU utilization of {{distro.tikv}} async apply thread. The label is [instance]. sched_worker: The CPU utilization of {{distro.tikv}} scheduler worker thread. The label is [instance]. snapshot: The CPU utilization of {{distro.tikv}} snapshot. The label is [instance]. unified read pool: The CPU utilization of {{distro.tikv}} unified read pool thread. The label is [instance]. storage read pool: The CPU utilization of {{distro.tikv}} storage read pool thread. The label is [instance]. storage read pool normal: The CPU utilization of {{distro.tikv}} storage read pool normal thread. The label is [instance]. storage read pool high: The CPU utilization of {{distro.tikv}} storage read pool high thread. The label is [instance]. storage read pool low: The CPU utilization of {{distro.tikv}} storage read pool low thread. The label is [instance]. cop: The CPU utilization of {{distro.tikv}} Coprocessor. The label is [instance]. cop normal: The CPU utilization of {{distro.tikv}} Coprocessor normal thread. The label is [instance]. cop high: The CPU utilization of {{distro.tikv}} Coprocessor high thread. The label is [instance]. cop low: The CPU utilization of {{distro.tikv}} Coprocessor low thread. The label is [instance]. rocksdb: The CPU utilization {{distro.tikv}} RocksDB. The label is [instance]. gc: The CPU utilization of {{distro.tikv}} GC. The label is [instance]. split_check: The CPU utilization of {{distro.tikv}} split_check. The label is [instance]. region_score: The Region score of store. The label is [address]. leader_score: The Leader score of store. The label is [address]. region_count: The Region count of store. The label is [address]. leader_count: The Leader score of store. The label is [address]. region_size: The Region size of store. The label is [address]. leader_size: The Leader size of store. The label is [address]. tikv_memtable_hit: The hit rate of memtable. The label is [instance]. tikv_block_all_cache_hit: The hit rate of all block cache. The label is [instance]. tikv_block_index_cache_hit: The hit rate of index block cache. The label is [instance]. tikv_block_filter_cache_hit: The hit rate of filter block cache. The label is [instance]. tikv_block_data_cache_hit: The hit rate of data block cache. The label is [instance]. tikv_block_bloom_prefix_cache_hit: The hit rate of bloom_prefix block cache. The label is [instance]. get duration: The time consumed when RocksDB executing get operations. The label is [instance]. seek duration: The time consumed when RocksDB executing seek operations. The label is [instance]. write duration: The time consumed when RocksDB executing write operations. The label is [instance]. WAL sync duration: The time consumed when RocksDB executing WAL sync operations. The label is [instance]. compaction duration: The time consumed when RocksDB executing compaction operations. The label is [instance]. SST read duration: The time consumed when RocksDB reading SST files. The label is [instance]. write stall duration: The time cost of write stall. The label is [instance]. ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/diagnoseReportApp/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/diagnoseReportApp/translations/zh.yaml ================================================ diagnosis: title: '{{distro.tidb}} 集群系统报告' expand_all: 展开所有 fold_all: 收起所有 expand: 展开 fold: 收起 all_tables: 报告信息总览 tables: category: header: 基本信息 diagnose: 诊断 load: 负载 overview: 各组件信息总览 TiDB: '{{distro.tidb}} 组件' PD: '{{distro.pd}} 组件' TiKV: '{{distro.tikv}} 组件' config: 配置 error: 错误 title: compare_diagnose: 诊断对比 compare_report_time_range: 对比报告区间 top_10_slow_query_in_time_range_t1: t1 中的 Top 10 慢查询 top_10_slow_query_in_time_range_t2: t2 中的 Top 10 慢查询 top_10_slow_query_group_by_digest_in_time_range_t1: 按 SQL 指纹聚合的 t1 Top 10 慢查询 top_10_slow_query_group_by_digest_in_time_range_t2: 按 SQL 指纹聚合的 t2 Top 10 慢查询 slow_query_with_diff_plan_in_time_range_t1: t1 中的不同执行计划的慢查询 slow_query_with_diff_plan_in_time_range_t2: t2 中的不同执行计划的慢查询 diagnose_in_time_range_t1: t1 中的诊断信息 diagnose_in_time_range_t2: t2 中的诊断信息 max_diff_item: 最大不同项 slow_query_t2: t2 中的慢查询 generate_report_error: 生成报告的报错 report_time_range: 报告区间 diagnose: 诊断结果 total_time_consume: 各组件总耗时 total_error: 各组件总报错数 time_consume: 耗时 tidb_time_consume: '{{distro.tidb}} 中事件耗时' transaction: '{{distro.tidb}} 事务' tidb_connection_count: '{{distro.tidb}} 连接数' statistics_info: 统计信息 ddl_owner: DDL Owner scheduler_initial_config: 调度器初始配置 scheduler_change_config: 调度器配置修改历史 tidb_gc_initial_config: '{{distro.tidb}} GC 初始配置' tidb_gc_change_config: '{{distro.tidb}} GC 配置修改历史' tikv_rocksdb_initial_config: '{{distro.tikv}} RocksDB 初始配置' tikv_rocksdb_change_config: '{{distro.tikv}} RocksDB 配置修改历史' tikv_raftstore_initial_config: '{{distro.tikv}} RaftStore 初始配置' tikv_raftstore_change_config: '{{distro.tikv}} RaftStore 配置修改历史' pd_time_consume: '{{distro.pd}} 中事件耗时' balance_leader_region: Leader/Region 调度数 approximate_region_size: Approximate Region 大小 tikv_engine_size: '{{distro.tikv}} 实例存储大小' tikv_time_consume: '{{distro.tikv}} 中事件耗时' scheduler_info: '{{distro.tikv}} 调度器信息' gc_info: GC 信息 task_info: '{{distro.tikv}} 任务信息' snapshot_info: '{{distro.tikv}} 快照信息' coprocessor_info: Coprocessor 信息 raft_info: Raft 信息 tikv_error: '{{distro.tikv}} 错误' tidb_current_config: '{{distro.tidb}} 当前配置' pd_current_config: '{{distro.pd}} 当前配置' tikv_current_config: '{{distro.tikv}} 当前配置' node_load_info: 服务器负载信息 process_cpu_usage: 各实例 CPU 使用率 process_memory_usage: 各实例内存消耗 tidb/pd_goroutines_count: '{{distro.tidb}}/{{distro.pd}} 的 Goroutines 数量' tikv_thread_cpu_usage: '{{distro.tikv}} 的 CPU 使用情况' store_status: '{{distro.tikv}} 节点的存储状态' cluster_status: 集群状态 etcd_status: etcd 状态 cluster_info: 集群拓扑信息 cache_hit: 缓存命中率 cluster_hardware: 集群硬件信息 rocksdb_time_consume: RocksDB 事件耗时 top_10_slow_query: Top 10 慢查询 top_10_slow_query_group_by_digest: 按 SQL 指纹聚合的 Top 10 慢查询 slow_query_with_diff_plan: 不同执行计划的慢查询 comment: compare_diagnose: 通过与参考时间的比较,自动诊断集群问题。 max_diff_item: 两段时间中的最大不同项。 diagnose: 该表显示的是自动诊断的结果,即集群中出现的问题。 total_time_consume: 该表显示的是 {{distro.tidb}}/{{distro.tikv}}/{{distro.pd}} 组件中各事件的耗时。METRIC_NAME 是事件名称;LABEL 是事件标签,如实例、事件类型等;TIME_RATIO 是该事件的总时间除以 TIME_RATIO 为 1 的事件的总时间;TOTAL_TIME 是该事件的总耗时;TOTAL_COUNT 是该事件的总计数;P999 是 0.999 分位数的最大时间;P99 是 0.99 分位数的最大时间;P90 是 0.90 分位数的最大时间;P80 是 0.80 分位数的最大时间。 total_error: 该表显示的是各错误事件的数量。METRIC_NAME 是错误事件名称;LABEL 是事件标签,如实例、事件类型;TOTAL_COUNT 是该错误事件的总数。 tidb_time_consume: 该表显示的是 {{distro.tidb}} 组件中各事件的耗时。METRIC_NAME 是事件名称;LABEL 是事件标签,如实例、事件类型等;TIME_RATIO 是该事件的总时间除以 TIME_RATIO 为 1 的事件的总时间;TOTAL_TIME 是该事件的总耗时;TOTAL_COUNT 是该事件的总计数;P999 是 0.999 分位数的最大时间;P99 是 0.99 分位数的最大时间;P90 是 0.90 分位数的最大时间;P80 是 0.80 分位数的最大时间。 transaction: 该表显示了 {{distro.tidb}} 事务的统计信息。METRIC_NAME 是对象名;LABEL 是对象标签,如实例、事件类型等;TOTAL_VALUE 是该对象的总大小;TOTAL_COUNT 是该对象的总计数;P999 为 0.999 分位数的最大值;P99 是 0.99 分位数的最大值;P90 是 0.90 分位数的最大值;P80 是 0.80 分位数的最大值。 tidb_connection_count: '{{distro.tidb}} 服务器的连接数。' ddl_owner: DDL Owner 的信息。注意:如果没有 DDL 请求被执行,下面的 Owner 信息可能为空,这并不表示 DDL Owner 不存在。 scheduler_initial_config: '{{distro.pd}} 调度器的初始配置值。初始时间是报表的开始时间。' scheduler_change_config: '{{distro.pd}} 调度器的配置更改历史。APPROXIMATE_CHANGE_TIME 为最近的有效更改时间。' tidb_gc_initial_config: '{{distro.tidb}} GC 的初始配置值。初始时间是报表的开始时间。' tidb_gc_change_config: '{{distro.tidb}} GC 的配置更改历史。APPROXIMATE_CHANGE_TIME 为最近的有效更改时间。' tikv_rocksdb_initial_config: '{{distro.tikv}} RocksDB 的初始配置值。初始时间是报表的开始时间。' tikv_rocksdb_change_config: '{{distro.tikv}} RocksDB 的配置更改历史。APPROXIMATE_CHANGE_TIME 为最近的有效更改时间。' tikv_raftstore_initial_config: '{{distro.tikv}} RaftStore 的初始配置值。初始时间是报表的开始时间。' tikv_raftstore_change_config: '{{distro.tikv}} RaftStore 的配置更改历史。APPROXIMATE_CHANGE_TIME 为最近的有效更改时间。' pd_time_consume: 该表显示的是 {{distro.pd}} 组件中各事件的耗时。METRIC_NAME 是事件名称;LABEL 是事件标签,如实例、事件类型等;TIME_RATIO 是该事件的总时间除以 TIME_RATIO 为 1 的事件的总时间;TOTAL_TIME 是该事件的总耗时;TOTAL_COUNT 是该事件的总计数;P999 是 0.999 分位数的最大时间;P99 是 0.99 分位数的最大时间;P90 是 0.90 分位数的最大时间;P80 是 0.80 分位数的最大时间。 tikv_time_consume: 该表显示的是 {{distro.tikv}} 组件中各事件的耗时。METRIC_NAME 是事件名称;LABEL 是事件标签,如实例、事件类型等;TIME_RATIO 是该事件的总时间除以 TIME_RATIO 为 1 的事件的总时间;TOTAL_TIME 是该事件的总耗时;TOTAL_COUNT 是该事件的总计数;P999 是 0.999 分位数的最大时间;P99 是 0.99 分位数的最大时间;P90 是 0.90 分位数的最大时间;P80 是 0.80 分位数的最大时间。 table: name: tidb_transaction: '{{distro.tidb}} 事务' tidb_kv_request: '{{distro.tidb}} KV 请求' tidb_slow_query: 慢查询 tidb_ddl_handle_job: DDL 任务 tidb_ddl_batch_add_index: 批量索引添加 tidb_load_schema: Schema 加载 tidb_meta_operation: '{{distro.tidb}} 元操作' tidb_auto_id_request: '{{distro.tidb}} 自增 ID 请求' tidb_statistics_auto_analyze: '{{distro.tidb}} 自动分析' tidb_gc: 垃圾回收 pd_client_cmd: '{{distro.pd}} 客户端命令' pd_handle_request: '{{distro.pd}} 请求' pd_handle_transactions: etcd 事务 pd_peer_round_trip: 网络延迟 tikv_cop_request: Coprocessor 读请求 tikv_cop_handle: Coprocessor 请求 tikv_handle_snapshot: 快照处理 tikv_send_snapshot: 快照发送 tikv_commit_log: Raft 提交日志 tidb_transaction_retry_num: '{{distro.tidb}} 事务重试数' tidb_txn_region_num: 事务操作的 Region 数量 tidb_txn_kv_write_num: 事务执行的 KV 写入数量 tidb_txn_kv_write_size: 事务执行的 KV 写入大小 tidb_load_safepoint_total_num: 安全点装载总数量 tikv_scheduler_stage_total_num: 调度程序状态的总数量 tikv_worker_handled_tasks_total_num: worker 处理的任务总数量 tikv_worker_pending_tasks_total_num: 工作进程的挂起和运行任务的总数量 tikv_futurepool_handled_tasks_total_num: future_pool 处理的任务总数量 tikv_futurepool_pending_tasks_total_num: future_pool 总挂起和运行任务数量 tikv_snapshot_kv_count: 快照的 KV 数量 tikv_snapshot_size: 快照大小 tikv_cop_scan_keys_num: '{{distro.tikv}} Coprocessor 扫描键总数量' tikv_cop_total_response_total_size: '{{distro.tikv}} Coprocessor 响应总大小' tikv_cop_scan_num: '{{distro.tikv}} Coprocessor 扫描操作总数量' tikv_raft_sent_messages_total_num: 发送的 Raft 消息的总数量 tikv_flush_messages_total_num: 持久化 Raft 消息的总数量 tikv_receive_messages_total_num: 接受 Raft 消息的总数量 tikv_raft_dropped_messages_total: 丢弃 Raft 消息的总数量 tikv_raft_proposals_total_num: Raft proposal 的总数量 tikv_grpc_error_total_count: gRPC 消息失败的总数量 tikv_critical_error_total_count: '{{distro.tikv}} 临界误差的总数量' tikv_coprocessor_request_error_total_count: Coprocessor 错误总数量 node_disk_write_latency: 磁盘写延迟 node_disk_read_latency: 磁盘读取延迟 sched_worker: 调度器工作线程 tikv_memtable_hit: memtable 命中率 tikv_block_all_cache_hit: 所有块缓存命中率 tikv_block_index_cache_hit: 索引块缓存命中率 tikv_block_filter_cache_hit: 过滤块缓存命中率 tikv_block_data_cache_hit: 数据块缓存命中率 tikv_block_bloom_prefix_cache_hit: bloom_prefix 块缓存命中率 comment: tidb_query: SQL 查询耗时,标签是"SQL 类型"。 tidb_get_token(us): 会话获取令牌以执行 SQL 查询的耗时,标签是"实例"。 tidb_parse: 解析 SQL 的耗时,标签是"SQL 类型"。 tidb_compile: 构建查询计划的时间,标签是"SQL 类型"。 tidb_execute: 执行 SQL 的时间,不包括获得查询结果的时间,标签是"SQL 类型"。 tidb_distsql_execution: 执行 distsql 的耗时,标签是"类型"。 tidb_cop: KV storage Coprocessor 处理的耗时,标签是"实例"。 tidb_transaction: 事务执行 durations 的时间成本,包括重试,标签是"SQL 类型"。 tidb_transaction_local_latch_wait: 事务执行时本地锁占用的时间,标签是"实例"。 tidb_kv_backoff: '{{distro.tidb}} 事务锁等待键值存储的时间,标签是"类型"。' tidb_kv_request: KV 请求 durations 的耗时,标签是"类型"。 tidb_slow_query: '{{distro.tidb}} 慢查询的时间开销,标签是"实例"。' tidb_slow_query_cop_process: '{{distro.tidb}} 的慢查询总 cop 处理的耗时,标签是"实例"。' tidb_slow_query_cop_wait: '{{distro.tidb}} 的慢查询总 cop 的等待时间,标签是"实例"。' tidb_ddl_handle_job: 处理 {{distro.tidb}} DDL 任务的耗时,标签是"类型"。 tidb_ddl_worker: DDL worker 处理任务的耗时,标签是"实例"。 tidb_ddl_update_self_version: '{{distro.tidb}} schema 同步器版本更新的耗时,标签是"结果"。' tidb_owner_handle_syncer: 在 etcd 上执行 {{distro.tidb}} DDL 所有者操作的耗时,标签是"类型"。 tidb_ddl_batch_add_index: '{{distro.tidb}} 批量添加索引的耗时,标签是"类型"。' tidb_ddl_deploy_syncer: '{{distro.tidb}} DDL schema 同步器统计的时间成本,包括 init、start、watch、clear,标签是"类型"。' tidb_load_schema: 加载 {{distro.tidb}} schema 的时间成本,标签是"类型"。 tidb_meta_operation: '{{distro.tidb}} 元操作的时间成本,包括 get/set 模式和 DDL 作业,标签是"实例"。' tidb_auto_id_request: '{{distro.tidb}} 自增 ID 处理 ID 请求的耗时,标签是"类型"。' tidb_statistics_auto_analyze: 自动分析 {{distro.tidb}} 的耗时,标签是"类型"。 tidb_gc: KV 存储垃圾回收的时间,标签是"实例"。 tidb_gc_push_task: KV 存储范围内 worker 处理一项任务的耗时,标签是"实例"。 tidb_batch_client_unavailable: KV 存储批量处理不可用的耗时,标签是"类型"。 tidb_batch_client_wait: KV 存储批量处理客户端等待请求的耗时,标签是"实例"。 pd_start_tso_wait: 等待获取开始时间戳 timestamp 的耗时,标签是"实例"。 pd_tso_rpc: 发送 TSO 请求直到收到响应的时间,标签是"实例"。 pd_tso_wait: 客户端开始等待 timestamp 直到收到 timestamp 结果的耗时,标签是"实例"。 pd_client_cmd: '{{distro.pd}} 客户端命令的耗时,标签是"类型"。' pd_handle_request: '{{distro.pd}} 处理请求的耗时,标签是"类型"。' pd_grpc_completed_commands: '{{distro.pd}} 完成各种 gRPC 命令的耗时,标签是"gRPC 方法"。' pd_operator_finish: '{{distro.pd}} 完成各种调度命令的时间,标签是"类型"。' pd_operator_step_finish: '{{distro.pd}} 完成操作步骤的耗时,标签是"类型"。' pd_handle_transactions: '{{distro.pd}} 处理 etcd 事务的耗时,标签是"结果"。' pd_region_heartbeat: 每个 {{distro.tikv}} 实例中心跳的耗时,标签是"服务地址"。 etcd_wal_fsync: etcd 将 WAL 写入持久存储器的耗时,标签是"实例"。 pd_peer_round_trip: 网络的延迟,标签是"实例"。 tikv_grpc_message: gRPC 报文的 {{distro.tikv}} 处理耗时,标签是"类型"。 tikv_cop_request: Coprocessor 处理读请求的时间开销,标签是"请求"。 tikv_cop_handle: 处理 Coprocessor 请求的时间开销,标签是"请求"。 tikv_cop_wait: Coprocessor 请求等待处理的耗时,标签是"请求"。 tikv_scheduler_command: 执行 commit 命令的耗时,标签是"类型"。 tikv_scheduler_latch_wait: 提交命令中 {{distro.tikv}} 锁存器等待的时间开销,标签是"类型"。 tikv_handle_snapshot: 处理快照的时间开销,标签是"类型"。 tikv_send_snapshot: 发送快照的时间开销,标签是"实例"。 tikv_storage_async_request: 处理异步快照请求的时间开销,标签是"类型"。 tikv_raft_append_log: Raft appends log 的时间开销,标签是"实例"。 tikv_raft_apply_log: Raft apply log 的时间开销,标签是"实例"。 tikv_raft_apply_wait: Raft apply wait 的时间开销,标签是"实例"。 tikv_raft_process: Peer processes in Raft 的时间开销,标签是"实例"。 tikv_raft_propose_wait: 每一个 Raft 提议的等待时间,标签是"类型"。 tikv_raft_store_events: RaftStore events 的时间开销,标签是"类型"。 tikv_commit_log: Raft 提交日志的时间开销,标签是"实例"。 tikv_check_split: 运行分割检查的耗时,标签是"实例"。 tikv_ingest_sst: Ingest SST 文件的耗时,标签是"实例"。 tikv_gc_tasks: 执行 GC 任务的耗时,标签是"任务"。 tikv_pd_request: '{{distro.tikv}} 向 {{distro.pd}} 发送请求的耗时,标签是"类型"。' tikv_lock_manager_deadlock_detect: tikv_lock_manager_waiter_lifetime: tikv_backup_range: tikv_backup: tidb_transaction_retry_num: '{{distro.tidb}} 事务重试次数,标签是"实例"。' tidb_transaction_statement_num: 一个事务中 {{distro.tidb}} 语句数的总数量。Internal 是指 {{distro.tidb}} 内部事务,标签是"实例"。' tidb_txn_region_num: 每个事务进行操作的区域数量,标签是"实例"。 tidb_txn_kv_write_num: 每个事务执行的 KV 写入数量,标签是"实例"。 tidb_txn_kv_write_size: 每个事务执行的 KV 写入大小,标签是"实例"。 tidb_load_safepoint_total_num: 安全点装载总数量,标签是"实例"。 tidb_lock_resolver_total_num: lock resolve 的总数量,标签是"实例"。 pseudo_estimation_total_count: 使用伪估计的 {{distro.tidb}} 优化器的总数量,标签是"实例","类型"。 dump_feedback_total_count: '{{distro.tidb}} 转储统计数据回 KV 存储的操作总数量,标签是"实例"。' store_query_feedback_total_count: '{{distro.tidb}} 存储查询反馈的总数量,标签是"实例"。' update_stats_total_count: 使用反馈更新统计数据的 {{distro.tidb}} 总数量,标签是"实例"。 blance-leader-in: Leader 移动到 {{distro.tikv}} 存储的总数量,标签是"实例"。 blance-leader-out: Leader 移出 {{distro.tikv}} 存储的总数量,标签是"实例"。 blance-region-in: 移动到 {{distro.tikv}} 存储的 Region 总数量,标签是"实例"。 blance-region-out: 移出 {{distro.tikv}} 存储的的 Region 总数量,标签是"实例"。 Approximate Region size: 近似 Region 大小,标签是"实例"。 store size: 存储大小,标签是"实例"。 tikv_scheduler_keys_read: 由一条命令读取的键数量,标签是"实例","类型"。 tikv_scheduler_keys_written: 由一条命令写入的键数量,标签是"实例","类型"。 tikv_scheduler_scan_details_total_num: 在执行一条命令时,扫描每个 CF 的详细信息的总数量,标签是"实例"。 tikv_scheduler_stage_total_num: 调度程序状态的总数量,标签是"实例","阶段","类型"。 tikv_gc_keys_total_num: GC 期间 CF 中受影响的键的总数量,标签是"实例"。 tidb_gc_worker_action_total_num: KV 存储垃圾回收总量,标签是"实例","类型"。 tikv_worker_handled_tasks_total_num: worker 处理的任务总数量,标签是"实例"。 tikv_worker_pending_tasks_total_num: 工作进程的挂起和运行任务的总数量,标签是"实例"。 tikv_futurepool_handled_tasks_total_num: future_pool 处理的任务总数量,标签是"实例"。 tikv_futurepool_pending_tasks_total_num: future_pool 的总挂起和运行任务,标签是"实例"。 tikv_snapshot_kv_count: tikv_snapshot_kv_count,标签是"实例"。 tikv_snapshot_size: 快照内 KV 的数量,标签是"实例"。 tikv_snapshot_state_total_count: '{{distro.tikv}} 的快照大小,标签是"实例","类型"。' tikv_cop_scan_keys_num: '{{distro.tikv}} Coprocessor 扫描键总数量,标签是"实例"。' tikv_cop_total_response_total_size: '{{distro.tikv}} Coprocessor 响应总大小,标签是"实例"。' tikv_cop_scan_num: '{{distro.tikv}} Coprocessor 扫描操作总数量,标签是"实例"。' tikv_raft_sent_messages_total_num: 发送的 Raft 消息的总数量,标签是"实例","类型"。 tikv_flush_messages_total_num: Raft 上刷新了的信息总数量,标签是"实例"。 tikv_receive_messages_total_num: Raft 收到的的信息总数量,标签是"实例"。 tikv_raft_dropped_messages_total: Raft 丢掉的的信息总数量,标签是"实例","类型"。 tikv_raft_proposals_total_num: Raft 提议的的总数量,标签是"实例","类型"。 tikv_grpc_error_total_count: gRPC 消息失败的总数量,标签是"实例","类型"。 tikv_critical_error_total_count: '{{distro.tikv}} 临界误差的总数量,标签是"实例","类型"。' tikv_scheduler_is_busy_total_count: 使 {{distro.tikv}} 实例暂时不可用的调度器繁忙事件的总数量,标签是"实例"。 tikv_channel_full_total_count: 通道完全错误的总数量,它将使 {{distro.tikv}} 实例暂时不可用,标签是"实例"。 tikv_coprocessor_request_error_total_count: Coprocessor 错误的总数量,标签是"实例","原因"。 tikv_engine_write_stall: 指示使 {{distro.tikv}} 实例暂时不可用的写失速事件,标签是"实例"。 tikv_server_report_failures_total_count: 报告失败消息的总数量,标签是"实例"。 tikv_storage_async_request_error: 存储请求错误的总数量,标签是"实例","状态","类型"。 tikv_lock_manager_detect_error_total_count: '{{distro.tikv}} 锁管理器检测错误的总数量,标签是"实例","类型"。' tikv_backup_errors_total_count: '{{distro.tikv}} 锁管理的总错误,标签是"实例","错误"。' node_disk_write_latency: 每个节点的磁盘写延迟,标签是"实例","设备"。 node_disk_read_latency: 每个节点的磁盘读取延迟,标签是"实例","设备"。 grpc: 每个 {{distro.tikv}} gRPC 的 CPU 利用率,标签是"实例"。' raftstore: '{{distro.tikv}} RaftStore 线程的 CPU 利用率,标签是"实例"。' Async apply: '{{distro.tikv}} 异步应用线程的 CPU 利用率,标签是"实例"。' sched_worker: '{{distro.tikv}} 调度器工作线程的 CPU 利用率,标签是"实例"。' snapshot: '{{distro.tikv}} 快照的 CPU 利用率,标签是"实例"。' unified read pool: '{{distro.tikv}} 统一读池线程的 CPU 利用率,标签是"实例"。' storage read pool: '{{distro.tikv}} 存储读池线程的 CPU 利用率,标签是"实例"。' storage read pool normal: '{{distro.tikv}} 存储读池普通线程的 CPU 利用率,标签是"实例"。' storage read pool high: '{{distro.tikv}} 存储较高读线程的 CPU 利用率,标签是"实例"。' storage read pool low: '{{distro.tikv}} 存储较低读线程的 CPU 利用率,标签是"实例"。' cop: '{{distro.tikv}} Coprocessor 的 CPU 利用率,标签是"实例"。' cop normal: '{{distro.tikv}} Coprocessor 普通线程的 CPU 利用率,标签是"实例"。' cop high: '{{distro.tikv}} Coprocessor 高线程的 CPU 利用率,标签是"实例"。' cop low: '{{distro.tikv}} Coprocessor 低线程的 CPU 利用率,标签是"实例"。' rocksdb: '{{distro.tikv}} RocksDB 的 CPU 利用率,标签是"实例"。' gc: '{{distro.tikv}} GC 的 CPU 利用率,标签是"实例"。' split_check: '{{distro.tikv}} split_chec 的 CPU 利用率,标签是"实例"。' region_score: store 的 Region 得分,标签是"服务地址"。 leader_score: store 的 Leader 得分,标签是"服务地址"。 region_count: store 的 Region 数量,标签是"服务地址"。 leader_count: store 的 Leader 数量,标签是"服务地址"。 region_size: store 的 Region 大小,标签是"服务地址"。 leader_size: store 的 Leader 大小,标签是"服务地址"。 tikv_memtable_hit: memtable 的命中率,标签是"实例"。 tikv_block_all_cache_hit: 所有块缓存的命中率,标签是"实例"。 tikv_block_index_cache_hit: 索引块缓存的命中率,标签是"实例"。 tikv_block_filter_cache_hit: 过滤块缓存的命中率,标签是"实例"。 tikv_block_data_cache_hit: 数据块缓存的命中率,标签是"实例"。 tikv_block_bloom_prefix_cache_hit: bloom_prefix 块缓存的命中率,标签是"实例"。 get duration: RocksDB 执行 get 操作的耗时,标签是"实例"。 seek duration: RocksDB 执行 seek 操作的耗时,标签是"实例"。 write duration: RocksDB 执行写操作的耗时,标签是"实例"。 WAL sync duration: RocksDB 执行 WAL 同步操作的耗时,标签是"实例"。 compaction duration: RocksDB 执行压缩操作的耗时,标签是"实例"。 SST read duration: RocksDB 读取 SST 文件的耗时,标签是"实例"。 write stall duration: 由写停顿引起的时间,标签是"实例"。 ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/diagnoseReportApp/types.ts ================================================ import { createContext } from 'react' export interface TableRowDef { values: string[] sub_values: string[][] comment: string } export interface TableDef { category: string[] title: string comment: string column: string[] rows: TableRowDef[] } export const ExpandContext = createContext(false) ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/react-app-env.d.ts ================================================ declare module '*.module.css' { const classes: { readonly [key: string]: string } export default classes } declare module '*.module.less' { const classes: { readonly [key: string]: string } export default classes } declare module '*.yaml' { const classes: { readonly [key: string]: string } export default classes } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/styles/override.less ================================================ // reset ::selection pseudo element values ::selection { color: currentColor; background-color: #b3d6ff; } html { font-size: 14px; } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/styles/style.less ================================================ @root-entry-name: default; @import 'antd/lib/style/components.less'; // it is expected to import 'antd/es/style/components.less' but it doesn't exist this file ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/utils/distro/assetsRes.ts ================================================ import publicPathPrefix from '../publicPathPrefix' const lightLogoSvg = `${publicPathPrefix}/distro-res/logo-icon-light.svg` export { lightLogoSvg } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/utils/distro/stringsRes.ts ================================================ import { updateDistro } from '@pingcap/tidb-dashboard-lib' import defDistroStringsRes from './strings_res.json' let distro = defDistroStringsRes // it is a base64 encoded string let distroStringsRes = document .querySelector('meta[name="x-distro-strings-res"]') ?.getAttribute('content') if (distroStringsRes && distroStringsRes !== '__DISTRO_STRINGS_RES__') { try { const distroObj = JSON.parse(atob(distroStringsRes)) distro = { ...defDistroStringsRes, ...distroObj } } catch (error) { console.log(error) } } updateDistro(distro) ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/utils/distro/strings_res.json ================================================ { "tidb": "TiDB", "tikv": "TiKV", "pd": "PD", "tiflash": "TiFlash", "ticdc": "TiCDC", "tiproxy": "TiProxy", "tso": "TSO", "scheduling": "Scheduling" } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/utils/globalConfig.ts ================================================ import { IConProfilingConfig, IKeyVizConfig, IOverviewConfig, ISlowQueryConfig, IStatementConfig, ITopSQLConfig, ITopSlowQueryConfig } from '@pingcap/tidb-dashboard-lib' export type AppOptions = { lang: string hideNav: boolean // hidePageLoadProgress controls whether show the thin progress bar in the top of the page when switching pages hidePageLoadProgress: boolean skipNgmCheck: boolean skipLoadAppInfo: boolean skipReloadWhoAmI: boolean } export const defAppOptions: AppOptions = { lang: 'en', hideNav: false, hidePageLoadProgress: false, skipNgmCheck: false, skipLoadAppInfo: false, skipReloadWhoAmI: false } export type ClientOptions = { apiPathBase: string apiToken: string } export type ClusterInfo = { provider?: string region?: string orgId?: string projectId?: string clusterId?: string deployType?: string // dedicated / shared env?: string } export type AppsConfig = { overview?: Partial slowQuery?: Partial topSlowQuery?: Partial statement?: Partial topSQL?: Partial conProf?: Partial keyViz?: Partial } export type GlobalConfig = { appOptions?: AppOptions clientOptions: ClientOptions clusterInfo: ClusterInfo appsConfig?: AppsConfig // internal api for performance insight performanceInsightBaseUrl: string // appsDisabled has a higher priority than appsEnabled appsDisabled?: string[] appsEnabled?: string[] } // export const GlobalConfigContext = createContext(null) // export const GlobalConfigProvider = GlobalConfigContext.Provider ///////////////////////////////////// let _globalConfig: GlobalConfig export function setGlobalConfig(c: GlobalConfig) { _globalConfig = c } export function getGlobalConfig(): GlobalConfig { return _globalConfig } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/utils/publicPathPrefix.ts ================================================ let prefix = '.' export default prefix ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/utils/registry.ts ================================================ import React from 'react' import ReactDOM from 'react-dom' import singleSpaReact from 'single-spa-react' import * as singleSpa from 'single-spa' import { i18n, routing } from '@pingcap/tidb-dashboard-lib' import { AppOptions, getGlobalConfig } from './globalConfig' export default class AppRegistry { public defaultRouter = '' public apps = {} public constructor(public appOptions: AppOptions) {} static newReactSpaApp = function (rootComponentAsyncLoader, targetDomId) { const reactLifecycles = singleSpaReact({ React, ReactDOM, loadRootComponent: async () => { const component = await rootComponentAsyncLoader() if (component.default) { return component.default } return component }, domElementGetter: () => document.getElementById(targetDomId)! }) return { bootstrap: [reactLifecycles.bootstrap], mount: [reactLifecycles.mount], unmount: [reactLifecycles.unmount] } } /** * Register a TiDB Dashboard application. * * This function is a light encapsulation over single-spa's registerApplication * which provides some extra registry capabilities. * * @param {{ * id: string, * reactRoot: Function, * routerPrefix: string, * indexRoute: string, * isDefaultRouter: boolean, * icon: string, * }} app */ register(app) { // return if this app is disabled const disabledApps = getGlobalConfig().appsDisabled if (disabledApps && disabledApps.includes(app.id)) { return this } const enabledApps = getGlobalConfig().appsEnabled if (enabledApps && !enabledApps.includes(app.id)) { return this } if (app.translations) { i18n.addTranslations(app.translations) } singleSpa.registerApplication( app.id, AppRegistry.newReactSpaApp(app.reactRoot, '__spa_content__'), () => { return routing.isLocationMatchPrefix(app.routerPrefix) }, { registry: this, app } ) if (!app.indexRoute) { app.indexRoute = app.routerPrefix } if (!this.defaultRouter || app.isDefaultRouter) { this.defaultRouter = app.indexRoute } this.apps[app.id] = app return this } /** * Get the default router for initial routing. */ getDefaultRouter() { return this.defaultRouter || '/' } /** * Get the registry of the current active app. */ getActiveApp() { const mountedApps = singleSpa.getMountedApps() for (let i = 0; i < mountedApps.length; i++) { const app = mountedApps[i] if (this.apps[app] !== undefined) { return this.apps[app] } } } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/src/utils/store.ts ================================================ import { store, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { InfoInfoResponse } from '~/client' export async function reloadWhoAmI(): Promise { try { const resp = await client.getInstance().infoWhoami({ handleError: 'custom' } as ReqConfig) store.update((s) => { s.whoAmI = resp.data }) return true } catch (ex) { store.update((s) => { s.whoAmI = undefined }) return false } } export async function mustLoadAppInfo(): Promise { const resp = await client.getInstance().infoGet({ handleError: 'custom' } as ReqConfig) store.update((s) => { s.appInfo = resp.data }) return resp.data } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-cloud/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "noEmit": true, "noImplicitAny": false, "noImplicitThis": false, "baseUrl": ".", "paths": { "~/*": ["src/*"] } }, "include": ["src"] } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/README.md ================================================ # TiDB Dashboard for Clinic NPM: [@pingcap/tidb-dashboard-for-clinic-op](https://www.npmjs.com/package/@pingcap/tidb-dashboard-for-clinic-op) ## How to Use ```html
``` ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/builder.js ================================================ const path = require('path') const fs = require('fs-extra') const glob = require('glob') const md5File = require('md5-file') const chalk = require('chalk') const { watch } = require('chokidar') const { build } = require('esbuild') const postCssPlugin = require('@baurine/esbuild-plugin-postcss3') const autoprefixer = require('autoprefixer') const { yamlPlugin } = require('esbuild-plugin-yaml') const { lessModifyVars, lessGlobalVars } = require('../../less-vars') const isDev = process.env.NODE_ENV !== 'production' // load env const dotenv = require('dotenv') const envFile = isDev ? './.env.development' : './.env.production' dotenv.config({ path: path.resolve(process.cwd(), envFile) }) if (isDev && fs.pathExistsSync(path.resolve(process.cwd(), '.env.local'))) { dotenv.config({ path: '.env.local', override: true }) } const outDir = 'dist' const clinicUIDashboardPath = process.env.CLINIC_UI_DASHBOARD_PATH function genDefine() { const define = {} for (const k in process.env) { if (k.startsWith('REACT_APP_')) { let envVal = process.env[k] // Example: REACT_APP_VERSION=$npm_package_version // Expect output: REACT_APP_VERSION=0.1.0 if (envVal.startsWith('$')) { envVal = process.env[envVal.substring(1)] } define[`process.env.${k}`] = JSON.stringify(envVal) } } return define } // customized plugin: log time const logTime = (_options = {}) => ({ name: 'logTime', setup(build) { let time build.onStart(() => { time = new Date() console.log(`Build started`) }) build.onEnd(() => { console.log(`Build ended: ${chalk.yellow(`${new Date() - time}ms`)}`) }) } }) const esbuildParams = { color: true, entryPoints: { main: 'src/index.tsx' }, outdir: outDir, minify: !isDev, format: 'esm', bundle: true, sourcemap: true, logLevel: 'error', incremental: true, platform: 'browser', plugins: [ postCssPlugin.default({ lessOptions: { modifyVars: lessModifyVars, globalVars: lessGlobalVars, javascriptEnabled: true }, enableCache: true, plugins: [autoprefixer] }), yamlPlugin(), logTime() ], define: genDefine(), inject: ['./process-shim.js'] // fix runtime crash } function updateHtmlFiles(htmlFiles) { const jsContentHash = md5File.sync(`./${outDir}/main.js`) const cssContentHash = md5File.sync(`./${outDir}/main.css`) const packageVersion = process.env.npm_package_version htmlFiles.forEach(function (htmlFile) { let result = fs.readFileSync(htmlFile).toString() result = result.replaceAll('%JS_CONTENT_HASH%', jsContentHash.slice(0, 7)) result = result.replaceAll('%CSS_CONTENT_HASH%', cssContentHash.slice(0, 7)) result = result.replaceAll('%PACKAGE_VERSION%', packageVersion) fs.writeFileSync(htmlFile, result) }) } function handleAssets() { fs.copySync('./public', `./${outDir}`) const htmlFiles = glob.sync(`./${outDir}/**/*.html`) updateHtmlFiles(htmlFiles) } function copyAssets() { // copy out dir to clinic ui repo if (!fs.existsSync(clinicUIDashboardPath)) { throw new Error( `clini ui dashboard path ${clinicUIDashboardPath} doesn't exist, please change it by your local path` ) } fs.removeSync(clinicUIDashboardPath) fs.copySync(`./${outDir}`, clinicUIDashboardPath) console.log('copy dashboard to clinic ui') } async function main() { fs.removeSync(`./${outDir}`) const builder = await build(esbuildParams) handleAssets() function rebuild() { builder.rebuild().catch((err) => console.log(err)) } if (isDev) { watch(`src/**/*`, { ignoreInitial: true }).on('all', () => { rebuild() }) watch('public/**/*', { ignoreInitial: true }).on('all', () => { handleAssets() }) // watch "node_modules/@pingcap/tidb-dashboard-lib/dist/**/*" triggers too many rebuild // so we just watch index.js to refine the experience watch('node_modules/@pingcap/tidb-dashboard-lib/dist/index.js', { ignoreInitial: true }).on('all', () => { rebuild() }) watch(`dist/**/*`).on('all', () => { copyAssets() }) } else { process.exit(0) } } main() ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/gulpfile.js ================================================ const { task, series, parallel } = require('gulp') const shell = require('gulp-shell') task('tsc:watch', shell.task('tsc --watch')) task('tsc:check', shell.task('tsc')) // https://www.npmjs.com/package/eslint-watch task('lint:watch', shell.task('esw --watch --cache --ext .tsx,.ts .')) task('lint:check', shell.task('esw --cache --ext tsx,ts .')) task('esbuild:dev', shell.task('NODE_ENV=development node builder.js')) task('esbuild:build', shell.task('NODE_ENV=production node builder.js')) task('dev', parallel('tsc:watch', 'lint:watch', 'esbuild:dev')) task('build', parallel('tsc:check', 'lint:check', 'esbuild:build')) ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/package.json ================================================ { "name": "@pingcap/tidb-dashboard-for-clinic-op", "version": "0.0.5", "main": "dist/main.js", "module": "dist/main.js", "files": [ "dist/*.js", "dist/*.css", "dist/*.map", "package.json", "README.md" ], "scripts": { "prepublish": "pnpm build", "dev": "gulp dev", "build": "gulp build" }, "keywords": [], "author": "", "license": "ISC", "repository": { "type": "git", "url": "https://github.com/pingcap/tidb-dashboard.git" }, "dependencies": { "@pingcap/clinic-client": "workspace:^1.0.0", "@pingcap/tidb-dashboard-lib": "workspace:^1.0.0", "antd": "^4.18.7", "axios": "^1.12.0", "i18next": "^23.7.11", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "6" }, "devDependencies": { "@types/node": "^16.9.1", "autoprefixer": "^10.4.2", "chalk": "4.1.2", "chokidar": "^3.5.2", "dotenv": "^16.0.1", "esbuild": "^0.14.23", "esbuild-plugin-svgr": "^1.0.0", "esbuild-plugin-yaml": "^0.0.1", "eslint-watch": "^8.0.0", "fs-extra": "^10.0.0", "glob": "^8.0.3", "gulp": "^4.0.2", "gulp-shell": "^0.8.0", "live-server": "^1.2.1", "md5-file": "^5.0.0", "rimraf": "^3.0.2", "typescript": "^4.7.3" } } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/process-shim.js ================================================ export let process = { // cwd: () => '', env: {} // to avoid `process.env` undefined in runtime } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/public/index.html ================================================
================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/App.tsx ================================================ import React, { useEffect, useState } from 'react' import SlowQuery from './apps/SlowQuery' function getLocHashPrefix() { let urlHashPath = window.location.hash const questionMarkPos = urlHashPath.indexOf('?') if (questionMarkPos > 0) { urlHashPath = urlHashPath.slice(0, questionMarkPos) } return urlHashPath.split('/')[1] } export default function () { const [locHashPrefix, setLocHashPrefix] = useState(() => getLocHashPrefix()) useEffect(() => { function handleRouteChange() { const curLocHashPrefix = getLocHashPrefix() if (curLocHashPrefix !== locHashPrefix) { setLocHashPrefix(curLocHashPrefix) } } window.addEventListener('dashboard:route-change', handleRouteChange) return () => { window.removeEventListener('dashboard:route-change', handleRouteChange) } }, [locHashPrefix]) if (locHashPrefix === 'slow_query') { return } return

No Matched Route!

} ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/apps/SlowQuery/context.ts ================================================ import { ISlowQueryDataSource, ISlowQueryEvent, ISlowQueryContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' export type DsExtra = { oid: string cid: string itemID: string beginTime: number endTime: number curQueryID: string } class DataSource implements ISlowQueryDataSource { constructor(public extra: DsExtra) {} getDatabaseList(beginTime: number, endTime: number, options?: ReqConfig) { // return Promise.reject(new Error('no need to implemented')) return Promise.resolve({ data: [], status: 200, statusText: 'ok', headers: {}, config: {} } as any) } infoListResourceGroupNames(options?: ReqConfig) { // return Promise.reject(new Error('no need to implemented')) return Promise.resolve({ data: [], status: 200, statusText: 'ok', headers: {}, config: {} } as any) } slowQueryAvailableFieldsGet(options?: ReqConfig) { // return Promise.reject(new Error('no need to implemented')) return Promise.resolve({ data: [], status: 200, statusText: 'ok', headers: {}, config: {} } as any) } slowQueryListGet( beginTime?: number, db?: Array, desc?: boolean, digest?: string, endTime?: number, fields?: string, limit?: number, orderBy?: string, plans?: Array, resourceGroup?: Array, text?: string, showInternal?: boolean, options?: ReqConfig ) { return client.getInstance().orgsOidClustersCidSlowqueriesGet( { xCsrfToken: client.getToken(), oid: this.extra.oid, itemID: this.extra.itemID, cid: this.extra.cid, beginTime, endTime, db, limit, text, orderBy, desc, plans, digest }, options ) } slowQueryDetailGet( connectId?: string, digest?: string, timestamp?: number, options?: ReqConfig ) { return client.getInstance().orgsOidClustersCidSlowqueriesQueryidGet( { xCsrfToken: client.getToken(), oid: this.extra.oid, itemID: this.extra.itemID, cid: this.extra.cid, queryid: this.extra.curQueryID }, options ) } slowQueryDownloadTokenPost(request: any, options?: ReqConfig) { return Promise.reject(new Error('no need to implemented')) // return Promise.resolve({ // data: '', // status: 200, // statusText: 'ok', // headers: {}, // config: {} // }) } } class EventHandler implements ISlowQueryEvent { constructor(public extra: DsExtra) {} selectSlowQueryItem(item: any) { this.extra.curQueryID = item.id } } export const ctx: (extra: DsExtra) => ISlowQueryContext = (extra) => ({ ds: new DataSource(extra), event: new EventHandler(extra), cfg: { apiPathBase: client.getBasePath(), enableExport: false, showDBFilter: false, showDigestFilter: false, showResourceGroupFilter: true } }) ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/apps/SlowQuery/index.tsx ================================================ import React from 'react' import { SlowQueryApp, SlowQueryProvider } from '@pingcap/tidb-dashboard-lib' import { ctx, DsExtra } from './context' function getDsExtra(): DsExtra { const searchParams = new URLSearchParams(window.location.search) const oid = searchParams.get('oid') || '' const cid = searchParams.get('cid') || '' const itemID = searchParams.get('item_id') || '' const urlHashParmasStr = window.location.hash.slice( window.location.hash.indexOf('?') ) const params = new URLSearchParams(urlHashParmasStr) const beginTime = parseInt(params.get('from') || '0') const endTime = parseInt(params.get('to') || '0') return { oid, cid, itemID, beginTime, endTime, curQueryID: '' } } export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/client/index.tsx ================================================ import React from 'react' import i18next from 'i18next' import axios, { AxiosInstance } from 'axios' import { message, Modal, notification } from 'antd' import { routing, i18n } from '@pingcap/tidb-dashboard-lib' import { Configuration, DefaultApi as ClinicApi } from '@pingcap/clinic-client' import translations from './translations' export * from '@pingcap/clinic-client' ////////////////////////////// const client = { _init(apiBasePath: string, apiToken: string, apiInstance: ClinicApi) { this.apiBasePath = apiBasePath this.apiToken = apiToken this.apiInstance = apiInstance }, getInstance(): ClinicApi { return this.apiInstance }, getBasePath(): string { return this.apiBasePath }, getToken(): string { return this.apiToken } } export default client ////////////////////////////// type HandleError = 'default' | 'custom' function applyErrorHandlerInterceptor(instance: AxiosInstance) { instance.interceptors.response.use(undefined, async function (err) { const { response, config } = err const handleError = config.handleError as HandleError const method = (config.method as string).toLowerCase() let errCode: string let content: string if (err.message === 'Network Error') { errCode = 'common.network' } else { errCode = response?.data?.code } if (i18next.exists(`error.${errCode ?? ''}`)) { // If there is a translation for the code, use the translation. // TODO: Better to display error details somewhere. content = i18next.t(`error.${errCode}`) } else { content = String( response?.data?.message || err.message || 'Internal error' ) } err.message = content err.errCode = errCode if (errCode === 'common.unauthenticated') { // Handle unauthorized error in a unified way if (!routing.isLocationMatch('/') && !routing.isSignInPage()) { message.error({ content, key: errCode }) } err.handled = true } else if (handleError === 'default') { if (method === 'get') { const fullUrl = config.url as string const API = fullUrl.replace(client.getBasePath(), '').split('?')[0] notification.error({ key: API, message: i18next.t('error.title'), description: ( API: {API}
{content}
) }) } else if (['post', 'put', 'delete', 'patch'].includes(method)) { Modal.error({ title: i18next.t('error.title'), content: content, zIndex: 2000 // higher than popover }) } err.handled = true } return Promise.reject(err) }) } function initAxios() { const instance = axios.create() applyErrorHandlerInterceptor(instance) return instance } export function setupClient(apiBasePath: string, apiToken: string) { i18n.addTranslations(translations) const axiosInstance = initAxios() const clinicApi = new ClinicApi( new Configuration({ basePath: apiBasePath, baseOptions: { handleError: 'default' } }), undefined, axiosInstance ) client._init(apiBasePath, apiToken, clinicApi) } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/client/translations/en.yaml ================================================ error: title: Error common: network: Network connection error unauthenticated: Please sign in again (session is expired) forbidden: The current user is not authorized to perform this action api: user: signin: invalid_code: Authorization Code is invalid or expired insufficient_priv: The user does not have sufficient privileges to access {{distro.tidb}} Dashboard. slow_query: export_no_data: No slow queires can be exported statement: export_no_data: No statements can be exported continuous_profiling: ng_monitoring_not_ready: | To use or learn more about "Continuous Profiling" feature, please search for "Continuous Profiling" in the {{distro.tidb}} official docs for more information. If it doesn't resove the issue, please contact the product's technical support. feature_not_supported: The cluster of this version doesn't support or can't use this feature, please contact with technical support to get more information. tidb: no_alive_tidb: No alive {{distro.tidb}} instance pd_access_failed: Failed to access {{distro.pd}} node tidb_conn_failed: Failed to connect to {{distro.tidb}} tidb_auth_failed: '{{distro.tidb}} authentication failed' ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/client/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/client/translations/zh.yaml ================================================ error: title: 错误 common: network: 网络连接失败 unauthenticated: 会话已过期,请重新登录 forbidden: 当前用户没有权限进行该操作 api: user: signin: invalid_code: 授权码无效或已过期 insufficient_priv: 该用户缺少足够的权限访问 {{distro.tidb}} Dashboard。 slow_query: export_no_data: 没有可导出的慢查询日志 statement: export_no_data: 没有可导出的语句 continuous_profiling: ng_monitoring_not_ready: | 想使用或深入了解“持续性能分析”功能,请在 {{distro.tidb}} 官方文档搜索“持续性能分析”查看更多内容。 若未能解决问题,请联系本产品技术支持。 feature_not_supported: 当前版本的集群不支持或无法使用该功能,请联系技术支持了解详细情况。 tidb: no_alive_tidb: 没有正在运行的 {{distro.tidb}} 实例 pd_access_failed: 无法访问 {{distro.pd}} 节点 tidb_conn_failed: 无法连接到 {{distro.tidb}} tidb_auth_failed: '{{distro.tidb}} 登录验证失败' ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/index.tsx ================================================ import i18next from 'i18next' import React from 'react' import ReactDOM from 'react-dom' import { telemetry } from '@pingcap/tidb-dashboard-lib' import { setupClient } from '~/client' import App from './App' import './styles/style.less' import '@pingcap/tidb-dashboard-lib/dist/index.css' import './styles/override.less' function renderApp() { ReactDOM.render( , document.getElementById('root') ) } function trackRouteChange() { let preRoute = '' function handler(ev) { const loc = ev.detail.location if (loc.pathname !== preRoute) { telemetry.trackRouteChange('#' + loc.pathname) preRoute = loc.pathname } } window.addEventListener('dashboard:route-change', handler) } type StartOptions = { apiPathBase: string apiToken: string } function start({ apiPathBase, apiToken }: StartOptions) { // i18n i18next.changeLanguage('en') // api client setupClient(apiPathBase, apiToken) // telemetry telemetry.init( process.env.REACT_APP_MIXPANEL_HOST, process.env.REACT_APP_MIXPANEL_TOKEN ) telemetry.enable( `tidb-dashboard-for-clinic-op-${process.env.REACT_APP_VERSION}` ) trackRouteChange() renderApp() } export default start ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/react-app-env.d.ts ================================================ declare module '*.yaml' { const classes: { readonly [key: string]: string } export default classes } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/styles/override.less ================================================ // reset ::selection pseudo element values ::selection { color: currentColor; background-color: #b3d6ff; } html { font-size: 14px; } ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/src/styles/style.less ================================================ @root-entry-name: default; @import 'antd/lib/style/components.less'; // it is expected to import 'antd/es/style/components.less' but it doesn't exist this file ================================================ FILE: ui/packages/tidb-dashboard-for-clinic-op/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "noEmit": true, "noImplicitAny": false, "noImplicitThis": false, "baseUrl": ".", "paths": { "~/*": ["src/*"] } }, "include": ["src"] } ================================================ FILE: ui/packages/tidb-dashboard-for-op/builder.js ================================================ const os = require('os') const path = require('path') const fs = require('fs-extra') const chalk = require('chalk') const { watch } = require('chokidar') const { start } = require('live-server') const { createProxyMiddleware } = require('http-proxy-middleware') const { build } = require('esbuild') const postCssPlugin = require('@baurine/esbuild-plugin-postcss3') const autoprefixer = require('autoprefixer') const { yamlPlugin } = require('esbuild-plugin-yaml') const babelPlugin = require('@baurine/esbuild-plugin-babel') const { lessModifyVars, lessGlobalVars } = require('../../less-vars') const isDev = process.env.NODE_ENV !== 'production' const isE2E = process.env.E2E_TEST === 'true' // load env const envFile = isDev ? './.env.development' : './.env.production' require('dotenv').config({ path: path.resolve(process.cwd(), envFile) }) const outDir = 'dist' const dashboardApiPrefix = process.env.REACT_APP_DASHBOARD_API_URL || 'http://127.0.0.1:12333' const devServerPort = process.env.PORT const devServerParams = { port: devServerPort, root: outDir, open: '/dashboard', // Set base URL // https://github.com/tapio/live-server/issues/287 - How can I serve from a base URL? proxy: [['/dashboard', `http://127.0.0.1:${devServerPort}`]], middleware: isDev && [ function (req, _res, next) { if (/\/dashboard\/api\/diagnose\/reports\/\S+\/detail/.test(req.url)) { req.url = '/diagnoseReport.html' } next() }, createProxyMiddleware('/dashboard/api/diagnose/reports/*/data.js', { target: dashboardApiPrefix, changeOrigin: true }) ] } function genDefine() { const define = {} for (const k in process.env) { if (k.startsWith('REACT_APP_')) { let envVal = process.env[k] // Example: REACT_APP_VERSION=$npm_package_version // Expect output: REACT_APP_VERSION=0.1.0 if (envVal.startsWith('$')) { envVal = process.env[envVal.substring(1)] } define[`process.env.${k}`] = JSON.stringify(envVal) } } define['process.env.E2E_TEST'] = JSON.stringify(process.env.E2E_TEST) return define } // customized plugin: log time const logTime = (_options = {}) => ({ name: 'logTime', setup(build) { let time build.onStart(() => { time = new Date() console.log(`Build started`) }) build.onEnd(() => { console.log(`Build ended: ${chalk.yellow(`${new Date() - time}ms`)}`) }) } }) const esbuildParams = { color: true, entryPoints: { dashboardApp: 'src/dashboardApp/index.ts', diagnoseReport: 'src/diagnoseReportApp/index.tsx' }, outdir: outDir, minify: !isDev, format: 'esm', bundle: true, sourcemap: true, logLevel: 'error', incremental: true, splitting: true, platform: 'browser', plugins: [ postCssPlugin.default({ lessOptions: { modifyVars: lessModifyVars, globalVars: lessGlobalVars, javascriptEnabled: true }, enableCache: true, plugins: [autoprefixer] }), yamlPlugin(), logTime() ], define: genDefine(), inject: ['./process-shim.js'] // fix runtime crash } if (isE2E) { // use babel and istanbul to report test coverage for e2e test esbuildParams.plugins.push( babelPlugin({ filter: /\.tsx?/, config: { presets: ['@babel/preset-react', '@babel/preset-typescript'], plugins: ['istanbul'] } }) ) } function buildHtml(inputFilename, outputFilename) { let result = fs.readFileSync(inputFilename).toString() const placeholders = ['PUBLIC_URL'] placeholders.forEach((key) => { result = result.replace(new RegExp(`%${key}%`, 'g'), process.env[key]) }) // replace TIME_PLACE_HOLDER const nowTime = new Date().valueOf() result = result.replace(new RegExp(`%TIME_PLACE_HOLDER%`, 'g'), nowTime) if (isDev) { result = result.replace( new RegExp('__DISTRO_ASSETS_RES_TIMESTAMP__', 'g'), nowTime ) } // handle distro strings res, only for dev mode const distroStringsResFilePath = `./${outDir}/distro-res/strings.json` if (isDev && fs.existsSync(distroStringsResFilePath)) { const distroStringsRes = require(distroStringsResFilePath) result = result.replace( '__DISTRO_STRINGS_RES__', btoa(JSON.stringify(distroStringsRes)) ) } fs.writeFileSync(outputFilename, result) } function handleAssets() { fs.copySync('./public', `./${outDir}`) if (isDev) { copyDistroRes() } buildHtml('./public/index.html', `./${outDir}/index.html`) buildHtml('./public/diagnoseReport.html', `./${outDir}/diagnoseReport.html`) } function copyDistroRes() { const distroResPath = '../../../bin/distro-res' if (fs.existsSync(distroResPath)) { fs.copySync(distroResPath, `./${outDir}/distro-res`) } } async function main() { fs.removeSync(`./${outDir}`) const builder = await build(esbuildParams) handleAssets() function rebuild() { builder.rebuild().catch((err) => console.log(err)) } if (isDev) { start(devServerParams) watch(`src/**/*`, { ignoreInitial: true }).on('all', () => { rebuild() }) watch('public/**/*', { ignoreInitial: true }).on('all', () => { handleAssets() }) // watch "node_modules/@pingcap/tidb-dashboard-lib/dist/**/*" triggers too many rebuild // so we just watch index.js to refine the experience watch('node_modules/@pingcap/tidb-dashboard-lib/dist/index.js', { ignoreInitial: true }).on('all', () => { rebuild() }) } else { process.exit(0) } } main() ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/.eslintrc.json ================================================ { "extends": ["plugin:cypress/recommended"] } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/README.md ================================================ # E2E Test Since there are some features is different from version to version, we have `make e2e_compat_features_test` and `make e2e_common_features_test` to test features compatibility in different versions and common features in all versions, respectively. ## Install Cypress The Cypress has been added to package.json, so just run `pnpm i` to install it. We use Cypress@8.5.0 here since the version>8.5.0 has an unstable server connection error, the related issue can be referred [here](https://github.com/cypress-io/cypress/issues/18464). ## Run Test **Prerequisite**: TiDB Dashboard server has to be started before run cypress test. ### Open Test Runner to Run Test Locally #### Test E2E with FEATURE_VERSION >= 5.3.0 ```shell # start frontend server cd ui && pnpm dev # start backend server make dev && make run # open cypress test runner cd ui/pacakges/tidb-dashboard-for-op && pnpm open:cypress ``` #### Test E2E with FEATURE_VERSION < 5.3.0 ```shell # start frontend server cd ui && pnpm dev # start backend server make dev && make run FEATURE_VERSION=5.0.0 # open cypress test runner cd ui/pacakges/tidb-dashboard-for-op && pnpm open:cypress --env FEATURE_VERSION=5.0.0 ``` Run test by choosing test file under `/integration` on cypress test runner, cypress will open a broswer to run e2e test. ### Run on CI ```shell # start frontend server make ui # start backend server UI=1 make && make run FEATURE_VERSION=${FEATURE_VERSION} # run e2e_compat_features and e2e_common_features tests make e2e_test FEATURE_VERSION=${FEATURE_VERSION} ``` ### Upload Visual Test Snapshots > TODO: Use the official cypress docker image to make sure visual test stable between operating systems. Since there was no cypress image of m1 before. So we use github actions to generate the snapshots that we need for visual tests. #### How to generate snapshots in GitHub Actions 1. Go to [tidb-dashboard Actions - Upload E2E Snapshots](https://github.com/pingcap/tidb-dashboard/actions/workflows/upload-e2e-snapshots.yaml) 2. Click "Run workflow" 3. Enter which git SHA you want the test to run on 4. Specify the test specs to generate the snapshots, base path is `${PROJECT_DIR}/ui/packages/tidb-dashboard-for-op/cypress/integration` 5. Enter the action after all jobs finished, download the e2e-snapshots artifact below. ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/fixtures/topsql_instance:end=1641934800&start=1641916800.json ================================================ { "data": [ { "instance": "127.0.0.1:10080", "instance_type": "tidb" }, { "instance": "127.0.0.1:20160", "instance_type": "tikv" } ], "status": "ok" } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/fixtures/topsql_summary:end=1641934800&instance=127.0.0.1%3A10080&instance_type=tidb&start=1641916800&top=5&window=123s.json ================================================ { "data": [ { "sql_digest": "b95a604794f9eff17a1a6a37d754324be11ede348a0d1e53da2bc3c32d6a4142", "sql_text": "select `variable_name` , `variable_value` from `mysql` . `global_variables` where `variable_name` in ( ... )", "is_other": false, "cpu_time_ms": 390, "exec_count_per_sec": 0.333259263374257, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "9449388a4efbc35c8eca1639aec164392df687869239f9ad16ea37887d98c42a", "plan_text": "\tBatch_Point_Get\troot\ttable:GLOBAL_VARIABLES, index:PRIMARY(VARIABLE_NAME), keep order:false, desc:false", "timestamp_sec": [ 1641916791, 1641916914, 1641917037, 1641917160, 1641917283, 1641917406, 1641917529, 1641917652, 1641917775, 1641917898, 1641918021, 1641918144, 1641918267, 1641918390, 1641918513, 1641918636, 1641918759, 1641918882, 1641919005, 1641919128, 1641919251, 1641919374, 1641919497, 1641919620, 1641919743, 1641919866, 1641919989, 1641920112, 1641920235, 1641920358, 1641920481, 1641920604, 1641920727, 1641920850, 1641920973, 1641921096, 1641921219, 1641921342, 1641921465, 1641921588, 1641921711, 1641921834, 1641921957, 1641922080, 1641922203, 1641922326, 1641922449, 1641922572, 1641922695, 1641922818, 1641922941, 1641923064, 1641923187, 1641923310, 1641923433, 1641923556, 1641923679, 1641923802, 1641923925, 1641924048, 1641924171, 1641924294, 1641924417, 1641924540, 1641924663, 1641924786, 1641924909, 1641925032, 1641925155, 1641925278, 1641925401, 1641925524, 1641925647, 1641925770, 1641925893, 1641926016, 1641926139, 1641926262, 1641926385, 1641926508, 1641926631, 1641926754, 1641926877, 1641927000, 1641927123, 1641927246, 1641927369, 1641927492, 1641927615, 1641927738, 1641927861, 1641927984, 1641928107, 1641928230, 1641928353, 1641928476, 1641928599, 1641928722, 1641928845, 1641928968, 1641929091, 1641929214, 1641929337, 1641929460, 1641929583, 1641929706, 1641929829, 1641929952, 1641930075, 1641930198, 1641930321, 1641930444, 1641930567, 1641930690, 1641930813, 1641930936, 1641931059, 1641931182, 1641931305, 1641931428, 1641931551, 1641931674, 1641931797, 1641931920, 1641932043, 1641932166, 1641932289, 1641932412, 1641932535, 1641932658, 1641932781, 1641932904, 1641933027, 1641933150, 1641933273, 1641933396, 1641933519, 1641933642, 1641933765, 1641933888, 1641934011, 1641934134, 1641934257, 1641934380, 1641934503, 1641934626, 1641934749 ], "cpu_time_ms": [ 0, 0, 0, 0, 0, 10, 0, 0, 10, 0, 10, 0, 10, 10, 0, 0, 10, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 10, 10, 0, 0, 0, 10, 0, 10, 10, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 10, 10, 10, 0, 0, 0, 0, 0, 20, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 10, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 10, 10, 0, 0, 10, 0, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0 ], "exec_count_per_sec": 0.333259263374257, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "e2769f5619db3dc4916298c4c6b3bc2c36c6d64a84e2a5549284521482564ce0", "sql_text": "select high_priority ( `variable_value` ) from `mysql` . `tidb` where `variable_name` = ? for update", "is_other": false, "cpu_time_ms": 390, "exec_count_per_sec": 0.06994055885784123, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641916791, 1641917406, 1641917775, 1641918144, 1641918513, 1641918759, 1641919005, 1641919866, 1641920112, 1641920604, 1641920850, 1641921342, 1641921465, 1641922695, 1641923187, 1641923433, 1641923925, 1641925524, 1641925647, 1641926508, 1641926877, 1641927861, 1641928845, 1641929706, 1641931059, 1641932289, 1641932658, 1641932904, 1641933150, 1641933396 ], "cpu_time_ms": [ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 20, 10, 10, 10, 10, 10, 20, 10, 20, 10, 10, 10, 10, 10, 10, 10, 10, 10 ], "exec_count_per_sec": 0, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 }, { "plan_digest": "892b61d58192589f43d81f3ee50710d71e6a8c286a59877069eaa00cd3bb8f5c", "plan_text": "\tProjection \troot\tmysql.tidb.variable_value\n\t└─SelectLock \troot\t\n\t └─IndexLookUp \troot\t\n\t ├─IndexRangeScan\tcop \ttable:tidb, index:PRIMARY(VARIABLE_NAME), range:[?,?], keep order:false\n\t └─TableRowIDScan\tcop \ttable:tidb, keep order:false", "timestamp_sec": [ 1641916791, 1641916914, 1641917037, 1641917160, 1641917283, 1641917406, 1641917529, 1641917652, 1641917775, 1641917898, 1641918021, 1641918144, 1641918267, 1641918390, 1641918513, 1641918636, 1641918759, 1641918882, 1641919005, 1641919128, 1641919251, 1641919374, 1641919497, 1641919620, 1641919743, 1641919866, 1641919989, 1641920112, 1641920235, 1641920358, 1641920481, 1641920604, 1641920727, 1641920850, 1641920973, 1641921096, 1641921219, 1641921342, 1641921465, 1641921588, 1641921711, 1641921834, 1641921957, 1641922080, 1641922203, 1641922326, 1641922449, 1641922572, 1641922695, 1641922818, 1641922941, 1641923064, 1641923187, 1641923310, 1641923433, 1641923556, 1641923679, 1641923802, 1641923925, 1641924048, 1641924171, 1641924294, 1641924417, 1641924540, 1641924663, 1641924786, 1641924909, 1641925032, 1641925155, 1641925278, 1641925401, 1641925524, 1641925647, 1641925770, 1641925893, 1641926016, 1641926139, 1641926262, 1641926385, 1641926508, 1641926631, 1641926754, 1641926877, 1641927000, 1641927123, 1641927246, 1641927369, 1641927492, 1641927615, 1641927738, 1641927861, 1641927984, 1641928107, 1641928230, 1641928353, 1641928476, 1641928599, 1641928722, 1641928845, 1641928968, 1641929091, 1641929214, 1641929337, 1641929460, 1641929583, 1641929706, 1641929829, 1641929952, 1641930075, 1641930198, 1641930321, 1641930444, 1641930567, 1641930690, 1641930813, 1641930936, 1641931059, 1641931182, 1641931305, 1641931428, 1641931551, 1641931674, 1641931797, 1641931920, 1641932043, 1641932166, 1641932289, 1641932412, 1641932535, 1641932658, 1641932781, 1641932904, 1641933027, 1641933150, 1641933273, 1641933396, 1641933519, 1641933642, 1641933765, 1641933888, 1641934011, 1641934134, 1641934257, 1641934380, 1641934503, 1641934626, 1641934749 ], "cpu_time_ms": [ 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], "exec_count_per_sec": 0.06994055885784123, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "da5f21b51be132c69a10de76148c95ab1ad8e6fb6c0fdacb3dcedfe4b6d3c41f", "sql_text": "select distinct `table_id` from `mysql` . `stats_feedback`", "is_other": false, "cpu_time_ms": 320, "exec_count_per_sec": 0.06666296316871285, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641924294, 1641928476, 1641928845, 1641929460, 1641930075, 1641931551, 1641931797, 1641932535, 1641933396, 1641934011, 1641934749 ], "cpu_time_ms": [10, 10, 10, 10, 10, 10, 10, 10, 10, 20, 10], "exec_count_per_sec": 0, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 }, { "plan_digest": "8d5611b5d6f8ade5b8d2f721db81164a09cf915d836dfcfc3fd00237750350ca", "plan_text": "\tHashAgg \troot\tgroup by:mysql.stats_feedback.table_id, funcs:firstrow(mysql.stats_feedback.table_id)-\u003emysql.stats_feedback.table_id\n\t└─IndexReader \troot\tindex:HashAgg_4\n\t └─HashAgg \tcop \tgroup by:mysql.stats_feedback.table_id, \n\t └─IndexFullScan\tcop \ttable:stats_feedback, index:hist(table_id, is_index, hist_id), range:[?,?], keep order:false", "timestamp_sec": [ 1641916791, 1641916914, 1641917037, 1641917160, 1641917283, 1641917406, 1641917529, 1641917652, 1641917775, 1641917898, 1641918021, 1641918144, 1641918267, 1641918390, 1641918513, 1641918636, 1641918759, 1641918882, 1641919005, 1641919128, 1641919251, 1641919374, 1641919497, 1641919620, 1641919743, 1641919866, 1641919989, 1641920112, 1641920235, 1641920358, 1641920481, 1641920604, 1641920727, 1641920850, 1641920973, 1641921096, 1641921219, 1641921342, 1641921465, 1641921588, 1641921711, 1641921834, 1641921957, 1641922080, 1641922203, 1641922326, 1641922449, 1641922572, 1641922695, 1641922818, 1641922941, 1641923064, 1641923187, 1641923310, 1641923433, 1641923556, 1641923679, 1641923802, 1641923925, 1641924048, 1641924171, 1641924294, 1641924417, 1641924540, 1641924663, 1641924786, 1641924909, 1641925032, 1641925155, 1641925278, 1641925401, 1641925524, 1641925647, 1641925770, 1641925893, 1641926016, 1641926139, 1641926262, 1641926385, 1641926508, 1641926631, 1641926754, 1641926877, 1641927000, 1641927123, 1641927246, 1641927369, 1641927492, 1641927615, 1641927738, 1641927861, 1641927984, 1641928107, 1641928230, 1641928353, 1641928476, 1641928599, 1641928722, 1641928845, 1641928968, 1641929091, 1641929214, 1641929337, 1641929460, 1641929583, 1641929706, 1641929829, 1641929952, 1641930075, 1641930198, 1641930321, 1641930444, 1641930567, 1641930690, 1641930813, 1641930936, 1641931059, 1641931182, 1641931305, 1641931428, 1641931551, 1641931674, 1641931797, 1641931920, 1641932043, 1641932166, 1641932289, 1641932412, 1641932535, 1641932658, 1641932781, 1641932904, 1641933027, 1641933150, 1641933273, 1641933396, 1641933519, 1641933642, 1641933765, 1641933888, 1641934011, 1641934134, 1641934257, 1641934380, 1641934503, 1641934626, 1641934749 ], "cpu_time_ms": [ 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 10, 0, 20, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 10, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 10, 0, 10, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0 ], "exec_count_per_sec": 0.06666296316871285, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "ea4709893ffb8edc8d58191ccbd93c4c4fdfc1d20ebbcc7f48707df328d6dbb2", "sql_text": "select `version` , `table_id` , `modify_count` , `count` from `mysql` . `stats_meta` where `version` \u003e ? order by `version`", "is_other": false, "cpu_time_ms": 3590, "exec_count_per_sec": 0.333259263374257, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641916791, 1641916914, 1641917037, 1641917160, 1641917283, 1641917406, 1641917529, 1641917652, 1641917775, 1641917898, 1641918144, 1641918390, 1641918513, 1641918636, 1641918759, 1641919005, 1641919251, 1641919620, 1641919743, 1641919866, 1641919989, 1641920112, 1641920235, 1641920358, 1641920481, 1641920604, 1641920850, 1641920973, 1641921342, 1641921465, 1641921588, 1641921711, 1641921834, 1641921957, 1641922080, 1641922203, 1641922326, 1641922449, 1641922572, 1641922695, 1641922818, 1641922941, 1641923064, 1641923187, 1641923310, 1641923433, 1641923556, 1641923679, 1641923802, 1641923925, 1641924048, 1641924171, 1641924294, 1641924417, 1641924663, 1641924786, 1641924909, 1641925032, 1641925155, 1641925278, 1641925401, 1641925524, 1641925770, 1641925893, 1641926016, 1641926139, 1641926262, 1641926385, 1641926631, 1641926754, 1641926877, 1641927000, 1641927246, 1641927369, 1641927492, 1641927738, 1641927861, 1641927984, 1641928230, 1641928476, 1641928599, 1641928722, 1641928845, 1641929091, 1641929214, 1641929337, 1641929460, 1641929583, 1641929706, 1641929829, 1641929952, 1641930075, 1641930321, 1641930444, 1641930567, 1641930813, 1641930936, 1641931059, 1641931182, 1641931305, 1641931428, 1641931551, 1641931674, 1641931797, 1641931920, 1641932043, 1641932166, 1641932289, 1641932412, 1641932535, 1641932658, 1641932781, 1641932904, 1641933027, 1641933150, 1641933273, 1641933396, 1641933519, 1641933642, 1641933765, 1641933888, 1641934011, 1641934134, 1641934257, 1641934380, 1641934503, 1641934626, 1641934749 ], "cpu_time_ms": [ 50, 10, 10, 10, 20, 30, 30, 20, 30, 40, 10, 40, 20, 30, 40, 50, 20, 20, 10, 30, 50, 40, 40, 10, 20, 30, 10, 10, 10, 10, 50, 10, 10, 20, 20, 30, 10, 20, 20, 20, 10, 20, 40, 70, 40, 20, 20, 20, 10, 10, 20, 10, 10, 30, 10, 50, 20, 40, 30, 30, 10, 30, 30, 20, 30, 30, 30, 50, 10, 30, 20, 30, 10, 10, 20, 30, 10, 40, 20, 10, 30, 20, 10, 30, 20, 20, 10, 20, 20, 30, 30, 40, 40, 10, 20, 20, 10, 30, 20, 30, 40, 30, 20, 10, 30, 30, 10, 10, 20, 40, 10, 30, 20, 20, 30, 10, 20, 40, 20, 10, 10, 20, 20, 10, 30, 20, 10, 20 ], "exec_count_per_sec": 0, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 }, { "plan_digest": "42d48b331dfe53300ddea68d4217dc467244bd898f80f10a913f2104d26d4989", "plan_text": "\tProjection \troot\tmysql.stats_meta.count, mysql.stats_meta.modify_count, mysql.stats_meta.table_id, mysql.stats_meta.version\n\t└─IndexLookUp \troot\t\n\t ├─IndexRangeScan\tcop \ttable:stats_meta, index:idx_ver(version), range:[?,?], keep order:true\n\t └─TableRowIDScan\tcop \ttable:stats_meta, keep order:false", "timestamp_sec": [ 1641916791, 1641916914, 1641917037, 1641917160, 1641917283, 1641917406, 1641917529, 1641917652, 1641917775, 1641917898, 1641918021, 1641918144, 1641918267, 1641918390, 1641918513, 1641918636, 1641918759, 1641918882, 1641919005, 1641919128, 1641919251, 1641919374, 1641919497, 1641919620, 1641919743, 1641919866, 1641919989, 1641920112, 1641920235, 1641920358, 1641920481, 1641920604, 1641920727, 1641920850, 1641920973, 1641921096, 1641921219, 1641921342, 1641921465, 1641921588, 1641921711, 1641921834, 1641921957, 1641922080, 1641922203, 1641922326, 1641922449, 1641922572, 1641922695, 1641922818, 1641922941, 1641923064, 1641923187, 1641923310, 1641923433, 1641923556, 1641923679, 1641923802, 1641923925, 1641924048, 1641924171, 1641924294, 1641924417, 1641924540, 1641924663, 1641924786, 1641924909, 1641925032, 1641925155, 1641925278, 1641925401, 1641925524, 1641925647, 1641925770, 1641925893, 1641926016, 1641926139, 1641926262, 1641926385, 1641926508, 1641926631, 1641926754, 1641926877, 1641927000, 1641927123, 1641927246, 1641927369, 1641927492, 1641927615, 1641927738, 1641927861, 1641927984, 1641928107, 1641928230, 1641928353, 1641928476, 1641928599, 1641928722, 1641928845, 1641928968, 1641929091, 1641929214, 1641929337, 1641929460, 1641929583, 1641929706, 1641929829, 1641929952, 1641930075, 1641930198, 1641930321, 1641930444, 1641930567, 1641930690, 1641930813, 1641930936, 1641931059, 1641931182, 1641931305, 1641931428, 1641931551, 1641931674, 1641931797, 1641931920, 1641932043, 1641932166, 1641932289, 1641932412, 1641932535, 1641932658, 1641932781, 1641932904, 1641933027, 1641933150, 1641933273, 1641933396, 1641933519, 1641933642, 1641933765, 1641933888, 1641934011, 1641934134, 1641934257, 1641934380, 1641934503, 1641934626, 1641934749 ], "cpu_time_ms": [ 0, 0, 0, 0, 0, 10, 10, 10, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 10, 0, 10, 0, 0, 0, 0, 0, 10, 40, 0, 0, 10, 10, 20, 10, 0, 10, 10, 10, 10, 20, 0, 10, 0, 0, 10, 10, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 10, 0, 10, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 10, 10, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 10, 10, 0, 0, 0, 10, 0, 10, 10, 0, 0, 20, 10, 0, 0, 0, 0, 20, 10, 40, 0, 0, 0, 0, 0, 10, 0, 20, 0, 0, 10, 0, 0, 0, 0, 10, 10, 20 ], "exec_count_per_sec": 0.333259263374257, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "61f4cce222f1a26d6b0650865051ebed3d077412ae053636e2f5acf9dc426a42", "sql_text": "insert high_priority into `mysql` . `tidb` values ( ... ) on duplicate key update `variable_value` = ? , comment = ?", "is_other": false, "cpu_time_ms": 260, "exec_count_per_sec": 0.019998888950613854, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641916791, 1641916914, 1641917037, 1641917160, 1641917283, 1641917406, 1641917529, 1641917652, 1641917775, 1641917898, 1641918021, 1641918144, 1641918267, 1641918390, 1641918513, 1641918636, 1641918759, 1641918882, 1641919005, 1641919128, 1641919251, 1641919374, 1641919497, 1641919620, 1641919743, 1641919866, 1641919989, 1641920112, 1641920235, 1641920358, 1641920481, 1641920604, 1641920727, 1641920850, 1641920973, 1641921096, 1641921219, 1641921342, 1641921465, 1641921588, 1641921711, 1641921834, 1641921957, 1641922080, 1641922203, 1641922326, 1641922449, 1641922572, 1641922695, 1641922818, 1641922941, 1641923064, 1641923187, 1641923310, 1641923433, 1641923556, 1641923679, 1641923802, 1641923925, 1641924048, 1641924171, 1641924294, 1641924417, 1641924540, 1641924663, 1641924786, 1641924909, 1641925032, 1641925155, 1641925278, 1641925401, 1641925524, 1641925647, 1641925770, 1641925893, 1641926016, 1641926139, 1641926262, 1641926385, 1641926508, 1641926631, 1641926754, 1641926877, 1641927000, 1641927123, 1641927246, 1641927369, 1641927492, 1641927615, 1641927738, 1641927861, 1641927984, 1641928107, 1641928230, 1641928353, 1641928476, 1641928599, 1641928722, 1641928845, 1641928968, 1641929091, 1641929214, 1641929337, 1641929460, 1641929583, 1641929706, 1641929829, 1641929952, 1641930075, 1641930198, 1641930321, 1641930444, 1641930567, 1641930690, 1641930813, 1641930936, 1641931059, 1641931182, 1641931305, 1641931428, 1641931551, 1641931674, 1641931797, 1641931920, 1641932043, 1641932166, 1641932289, 1641932412, 1641932535, 1641932658, 1641932781, 1641932904, 1641933027, 1641933150, 1641933273, 1641933396, 1641933519, 1641933642, 1641933765, 1641933888, 1641934011, 1641934134, 1641934257, 1641934380, 1641934503, 1641934626, 1641934749 ], "cpu_time_ms": [ 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 10, 0, 0, 0, 0, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20 ], "exec_count_per_sec": 0.019998888950613854, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "", "sql_text": "", "is_other": true, "cpu_time_ms": 660, "exec_count_per_sec": 1.161546580745514, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641916791, 1641916914, 1641917037, 1641917160, 1641917283, 1641917406, 1641917529, 1641917652, 1641917775, 1641917898, 1641918021, 1641918144, 1641918267, 1641918390, 1641918513, 1641918636, 1641918759, 1641918882, 1641919005, 1641919128, 1641919251, 1641919374, 1641919497, 1641919620, 1641919743, 1641919866, 1641919989, 1641920112, 1641920235, 1641920358, 1641920481, 1641920604, 1641920727, 1641920850, 1641920973, 1641921096, 1641921219, 1641921342, 1641921465, 1641921588, 1641921711, 1641921834, 1641921957, 1641922080, 1641922203, 1641922326, 1641922449, 1641922572, 1641922695, 1641922818, 1641922941, 1641923064, 1641923187, 1641923310, 1641923433, 1641923556, 1641923679, 1641923802, 1641923925, 1641924048, 1641924171, 1641924294, 1641924417, 1641924540, 1641924663, 1641924786, 1641924909, 1641925032, 1641925155, 1641925278, 1641925401, 1641925524, 1641925647, 1641925770, 1641925893, 1641926016, 1641926139, 1641926262, 1641926385, 1641926508, 1641926631, 1641926754, 1641926877, 1641927000, 1641927123, 1641927246, 1641927369, 1641927492, 1641927615, 1641927738, 1641927861, 1641927984, 1641928107, 1641928230, 1641928353, 1641928476, 1641928599, 1641928722, 1641928845, 1641928968, 1641929091, 1641929214, 1641929337, 1641929460, 1641929583, 1641929706, 1641929829, 1641929952, 1641930075, 1641930198, 1641930321, 1641930444, 1641930567, 1641930690, 1641930813, 1641930936, 1641931059, 1641931182, 1641931305, 1641931428, 1641931551, 1641931674, 1641931797, 1641931920, 1641932043, 1641932166, 1641932289, 1641932412, 1641932535, 1641932658, 1641932781, 1641932904, 1641933027, 1641933150, 1641933273, 1641933396, 1641933519, 1641933642, 1641933765, 1641933888, 1641934011, 1641934134, 1641934257, 1641934380, 1641934503, 1641934626, 1641934749 ], "cpu_time_ms": [ 0, 0, 10, 0, 0, 0, 0, 0, 20, 30, 10, 20, 0, 0, 10, 0, 10, 0, 10, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 10, 20, 0, 10, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 10, 0, 20, 10, 0, 0, 0, 0, 10, 0, 10, 30, 0, 0, 0, 0, 0, 0, 0, 10, 10, 0, 10, 0, 10, 0, 0, 0, 0, 10, 0, 0, 0, 10, 20, 0, 0, 0, 0, 10, 0, 0, 0, 30, 10, 10, 0, 20, 0, 20, 0, 0, 0, 20, 0, 20, 0, 0, 10, 10, 0, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 20, 10, 10, 0, 0, 0, 0, 0, 0, 0, 10, 0, 10, 0 ], "exec_count_per_sec": 1.161546580745514, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] } ], "status": "ok" } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/fixtures/topsql_summary_large_timerange:end=1641916800&instance=127.0.0.1%3A10080&instance_type=tidb&start=1641484800&top=5&window=2929s.json ================================================ { "data": [ { "sql_digest": "e6f07d43b5c21db0fbb9a31feac2dc599787763393dd5acbfad80e247eb02ad5", "sql_text": "begin", "is_other": false, "cpu_time_ms": 1570, "exec_count_per_sec": 0.0991201409255997, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641804086, 1641807015, 1641809944, 1641812873, 1641815802, 1641818731, 1641821660, 1641824589, 1641827518, 1641830447, 1641833376, 1641836305, 1641839234, 1641842163, 1641845092, 1641848021, 1641850950, 1641853879, 1641856808, 1641859737, 1641862666, 1641865595, 1641868524, 1641871453, 1641874382, 1641877311, 1641880240, 1641883169, 1641886098, 1641889027, 1641891956, 1641894885, 1641897814, 1641900743, 1641903672, 1641906601, 1641909530, 1641912459, 1641915388 ], "cpu_time_ms": [ 20, 30, 30, 30, 30, 20, 10, 0, 60, 40, 20, 20, 40, 30, 10, 100, 30, 50, 30, 90, 30, 40, 80, 40, 40, 30, 50, 60, 40, 40, 50, 60, 40, 40, 70, 70, 20, 50, 30 ], "exec_count_per_sec": 0.0991201409255997, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "ea4709893ffb8edc8d58191ccbd93c4c4fdfc1d20ebbcc7f48707df328d6dbb2", "sql_text": "select `version` , `table_id` , `modify_count` , `count` from `mysql` . `stats_meta` where `version` \u003e ? order by `version`", "is_other": false, "cpu_time_ms": 20790, "exec_count_per_sec": 0.08658544771887101, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641804086, 1641807015, 1641809944, 1641812873, 1641815802, 1641818731, 1641821660, 1641824589, 1641827518, 1641830447, 1641833376, 1641836305, 1641839234, 1641842163, 1641845092, 1641848021, 1641850950, 1641853879, 1641856808, 1641859737, 1641862666, 1641865595, 1641868524, 1641871453, 1641874382, 1641877311, 1641880240, 1641883169, 1641886098, 1641889027, 1641891956, 1641894885, 1641897814, 1641900743, 1641903672, 1641906601, 1641909530, 1641912459, 1641915388 ], "cpu_time_ms": [ 180, 340, 440, 490, 410, 410, 400, 630, 480, 450, 390, 570, 500, 490, 440, 500, 530, 530, 580, 390, 420, 440, 480, 490, 540, 550, 520, 470, 480, 370, 430, 430, 430, 410, 380, 590, 360, 300, 370 ], "exec_count_per_sec": 0, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 }, { "plan_digest": "42d48b331dfe53300ddea68d4217dc467244bd898f80f10a913f2104d26d4989", "plan_text": "\tProjection \troot\tmysql.stats_meta.count, mysql.stats_meta.modify_count, mysql.stats_meta.table_id, mysql.stats_meta.version\n\t└─IndexLookUp \troot\t\n\t ├─IndexRangeScan\tcop \ttable:stats_meta, index:idx_ver(version), range:[?,?], keep order:true\n\t └─TableRowIDScan\tcop \ttable:stats_meta, keep order:false", "timestamp_sec": [ 1641804086, 1641807015, 1641809944, 1641812873, 1641815802, 1641818731, 1641821660, 1641824589, 1641827518, 1641830447, 1641833376, 1641836305, 1641839234, 1641842163, 1641845092, 1641848021, 1641850950, 1641853879, 1641856808, 1641859737, 1641862666, 1641865595, 1641868524, 1641871453, 1641874382, 1641877311, 1641880240, 1641883169, 1641886098, 1641889027, 1641891956, 1641894885, 1641897814, 1641900743, 1641903672, 1641906601, 1641909530, 1641912459, 1641915388 ], "cpu_time_ms": [ 40, 170, 80, 70, 80, 40, 110, 100, 70, 90, 50, 60, 50, 120, 50, 80, 60, 80, 90, 110, 50, 50, 50, 60, 50, 90, 60, 100, 120, 100, 60, 160, 120, 100, 70, 90, 30, 110, 110 ], "exec_count_per_sec": 0.08658544771887101, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "e2769f5619db3dc4916298c4c6b3bc2c36c6d64a84e2a5549284521482564ce0", "sql_text": "select high_priority ( `variable_value` ) from `mysql` . `tidb` where `variable_name` = ? for update", "is_other": false, "cpu_time_ms": 2030, "exec_count_per_sec": 0.01817588385212071, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641804086, 1641807015, 1641809944, 1641812873, 1641815802, 1641818731, 1641821660, 1641824589, 1641827518, 1641830447, 1641833376, 1641836305, 1641839234, 1641842163, 1641845092, 1641848021, 1641850950, 1641853879, 1641856808, 1641859737, 1641862666, 1641865595, 1641868524, 1641871453, 1641874382, 1641877311, 1641880240, 1641883169, 1641886098, 1641889027, 1641891956, 1641894885, 1641897814, 1641900743, 1641903672, 1641906601, 1641909530, 1641912459, 1641915388 ], "cpu_time_ms": [ 30, 60, 50, 40, 60, 50, 10, 20, 110, 50, 60, 50, 40, 90, 20, 30, 60, 20, 50, 30, 50, 30, 100, 10, 60, 20, 10, 70, 10, 90, 30, 60, 50, 20, 30, 80, 60, 40, 30 ], "exec_count_per_sec": 0, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 }, { "plan_digest": "892b61d58192589f43d81f3ee50710d71e6a8c286a59877069eaa00cd3bb8f5c", "plan_text": "\tProjection \troot\tmysql.tidb.variable_value\n\t└─SelectLock \troot\t\n\t └─IndexLookUp \troot\t\n\t ├─IndexRangeScan\tcop \ttable:tidb, index:PRIMARY(VARIABLE_NAME), range:[?,?], keep order:false\n\t └─TableRowIDScan\tcop \ttable:tidb, keep order:false", "timestamp_sec": [ 1641804086, 1641807015, 1641809944, 1641812873, 1641815802, 1641818731, 1641821660, 1641824589, 1641827518, 1641830447, 1641833376, 1641836305, 1641839234, 1641842163, 1641845092, 1641848021, 1641850950, 1641853879, 1641856808, 1641859737, 1641862666, 1641865595, 1641868524, 1641871453, 1641874382, 1641877311, 1641880240, 1641883169, 1641886098, 1641889027, 1641891956, 1641894885, 1641897814, 1641900743, 1641903672, 1641906601, 1641909530, 1641912459, 1641915388 ], "cpu_time_ms": [ 0, 0, 0, 0, 0, 10, 10, 0, 10, 20, 30, 0, 10, 10, 0, 0, 20, 10, 0, 0, 10, 0, 10, 10, 0, 0, 20, 10, 0, 0, 10, 10, 0, 20, 0, 0, 0, 10, 10 ], "exec_count_per_sec": 0.01817588385212071, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "b95a604794f9eff17a1a6a37d754324be11ede348a0d1e53da2bc3c32d6a4142", "sql_text": "select `variable_name` , `variable_value` from `mysql` . `global_variables` where `variable_name` in ( ... )", "is_other": false, "cpu_time_ms": 2380, "exec_count_per_sec": 0.08658544771887101, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [1641845092, 1641850950, 1641874382], "cpu_time_ms": [10, 10, 10], "exec_count_per_sec": 0, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 }, { "plan_digest": "9449388a4efbc35c8eca1639aec164392df687869239f9ad16ea37887d98c42a", "plan_text": "\tBatch_Point_Get\troot\ttable:GLOBAL_VARIABLES, index:PRIMARY(VARIABLE_NAME), keep order:false, desc:false", "timestamp_sec": [ 1641804086, 1641807015, 1641809944, 1641812873, 1641815802, 1641818731, 1641821660, 1641824589, 1641827518, 1641830447, 1641833376, 1641836305, 1641839234, 1641842163, 1641845092, 1641848021, 1641850950, 1641853879, 1641856808, 1641859737, 1641862666, 1641865595, 1641868524, 1641871453, 1641874382, 1641877311, 1641880240, 1641883169, 1641886098, 1641889027, 1641891956, 1641894885, 1641897814, 1641900743, 1641903672, 1641906601, 1641909530, 1641912459, 1641915388 ], "cpu_time_ms": [ 10, 40, 50, 40, 40, 40, 50, 80, 40, 90, 100, 70, 90, 60, 50, 60, 110, 100, 40, 30, 20, 60, 100, 80, 80, 20, 90, 50, 50, 100, 60, 40, 50, 70, 80, 40, 40, 80, 50 ], "exec_count_per_sec": 0.08658544771887101, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "da5f21b51be132c69a10de76148c95ab1ad8e6fb6c0fdacb3dcedfe4b6d3c41f", "sql_text": "select distinct `table_id` from `mysql` . `stats_feedback`", "is_other": false, "cpu_time_ms": 1810, "exec_count_per_sec": 0.017321719162687123, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641807015, 1641809944, 1641812873, 1641815802, 1641818731, 1641821660, 1641833376, 1641836305, 1641842163, 1641848021, 1641853879, 1641859737, 1641862666, 1641865595, 1641871453, 1641874382, 1641877311, 1641880240, 1641883169, 1641886098, 1641889027, 1641891956, 1641894885, 1641900743, 1641903672, 1641906601, 1641909530, 1641912459, 1641915388 ], "cpu_time_ms": [ 10, 20, 10, 10, 10, 20, 20, 10, 10, 20, 20, 20, 20, 10, 20, 20, 30, 10, 10, 10, 20, 30, 30, 20, 10, 10, 40, 10, 30 ], "exec_count_per_sec": 0, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 }, { "plan_digest": "8d5611b5d6f8ade5b8d2f721db81164a09cf915d836dfcfc3fd00237750350ca", "plan_text": "\tHashAgg \troot\tgroup by:mysql.stats_feedback.table_id, funcs:firstrow(mysql.stats_feedback.table_id)-\u003emysql.stats_feedback.table_id\n\t└─IndexReader \troot\tindex:HashAgg_4\n\t └─HashAgg \tcop \tgroup by:mysql.stats_feedback.table_id, \n\t └─IndexFullScan\tcop \ttable:stats_feedback, index:hist(table_id, is_index, hist_id), range:[?,?], keep order:false", "timestamp_sec": [ 1641804086, 1641807015, 1641809944, 1641812873, 1641815802, 1641818731, 1641821660, 1641824589, 1641827518, 1641830447, 1641833376, 1641836305, 1641839234, 1641842163, 1641845092, 1641848021, 1641850950, 1641853879, 1641856808, 1641859737, 1641862666, 1641865595, 1641868524, 1641871453, 1641874382, 1641877311, 1641880240, 1641883169, 1641886098, 1641889027, 1641891956, 1641894885, 1641897814, 1641900743, 1641903672, 1641906601, 1641909530, 1641912459, 1641915388 ], "cpu_time_ms": [ 10, 40, 50, 30, 10, 40, 70, 50, 30, 60, 20, 10, 50, 50, 20, 40, 40, 80, 30, 20, 70, 60, 10, 50, 40, 0, 30, 20, 10, 20, 20, 30, 10, 30, 40, 40, 30, 20, 20 ], "exec_count_per_sec": 0.017321719162687123, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "", "sql_text": "", "is_other": true, "cpu_time_ms": 5390, "exec_count_per_sec": 0.21266848919331205, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641804086, 1641807015, 1641809944, 1641812873, 1641815802, 1641818731, 1641821660, 1641824589, 1641827518, 1641830447, 1641833376, 1641836305, 1641839234, 1641842163, 1641845092, 1641848021, 1641850950, 1641853879, 1641856808, 1641859737, 1641862666, 1641865595, 1641868524, 1641871453, 1641874382, 1641877311, 1641880240, 1641883169, 1641886098, 1641889027, 1641891956, 1641894885, 1641897814, 1641900743, 1641903672, 1641906601, 1641909530, 1641912459, 1641915388 ], "cpu_time_ms": [ 90, 120, 290, 180, 160, 120, 100, 50, 150, 180, 140, 120, 120, 90, 120, 110, 190, 190, 120, 140, 120, 160, 150, 190, 240, 120, 130, 40, 140, 180, 110, 70, 180, 120, 160, 80, 130, 180, 110 ], "exec_count_per_sec": 0.21266848919331205, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] } ], "status": "ok" } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/fixtures/topsql_summary_small_timerange:end=1641920460&instance=127.0.0.1%3A10080&instance_type=tidb&start=1641920400&top=5&window=1s.json ================================================ { "data": [ { "sql_digest": "61f4cce222f1a26d6b0650865051ebed3d077412ae053636e2f5acf9dc426a42", "sql_text": "insert high_priority into `mysql` . `tidb` values ( ... ) on duplicate key update `variable_value` = ? , comment = ?", "is_other": false, "cpu_time_ms": 10, "exec_count_per_sec": 0.01639344262295082, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [1641920435, 1641920436], "cpu_time_ms": [10, 0], "exec_count_per_sec": 0.01639344262295082, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "e6f07d43b5c21db0fbb9a31feac2dc599787763393dd5acbfad80e247eb02ad5", "sql_text": "begin", "is_other": false, "cpu_time_ms": 10, "exec_count_per_sec": 0.3770491803278688, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641920402, 1641920405, 1641920408, 1641920411, 1641920414, 1641920417, 1641920420, 1641920423, 1641920426, 1641920429, 1641920432, 1641920435, 1641920436, 1641920438, 1641920441, 1641920444, 1641920447, 1641920450, 1641920453, 1641920456, 1641920459 ], "cpu_time_ms": [ 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], "exec_count_per_sec": 0.3770491803278688, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "ea4709893ffb8edc8d58191ccbd93c4c4fdfc1d20ebbcc7f48707df328d6dbb2", "sql_text": "select `version` , `table_id` , `modify_count` , `count` from `mysql` . `stats_meta` where `version` \u003e ? order by `version`", "is_other": false, "cpu_time_ms": 10, "exec_count_per_sec": 0.32786885245901637, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [1641920426], "cpu_time_ms": [10], "exec_count_per_sec": 0, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 }, { "plan_digest": "42d48b331dfe53300ddea68d4217dc467244bd898f80f10a913f2104d26d4989", "plan_text": "\tProjection \troot\tmysql.stats_meta.count, mysql.stats_meta.modify_count, mysql.stats_meta.table_id, mysql.stats_meta.version\n\t└─IndexLookUp \troot\t\n\t ├─IndexRangeScan\tcop \ttable:stats_meta, index:idx_ver(version), range:[?,?], keep order:true\n\t └─TableRowIDScan\tcop \ttable:stats_meta, keep order:false", "timestamp_sec": [ 1641920402, 1641920405, 1641920408, 1641920411, 1641920414, 1641920417, 1641920420, 1641920423, 1641920426, 1641920429, 1641920432, 1641920435, 1641920438, 1641920441, 1641920444, 1641920447, 1641920450, 1641920453, 1641920456, 1641920459 ], "cpu_time_ms": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], "exec_count_per_sec": 0.32786885245901637, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "b95a604794f9eff17a1a6a37d754324be11ede348a0d1e53da2bc3c32d6a4142", "sql_text": "select `variable_name` , `variable_value` from `mysql` . `global_variables` where `variable_name` in ( ... )", "is_other": false, "cpu_time_ms": 10, "exec_count_per_sec": 0.32786885245901637, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "9449388a4efbc35c8eca1639aec164392df687869239f9ad16ea37887d98c42a", "plan_text": "\tBatch_Point_Get\troot\ttable:GLOBAL_VARIABLES, index:PRIMARY(VARIABLE_NAME), keep order:false, desc:false", "timestamp_sec": [ 1641920402, 1641920405, 1641920408, 1641920411, 1641920414, 1641920417, 1641920420, 1641920423, 1641920426, 1641920429, 1641920432, 1641920435, 1641920438, 1641920441, 1641920444, 1641920447, 1641920450, 1641920453, 1641920456, 1641920459 ], "cpu_time_ms": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0 ], "exec_count_per_sec": 0.32786885245901637, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "e2769f5619db3dc4916298c4c6b3bc2c36c6d64a84e2a5549284521482564ce0", "sql_text": "select high_priority ( `variable_value` ) from `mysql` . `tidb` where `variable_name` = ? for update", "is_other": false, "cpu_time_ms": 0, "exec_count_per_sec": 0.06557377049180328, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "892b61d58192589f43d81f3ee50710d71e6a8c286a59877069eaa00cd3bb8f5c", "plan_text": "\tProjection \troot\tmysql.tidb.variable_value\n\t└─SelectLock \troot\t\n\t └─IndexLookUp \troot\t\n\t ├─IndexRangeScan\tcop \ttable:tidb, index:PRIMARY(VARIABLE_NAME), range:[?,?], keep order:false\n\t └─TableRowIDScan\tcop \ttable:tidb, keep order:false", "timestamp_sec": [1641920435, 1641920436], "cpu_time_ms": [0, 0], "exec_count_per_sec": 0.06557377049180328, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] }, { "sql_digest": "", "sql_text": "", "is_other": true, "cpu_time_ms": 0, "exec_count_per_sec": 0.7868852459016393, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0, "plans": [ { "plan_digest": "", "plan_text": "", "timestamp_sec": [ 1641920402, 1641920405, 1641920408, 1641920411, 1641920414, 1641920417, 1641920419, 1641920420, 1641920423, 1641920426, 1641920429, 1641920432, 1641920435, 1641920436, 1641920438, 1641920441, 1641920444, 1641920447, 1641920449, 1641920450, 1641920453, 1641920456, 1641920459 ], "cpu_time_ms": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], "exec_count_per_sec": 0.7868852459016393, "duration_per_exec_ms": 0, "scan_records_per_sec": 0, "scan_indexes_per_sec": 0 } ] } ], "status": "ok" } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/fixtures/uri.json ================================================ { "root": "/", "login": "/signin", "overview": "/overview", "slow_query": "/slow_query", "statement": "/statement", "configuration": "/configuration", "topsql": "/topsql" } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/components.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. export const testBaseSelectorOptions = (optionsList, dataE2EValue) => { cy.get(`[data-e2e=${dataE2EValue}]`) .click() .then(() => { cy.get('[data-e2e=multi_select_options]') .should('have.length', optionsList.length) .each(($option, $idx) => { cy.wrap($option).should('have.text', optionsList[$idx]) }) }) } export const checkAllOptionsInBaseSelector = (dataE2EValue) => { cy.get(`[data-e2e=${dataE2EValue}]`) .click() .then(() => { if (cy.get('.ant-dropdown').should('exist')) { cy.get('.ant-dropdown').within(() => { cy.get('[role=columnheader]') .eq(0) .within(() => { cy.get('.ant-checkbox').click() }) }) } }) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/login/login_session.spec.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. describe('Login session', () => { beforeEach(() => { cy.fixture('uri.json').then(function (uri) { this.uri = uri }) }) it('Redirect to sigin page when user not login', function () { cy.visit(this.uri.overview) /* eslint-disable no-unused-expressions */ expect(localStorage.getItem('dashboard_auth_token')).to.be.null cy.url().should('include', this.uri.login) }) // Use fake token to indicate session expired. it('Redirect user to sigin page when session token expired', function () { // Set `dashboard_auth_token` with an invalid token localStorage.setItem('dashboard_auth_token', 'invalid_auth_token') cy.visit(this.uri.overview) cy.url().should('include', this.uri.login) cy.get('.ant-message').should('be.visible') cy.get('.ant-message-error > span:last-child').should( 'has.text', 'Please sign in again (session is expired)' ) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/login/user_login.compat_spec.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. // Test User Login Compatibility // FEATURE_VERSION < 5.3.0 of TiDB Dashboard does not support nonRootLogin // FEATURE_VERSION >= 5.3.0 of TiDB Dashboard supports nonRootLogin describe('User Login', () => { if (Cypress.env('FEATURE_VERSION') === '6.0.0') { // Create user test before(() => { let queryData = { query: 'DROP USER IF EXISTS "test"@"%"' } cy.task('queryDB', { ...queryData }) queryData = { query: "CREATE USER 'test'@'%' IDENTIFIED BY 'test_pwd'" } cy.task('queryDB', { ...queryData }) queryData = { query: "GRANT ALL PRIVILEGES ON *.* TO 'test'@'%' WITH GRANT OPTION" } cy.task('queryDB', { ...queryData }) }) // Run before each test beforeEach(() => { // Load a fixed set of data located in cypress/fixtures. // Direct to login page cy.fixture('uri.json').then(function (uri) { this.uri = uri cy.visit(this.uri.overview) }) }) it('noRootLogin is supported', () => { cy.log('FEATURE_VERSION is: ', Cypress.env('FEATURE_VERSION')) // Check username input is not disabled cy.get('[data-e2e=signin_username_input]').should('not.be.disabled') }) it('nonRoot user with correct password', function () { cy.get('[data-e2e=signin_username_input]').clear().type('test') cy.get('[data-e2e="signin_password_input"]').type('test_pwd{enter}') cy.url().should('include', this.uri.overview) }) it('nonRoot user with incorrect password', () => { cy.intercept('POST', '/dashboard/api/user/login').as('login') cy.get('[data-e2e=signin_username_input]').clear().type('test') cy.get('[data-e2e="signin_password_input"]').type('incorrect_pwd{enter}') cy.wait('@login').should(({ response }) => { expect(response.body).to.have.property('code', 'tidb.tidb_auth_failed') }) }) } else if (Cypress.env('FEATURE_VERSION') === '5.0.0') { beforeEach(() => { cy.fixture('uri.json').then(function (uri) { this.uri = uri cy.visit(this.uri.overview) }) }) it('noRootLogin is unsupported', () => { cy.log('FEATURE_VERSION is: ', Cypress.env('FEATURE_VERSION')) // Check username input is disabled cy.get('[data-e2e=signin_username_input]').should('be.disabled') }) } }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/login/user_login.spec.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. // Test User login describe('Root User Login', () => { beforeEach(function () { cy.fixture('uri.json').then(function (uri) { this.uri = uri cy.visit(this.uri.root) }) }) it('authenticated redirect', function () { // Redirect to login cy.visit(this.uri.root) cy.url().should('eq', `${Cypress.config().baseUrl}${this.uri.root}`) }) it('root login with no pwd', function () { cy.get('[data-e2e=signin_username_input]').should('have.value', 'root') cy.get('[data-e2e=signin_submit]').click() cy.url().should('include', this.uri.overview) }) it('remember last succeeded login username', () => { cy.get('[data-e2e=signin_username_input]').should('have.value', 'root') }) it('root login with incorrect pwd', () => { cy.intercept('POST', `${Cypress.env('apiBasePath')}user/login`).as('login') // {enter} causes the form to submit cy.get('[data-e2e="signin_password_input"]').type( 'incorrect_password{enter}' ) cy.wait('@login').should(({ response }) => { expect(response.body).to.have.property('code', 'tidb.tidb_auth_failed') }) }) it('root login with correct pwd', function () { // set password for root let queryData = { query: 'SET PASSWORD FOR "root"@"%" = "root_pwd"' } cy.task('queryDB', { ...queryData }) cy.get('[data-e2e="signin_password_input"]').type('root_pwd{enter}') cy.url().should('include', this.uri.overview) // set empty password for root queryData = { query: 'SET PASSWORD FOR "root"@"%" = ""', password: 'root_pwd' } cy.task('queryDB', { ...queryData }) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/slow_query/01-list.spec.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import dayjs from 'dayjs' import { restartTiUP, validateSlowQueryCSVList, deleteDownloadsFolder } from '../utils' import { testBaseSelectorOptions } from '../components' const neatCSV = require('neat-csv') const path = require('path') describe('SlowQuery list page', () => { before(() => { cy.fixture('uri.json').then(function (uri) { this.uri = uri }) // Restart tiup restartTiUP() deleteDownloadsFolder() }) beforeEach(function () { cy.login('root') cy.visit(this.uri.slow_query) cy.url().should('include', this.uri.slow_query) }) describe('Initialize slow query page', () => { it('Slow query side bar highlighted', () => { cy.get('[data-e2e=menu_item_slow_query]') .should('be.visible') .and('has.class', 'ant-menu-item-selected') }) it('Has Toolbar', function () { cy.get('[data-e2e=slow_query_toolbar]').should('be.visible') }) it('Get slow query bad request', () => { const staticResponse = { statusCode: 400, body: { code: 'common.bad_request', error: true, message: 'common.bad_request' } } // stub out a response body cy.intercept( `${Cypress.env('apiBasePath')}slow_query/list*`, staticResponse ).as('slow_query_list') cy.wait('@slow_query_list').then(() => { cy.get('[data-e2e=alert_error_bar]').should( 'has.text', staticResponse.body.message ) }) }) }) describe('Filter slow query list', () => { it('Run workload', () => { const workloads = [ 'SELECT sleep(1);', 'SELECT sleep(0.4);', 'SELECT sleep(2);' ] const waitTwoSecond = (query, idx) => new Promise((resolve) => { // run workload every 5 seconds setTimeout(() => { resolve(query) }, 5000 * idx) }) workloads.forEach((query, idx) => { cy.wrap(waitTwoSecond(query, idx)).then((query) => { // return a promise to cy.then() that // is awaited until it resolves cy.task('queryDB', { query }) }) }) }) describe('Filter slow query by changing time range', () => { let defaultSlowQueryList let lastSlowQueryTimeStamp let firstQueryTimeRangeStart, secondQueryTimeRangeStart, thirdQueryTimeRangeStart, thirdQueryTimeRangeEnd it('Default time range is 30 mins', () => { cy.get('[data-e2e=selected_timerange]').should( 'has.text', 'Recent 30 min' ) }) it('Show all slow_query', () => { cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`).as( 'slow_query' ) cy.wait('@slow_query').then((res) => { defaultSlowQueryList = res.response.body if (defaultSlowQueryList.length > 0) { lastSlowQueryTimeStamp = defaultSlowQueryList[0].timestamp const calTimestamp = (timestampDiff) => { return dayjs .unix(lastSlowQueryTimeStamp - timestampDiff) .format('YYYY-MM-DD HH:mm:ss') } firstQueryTimeRangeStart = calTimestamp(12) secondQueryTimeRangeStart = calTimestamp(7) thirdQueryTimeRangeStart = calTimestamp(2) thirdQueryTimeRangeEnd = calTimestamp(-3) } }) }) describe('Check slow query', () => { it('Check slow query in the 1st 5 seconds time range', () => { cy.get('[data-e2e=timerange-selector]') .click() .then(() => { cy.get('.ant-picker-range').click() cy.get('.ant-picker-input-active').type( `${firstQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` ) cy.get('.ant-picker-input-active').type( `${secondQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` ) }) .then(() => { cy.get('[data-automation-key=query]') .should('has.length', 1) .and('has.text', 'SELECT sleep(1);') }) }) it('Check slow query in the 2nd 5 seconds time range', () => { cy.get('[data-e2e=timerange-selector]') .click() .then(() => { cy.get('.ant-picker-range').click() cy.get('.ant-picker-input-active').type( `${secondQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` ) cy.get('.ant-picker-input-active').type( `${thirdQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` ) }) .then(() => { cy.get('[data-automation-key=query]').should('has.length', 0) }) }) it('Check slow query in the 3rd 5 seconds time range', () => { cy.get('[data-e2e=timerange-selector]') .click() .then(() => { cy.get('.ant-picker-range').click() cy.get('.ant-picker-input-active').type( `${thirdQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` ) cy.get('.ant-picker-input-active').type( `${thirdQueryTimeRangeEnd}{leftarrow}{leftarrow}{backspace}{enter}` ) }) .then(() => { cy.get('[data-automation-key=query]') .should('has.length', 1) .and('has.text', 'SELECT sleep(2);') }) }) it('Check slow query in the latest 15 seconds time range', () => { cy.get('[data-e2e=timerange-selector]') .click() .then(() => { cy.get('.ant-picker-range').click() cy.get('.ant-picker-input-active').type( `${firstQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` ) cy.get('.ant-picker-input-active').type( `${thirdQueryTimeRangeEnd}{leftarrow}{leftarrow}{backspace}{enter}` ) }) .then(() => { cy.get('[data-automation-key=query]').should('has.length', 2) }) }) }) }) describe('Filter slow query by changing database', () => { it('No database selected by default', () => { cy.get('[data-e2e=base_select_input_text]').should( 'has.text', 'All Databases' ) }) it('Show all databases', () => { cy.intercept(`${Cypress.env('apiBasePath')}info/databases`).as( 'databases' ) cy.wait('@databases').then((res) => { const databaseList = res.response.body testBaseSelectorOptions(databaseList, 'execution_database_name') }) }) it('Run workload without use database', () => { let queryData = { query: 'SELECT sleep(1.5);', database: '' } cy.task('queryDB', { ...queryData }) // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(2000) cy.reload() // global and use database queries will be listed cy.get('[data-automation-key=query]').should('has.length', 3) cy.get('[data-e2e=base_select_input_text]') .click({ force: true }) .then(() => { cy.get('.ms-DetailsHeader-checkTooltip') .click({ force: true }) .then(() => { // global query will not be listed cy.get('[data-automation-key=query]').should('has.length', 2) }) }) }) }) describe('Search function', () => { it('Default search text', () => { cy.get('[data-e2e=slow_query_search]').should('be.empty') }) it('Search item with space', () => { cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`).as( 'slow_query_list' ) cy.wait('@slow_query_list') cy.get('[data-e2e=slow_query_search]').type( ' SELECT sleep\\(1\\) {enter}' ) cy.wait('@slow_query_list') cy.get('[data-automation-key=query]').should('has.length', 1) // clear search text cy.get('[data-e2e=slow_query_search]').clear().type('{enter}') cy.wait('@slow_query_list') cy.get('[data-automation-key=query]').should('has.length', 3) }) it('Type search without pressing enter then reload', () => { cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`).as( 'slow_query_list' ) cy.wait('@slow_query_list') cy.get('[data-e2e=slow_query_search]').type(' SELECT sleep\\(1\\)') cy.wait('@slow_query_list') cy.get('[data-automation-key=query]').should('has.length', 1) cy.reload() cy.get('[data-automation-key=query]').should('has.length', 1) }) }) describe('Slow query list limitation', () => { it('Default limit', () => { cy.get('[data-e2e=slow_query_limit_select]').contains('100') }) const limitOptions = ['100', '200', '500', '1000'] it('Check limit options', () => { cy.get('[data-e2e=slow_query_limit_select]') .click() .then(() => { cy.get('[data-e2e=slow_query_limit_option]') .should('have.length', 4) .each(($option, $idx) => { cy.wrap($option).contains(limitOptions[$idx]) }) }) }) it('Check config remembered', () => { cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`).as( 'slow_query_list' ) cy.wait('@slow_query_list') cy.get('[data-e2e=slow_query_limit_select]').click() cy.get('[data-e2e=slow_query_limit_option]').eq(1).click() cy.wait('@slow_query_list') cy.reload() cy.get('[data-e2e=slow_query_limit_select]').contains('200') }) }) describe('Selected Columns', () => { const defaultColumns = ['Query', 'Finish Time', 'Latency', 'Max Memory'] const defaultColumnsKeys = [ 'query', 'timestamp', 'query_time', 'memory_max' ] it('Default selected columns', () => { cy.get('[role=columnheader]') .not('.is-empty') .should('have.length', 4) .each(($column, $idx) => { cy.wrap($column).contains(defaultColumns[$idx]) }) }) it('Hover on columns selector and check selected fields ', () => { cy.get('[data-e2e=columns_selector_popover]') .trigger('mouseover') .then(() => { cy.get('[data-e2e=columns_selector_popover_content]') .should('be.visible') .within(() => { // check default selectedColumns checked defaultColumns.forEach((c, idx) => { cy.contains(c) .parent() .within(() => { cy.get( `[data-e2e=columns_selector_field_${defaultColumnsKeys[idx]}]` ).should('be.checked') }) }) }) }) }) it('Select all column fields and then reset', () => { cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`).as( 'slow_query_list' ) cy.wait('@slow_query_list') cy.get('[data-e2e=columns_selector_popover]').trigger('mouseover') cy.get('[data-e2e=column_selector_title]').check() cy.wait('@slow_query_list') cy.get('[role=columnheader]').not('.is-empty').should('have.length', 44) // Columns should be remembered cy.reload() cy.wait('@slow_query_list') cy.get('[role=columnheader]').not('.is-empty').should('have.length', 44) // Click reset cy.get('[data-e2e=columns_selector_popover]').trigger('mouseover') cy.get('[data-e2e=column_selector_reset]').click() cy.wait('@slow_query_list') cy.get('[role=columnheader]').not('.is-empty').should('have.length', 4) }) it('Select an arbitary column field', () => { cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`).as( 'slow_query_list' ) cy.wait('@slow_query_list') cy.get('[data-e2e=columns_selector_popover]').trigger('mouseover') cy.contains('Max Disk').within(() => { cy.get('[data-e2e=columns_selector_field_disk_max]').check() }) cy.wait('@slow_query_list') cy.get('[role=columnheader]') .not('.is-empty') .last() .should('have.text', 'Max Disk ') // FIXME: the next contains should be performed over the popup only // cy.contains('Max Disk').within(() => { // cy.get('[data-e2e=columns_selector_field_disk_max]').uncheck() // }) // cy.wait('@slow_query_list') // cy.get('[role=columnheader]').eq(1).should('have.text', 'Finish Time ') }) it('Check SLOW_QUERY_SHOW_FULL_SQL', () => { cy.get('[data-e2e=columns_selector_popover]') .trigger('mouseover') .then(() => { cy.get('[data-e2e=slow_query_show_full_sql]') .check() .then(() => { cy.get('[data-automation-key=query]') .eq(0) .find('[data-e2e=syntax_highlighter_original]') }) cy.get('[data-e2e=slow_query_show_full_sql]') .uncheck() .then(() => { cy.get('[data-automation-key=query]') .eq(0) .trigger('mouseover') .find('[data-e2e=syntax_highlighter_compact]') }) }) }) }) }) describe('Refresh table list', () => { it('Click refresh will update table list', () => { cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`).as( 'slow_query_list' ) cy.wait('@slow_query_list') cy.contains('SELECT sleep(1.2)').should('not.exist') const queryData = { query: 'SELECT sleep(1.2)' } cy.task('queryDB', { ...queryData }) cy.get('[data-e2e=slow_query_search]').type('{enter}') cy.wait('@slow_query_list') cy.contains('SELECT sleep(1.2)') }) }) describe('Table list order', () => { it('Default order(desc) by Timestamp', () => { const defaultOrderByTimestamp = [ 'SELECT sleep(1.2);', 'SELECT sleep(1.5);', 'SELECT sleep(2);', 'SELECT sleep(1);' ] cy.get('[data-automation-key=query]').each(($query, $idx) => { cy.wrap($query).should('have.text', defaultOrderByTimestamp[$idx]) }) }) it('Asc order by Timestamp', () => { const AscOrderByTimestamp = [ 'SELECT sleep(1);', 'SELECT sleep(2);', 'SELECT sleep(1.5);', 'SELECT sleep(1.2);' ] cy.get('[data-item-key=timestamp]') .should('be.visible') .click() .then(() => { cy.get('[data-automation-key=query]').each(($query, $idx) => { cy.wrap($query).should('have.text', AscOrderByTimestamp[$idx]) }) }) }) it('Desc/Asc order by Latency', () => { const DescOrderByLatency = [ 'SELECT sleep(2);', 'SELECT sleep(1.5);', 'SELECT sleep(1.2);', 'SELECT sleep(1);' ] cy.get('[data-item-key=query_time]') .should('be.visible') .click() .then(() => { // Desc order by Latency cy.get('[data-automation-key=query]').each(($query, $idx) => { cy.wrap($query).should('have.text', DescOrderByLatency[$idx]) }) }) .then(() => { const AscOrderByLatency = [ 'SELECT sleep(1);', 'SELECT sleep(1.2);', 'SELECT sleep(1.5);', 'SELECT sleep(2);' ] // Asc order by Latency cy.get('[data-item-key=query_time]') .should('be.visible') .click() .then(() => { cy.get('[data-automation-key=query]').each(($query, $idx) => { cy.wrap($query).should('have.text', AscOrderByLatency[$idx]) }) }) }) }) }) describe('Go to slow query detail page', () => { it('Click first slow query and go to detail page', function () { cy.get('[data-automationid=ListCell]') .eq(0) .click() .then(() => { cy.url().should('include', `${this.uri.slow_query}/detail`) cy.get('[data-e2e=syntax_highlighter_compact]').should( 'have.text', 'SELECT sleep(1.2);' ) }) }) }) // FIXME: The following tests will break slow-query details E2E since it executes a SQL. // Fix the slow-query details E2E first. // describe('Slow network condition', () => { // const slowNetworkText = 'On-the-fly update is disabled' // it('Does not show slow information when network is fast', () => { // cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`).as( // 'slow_query_list' // ) // cy.wait('@slow_query_list') // cy.wait(500) // cy.contains(slowNetworkText).should('not.exist') // }) // it('Show slow information', () => { // cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`, (req) => { // req.on('response', (res) => { // res.setDelay(3000) // }) // }).as('slow_query_list') // cy.wait('@slow_query_list') // cy.contains(slowNetworkText) // }) // it('Does not send request automatically when network is slow', () => { // cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`, (req) => { // req.on('response', (res) => { // res.setDelay(3000) // }) // }).as('slow_query_list') // cy.wait('@slow_query_list') // cy.contains(slowNetworkText) // const queryData = { // query: 'SELECT 41212, sleep(1)', // } // cy.task('queryDB', { ...queryData }) // cy.reload() // cy.wait('@slow_query_list') // cy.contains(slowNetworkText) // cy.get('[data-e2e=slow_query_search]').type('SELECT 41212') // cy.wait(1000) // cy.get('[data-e2e=syntax_highlighter_compact]').contains( // 'SELECT sleep(1.2)' // ) // TODO: this depends on a previous test to finish.. // // request is sent only after a manual refresh // cy.get('[data-e2e=slow_query_search]').type('{enter}') // cy.wait('@slow_query_list') // cy.get('[data-e2e=syntax_highlighter_compact]').contains('SELECT 41212') // cy.get('[data-e2e=syntax_highlighter_compact]') // .contains('SELECT sleep(1.2)') // .should('not.exist') // }) // it('Updates the info when network is no longer slow', () => { // let shouldDelay = true // cy.intercept(`${Cypress.env('apiBasePath')}slow_query/list*`, (req) => { // req.on('response', (res) => { // if (shouldDelay) { // res.setDelay(3000) // } // }) // }).as('slow_query_list') // cy.wait('@slow_query_list') // cy.contains(slowNetworkText) // cy.get('[data-e2e=syntax_highlighter_compact]') // .contains('SELECT sleep(1.2)') // .then(() => { // shouldDelay = false // }) // cy.get('[data-e2e=slow_query_search]').type('{enter}') // cy.wait('@slow_query_list') // cy.wait(500) // cy.contains(slowNetworkText).should('not.exist') // // On-the-fly request should be recovered // cy.get('[data-e2e=slow_query_search]').type('SELECT 41212') // cy.wait('@slow_query_list') // cy.get('[data-e2e=syntax_highlighter_compact]').contains('SELECT 41212') // cy.get('[data-e2e=syntax_highlighter_compact]') // .contains('SELECT sleep(1.2)') // .should('not.exist') // }) // }) describe('Export slow query CSV ', () => { it('validate CSV File', () => { const downloadsFolder = Cypress.config('downloadsFolder') let downloadedFilename cy.get('[data-e2e=slow_query_export_menu]') .trigger('mouseover') .then(() => { cy.window() .document() .then(function (doc) { // Clicking link to download file causes page load timeout // it's a workround that fires a new page load event to skip this issue // Related issue: https://github.com/cypress-io/cypress/issues/14857 doc.addEventListener('click', () => { setTimeout(function () { doc.location?.reload() }, 5000) }) // Make sure the file exists cy.intercept( `${Cypress.env('apiBasePath')}slow_query/download?token=*` ).as('download_slow_query') cy.get('[data-e2e=slow_query_export_btn]').click() }) }) .then(() => { cy.wait('@download_slow_query').then((res) => { // join downloadFolder with CSV filename const filenameRegx = /"(.*)"/ downloadedFilename = path.join( downloadsFolder, res.response.headers['content-disposition'].match(filenameRegx)[1] ) cy.readFile(downloadedFilename, { timeout: 15000 }) // parse CSV text into objects .then(neatCSV) .then(validateSlowQueryCSVList) }) }) }) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/slow_query/02-detail.spec.js ================================================ describe('Slow query detail page E2E test', () => { before(() => { cy.fixture('uri.json').then(function (uri) { this.uri = uri }) }) beforeEach(function () { cy.login('root') cy.visit(this.uri.slow_query) cy.url().should('include', this.uri.slow_query) cy.intercept(`${Cypress.env('apiBasePath')}slow_query/detail?*`).as( 'slow_query_detail' ) cy.get('[data-automation-key=query]').eq(0).click() }) describe('Query descriptions', () => { it('Check sql and default format', () => { // sql is collapsed by default cy.get('[data-e2e=expandText]').eq(0).should('have.text', 'Expand') cy.get('[data-e2e=statement_query_detail_page_query]') .eq(0) .find('[data-e2e=syntax_highlighter_compact]') .and('have.text', 'SELECT sleep(1.2);') }) it('Expand sql', () => { // expand sql cy.get('[data-e2e=expandText]').eq(0).click() // sql is collapsed by default cy.get('[data-e2e=collapseText]').eq(0).should('have.text', 'Collapse') cy.get('[data-e2e=statement_query_detail_page_query]') .eq(0) .find('[data-e2e=syntax_highlighter_original]') .and('have.text', 'SELECT\n sleep(1.2);') }) it('Copy formatted sql to clipboard', () => { // Prompt alert when testing copy to clipboard on chromium-based browser // https://github.com/cypress-io/cypress/issues/2739 cy.window().then((win) => { cy.stub(win, 'prompt').returns(win.prompt).as('copyToClipboardPrompt') }) // cypress cannot simulate copy to the clipboard event, // we need to use realClick to fire copy event. // Related desc: https://github.com/dmtrKovalenko/cypress-real-events#why cy.get('[data-e2e=copy_formatted_sql_to_clipboard]') .realClick() .then(() => { cy.task('getClipboard').should('eq', 'SELECT\n sleep(1.2);') }) cy.get('[data-e2e=copied_success]').should('exist') }) it('Copy original sql to clipboard', () => { cy.window().then((win) => { cy.stub(win, 'prompt').returns(win.prompt).as('copyToClipboardPrompt') }) cy.get('[data-e2e=copy_original_sql_to_clipboard]') .realClick() .then(() => { cy.task('getClipboard').should('eq', 'SELECT sleep(1.2);') }) cy.get('[data-e2e=copied_success]').should('exist') }) }) describe('Plan descriptions', () => { it('Check sql and default format', () => { // sql is collapsed by default cy.get('[data-e2e=expandText]').eq(1).should('have.text', 'Expand') cy.wait('@slow_query_detail').then((res) => { const responseBody = res.response.body cy.get('[data-e2e=statement_query_detail_page_query]') .eq(1) .and('have.text', responseBody.plan) }) }) }) describe('Detail tabs', () => { it('Check tabs list', () => { const tabList = ['Basic', 'Time', 'Coprocessor', 'Transaction'] cy.get('[data-e2e=tabs]') .find('.ant-tabs-tab') .should('have.length', 4) .each(($tab, index) => { cy.wrap($tab).should('have.text', tabList[index]) }) }) }) describe('Detail table tabs', () => { it('Basic table rows count', () => { cy.get('.ms-List-cell').should('have.length', 14) }) it('Time table rows count', () => { cy.get('.ant-tabs-tab').eq(1).click() cy.get('.ms-List-cell').should('have.length', 21) }) it('Coprocessor table rows count', () => { cy.get('.ant-tabs-tab').eq(2).click() cy.get('.ms-List-cell').should('have.length', 10) }) it('Transaction table rows count', () => { cy.get('.ant-tabs-tab').eq(3).click() cy.get('.ms-List-cell').should('have.length', 5) }) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/slow_query/list.compat_spec.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import { skipOn } from '@cypress/skip-test' describe('SlowQuery list compatibility test', () => { before(() => { cy.fixture('uri.json').then(function (uri) { this.uri = uri }) }) beforeEach(function () { cy.login('root') cy.visit(this.uri.slow_query) cy.url().should('include', this.uri.slow_query) }) describe('Available fields', () => { skipOn(Cypress.env('FEATURE_VERSION') !== '6.0.0', () => { it('Show all available fields', () => { cy.intercept( `${Cypress.env('apiBasePath')}slow_query/available_fields` ).as('getAvailableFields') cy.wait('@getAvailableFields') const availableFields = [ 'query', 'digest', 'instance', 'db', 'connection_id', 'timestamp', 'query_time', 'parse_time', 'compile_time', 'process_time', 'memory_max', 'disk_max', 'txn_start_ts', 'success', 'is_internal', 'index_names', 'stats', 'backoff_types', 'user', 'host', 'wait_time', 'backoff_time', 'get_commit_ts_time', 'local_latch_wait_time', 'prewrite_time', 'commit_time', 'commit_backoff_time', 'resolve_lock_time', 'cop_proc_avg', 'cop_wait_avg', 'write_keys', 'write_size', 'prewrite_region', 'txn_retry', 'request_count', 'process_keys', 'total_keys', 'cop_proc_addr', 'cop_wait_addr', 'rocksdb_delete_skipped_count', 'rocksdb_key_skipped_count', 'rocksdb_block_cache_hit_count', 'rocksdb_block_read_count', 'rocksdb_block_read_byte' ] cy.get('[data-e2e="columns_selector_popover"]').trigger('mouseover') availableFields.forEach((f) => { cy.get(`[data-e2e="columns_selector_field_${f}"]`).should('exist') }) }) }) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/statement/01-list.spec.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import dayjs from 'dayjs' import { restartTiUP, validateStatementCSVList, deleteDownloadsFolder } from '../utils' import { testBaseSelectorOptions, checkAllOptionsInBaseSelector } from '../components' const neatCSV = require('neat-csv') const path = require('path') describe('SQL statements list page', () => { before(() => { cy.fixture('uri.json').then(function (uri) { this.uri = uri }) restartTiUP() deleteDownloadsFolder() }) beforeEach(function () { cy.login('root') cy.visit(this.uri.statement) cy.url().should('include', this.uri.statement) }) const defaultExecStmtList = [ 'SHOW DATABASES', 'SELECT DISTINCT `stmt_type` FROM `information_schema`.`cluster_statements_summary_history` ORDER BY `stmt_type` ASC', 'SELECT `version` ()' ] describe('Initialize statement list page', () => { it('Statement side bar highlighted', () => { cy.get('[data-e2e=menu_item_statement]') .should('be.visible') .and('has.class', 'ant-menu-item-selected') }) it('Has Toolbar', function () { cy.get('[data-e2e=statement_toolbar]').should('be.visible') }) it('Statements is enabled by default', () => { cy.get('[data-e2e=statements_table]').should('be.visible') }) it('Get statement list bad request', () => { const staticResponse = { statusCode: 400, body: { code: 'common.bad_request', error: true, message: 'common.bad_request' } } // stub out a response body cy.intercept( `${Cypress.env('apiBasePath')}statements/list*`, staticResponse ).as('statements_list') cy.wait('@statements_list').then(() => { cy.get('[data-e2e=alert_error_bar]').should( 'has.text', staticResponse.body.message ) }) }) it('Statements which executed by default when starting TiDB', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@statements_list').then((res) => { const response = res.response.body cy.get('[data-e2e=syntax_highlighter_compact]') .should('have.length', response.length) .then(($stmts) => { // we get a list of jQuery elements // let's convert the jQuery object into a plain array return ( Cypress.$.makeArray($stmts) // and extract inner text from each .map((stmt) => stmt.innerText) ) }) // make sure there exists the default executed statements .should('to.include.members', defaultExecStmtList) }) }) }) describe('Time range selector', () => { beforeEach(() => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@statements_list') // select last_seen column field cy.get('[data-e2e=columns_selector_popover]').trigger('mouseover') cy.contains('Last Seen').within(() => { cy.get('[data-e2e=columns_selector_field_last_seen]').check({ force: true }) }) }) const checkStmtListWithTimeRange = (stmtList, timeDiff) => { const now = dayjs().unix() stmtList.forEach((stmt) => { cy.wrap(stmt.last_seen) .should('be.lte', now) .and('be.gt', now - timeDiff) }) } describe('Common time range selector', () => { it('Default time range', () => { cy.get('[data-e2e=timerange-selector]').should( 'have.text', 'Recent 30 min' ) }) it('Init statement list', () => { cy.wait('@statements_list').then((res) => { const response = res.response.body cy.get('[data-automation-key=digest_text]').should( 'have.length', response.length ) checkStmtListWithTimeRange(response, 1800) }) }) it('Select time range as recent 15 mins', () => { cy.wait('@statements_list') // select recent 15 mins cy.get('[data-e2e=selected_timerange]') .click() .then(() => { cy.get('[data-e2e=timerange-900]').click() }) cy.wait('@statements_list').then((res) => { const response = res.response.body checkStmtListWithTimeRange(response, 900) }) // time rage will be remebered after reload page cy.reload() cy.get('[data-e2e=selected_timerange]').should( 'have.text', 'Recent 15 min' ) }) }) }) describe('Filter statements by changing database', () => { it('No database selected by default', () => { cy.get('[data-e2e=base_select_input_text]') .eq(0) .should('has.text', 'All Databases') }) it('Show all databases', () => { cy.intercept(`${Cypress.env('apiBasePath')}info/databases`).as( 'databases' ) cy.wait('@databases').then((res) => { const databases = res.response.body testBaseSelectorOptions(databases, 'execution_database_name') }) }) it('Filter statements without use database', () => { cy.intercept(`${Cypress.env('apiBasePath')}info/databases`).as( 'databases' ) cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@databases').wait('@statements_list') // check all options in databases selector checkAllOptionsInBaseSelector('execution_database_name') // check the existence of statements without use database cy.wait('@statements_list') cy.contains(defaultExecStmtList[0]).should('not.exist') cy.contains(defaultExecStmtList[2]).should('not.exist') }) it('Filter statements with use database (mysql)', () => { let queryData = { query: 'SELECT count(*) from user;', database: 'mysql' } cy.task('queryDB', { ...queryData }) cy.reload() cy.intercept(`${Cypress.env('apiBasePath')}info/databases`).as( 'databases' ) cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@databases').wait('@statements_list') cy.get('[data-e2e=execution_database_name]') .eq(0) .click() .get('.ant-dropdown') .within(() => { cy.get('.ant-checkbox-input').eq(3).click() }) cy.wait('@statements_list') cy.contains('SELECT count (?) FROM user;').should('exist') // Use databases config remembered cy.reload() cy.get('[data-e2e=base_select_input_text]') .eq(0) .should('has.text', '1 Databases') }) }) describe('Filter statements by changing kind', () => { it('No kind selected by default', () => { cy.get('[data-e2e=base_select_input_text]') .eq(1) .should('has.text', 'All Kinds') }) it('Show all kind of statements', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/stmt_types`).as( 'stmt_types' ) cy.wait('@stmt_types').then((res) => { const stmtTypesList = res.response.body testBaseSelectorOptions(stmtTypesList, 'statement_types') }) }) it('Filter statements with all kind checked', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/stmt_types`).as( 'stmt_types' ) cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait(['@stmt_types', '@statements_list']).then((interceptions) => { // check all options in kind selector checkAllOptionsInBaseSelector('statement_types') const statementsList = interceptions[1].response.body cy.get('[data-e2e=syntax_highlighter_compact]').should( 'have.length', statementsList.length ) }) }) it('Filter statements with one kind checked (select)', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/stmt_types`).as( 'stmt_types' ) cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@stmt_types').wait('@statements_list') cy.get('[data-e2e=statement_types]') .click() .get('.ant-dropdown') .within(() => { cy.get('[data-e2e=multi_select_options]') .contains('Select') .click({ force: true }) }) cy.wait('@statements_list') .get('[data-e2e=syntax_highlighter_compact]') .each(($sql) => { cy.wrap($sql).contains('SELECT') }) }) }) describe('Search function', () => { it('Default search text', () => { cy.get('[data-e2e=sql_statements_search]').should('be.empty') }) it('Search item with space', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@statements_list') cy.get('[data-e2e=syntax_highlighter_compact]').contains('SHOW DATABASES') cy.get('[data-e2e=sql_statements_search]').type('SELECT version') cy.wait('@statements_list') .get('[data-e2e=syntax_highlighter_compact]') .contains('SELECT `version` ()') cy.get('[data-e2e=syntax_highlighter_compact]') .contains('SHOW DATABASES') .should('not.exist') // check search text remembered after reload page cy.reload() cy.wait('@statements_list') cy.get('[data-e2e=syntax_highlighter_compact]').contains( 'SELECT `version` ()' ) // this should be filtered away cy.get('[data-e2e=syntax_highlighter_compact]') .contains('SHOW DATABASES') .should('not.exist') }) it('Type search then reload', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@statements_list') cy.get('[data-e2e=sql_statements_search]') .type('SELECT `version` ()') .wait('@statements_list') cy.reload() cy.wait('@statements_list').then((res) => { const statementsList = res.response.body cy.get('[data-e2e=syntax_highlighter_compact]').should( 'has.length', statementsList.length ) }) }) }) describe('Selected Columns', () => { const defaultColumns = { digest_text: 'Statement Template ', sum_latency: 'Total Latency ', avg_latency: 'Mean Latency ', exec_count: '# Exec ', plan_count: '# Plans ' } it('Default selected columns', () => { cy.get('[role=columnheader]') .not('.is-empty') .should('have.length', 5) .each(($column, idx) => { cy.wrap($column).contains( defaultColumns[Object.keys(defaultColumns)[idx]] ) }) }) it('Hover on columns selector and check selected fields', () => { cy.get('[data-e2e=columns_selector_popover]') .trigger('mouseover') .then(() => { cy.get('[data-e2e=columns_selector_popover_content]') .should('be.visible') .within(() => { cy.get('.ant-checkbox-wrapper-checked') // .should('have.length', 5) .then(($options) => { return Cypress.$.makeArray($options).map( (option) => option.innerText ) }) // make sure there exists the default executed statements .should('to.deep.eq', Object.values(defaultColumns)) }) }) }) it('Select all column fields', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@statements_list') cy.get('[data-e2e=columns_selector_popover]') .trigger('mouseover') .get('[data-e2e=column_selector_title]') .check() cy.wait('@statements_list') .get('[role=columnheader]') .not('.is-empty') .should('have.length', 43) // Columns should be remembered cy.reload() cy.wait('@statements_list') .get('[role=columnheader]') .not('.is-empty') .should('have.length', 43) }) it('Reset selected column fields', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@statements_list') cy.get('[data-e2e=columns_selector_popover]') .trigger('mouseover') .get('[data-e2e=column_selector_title]') .check() cy.wait('@statements_list') .get('[role=columnheader]') .not('.is-empty') .should('have.length', 43) cy.get('[data-e2e=columns_selector_popover]') .trigger('mouseover') .get('[data-e2e=column_selector_reset]') .click() cy.wait('@statements_list') .get('[role=columnheader]') .not('.is-empty') .should('have.length', 5) }) it('Select an arbitary column field', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@statements_list') cy.get('[data-e2e=columns_selector_popover]').trigger('mouseover') cy.contains('Total Coprocessor Tasks') .within(() => { cy.get('[data-e2e=columns_selector_field_sum_cop_task_num]').check() }) .then(() => { cy.wait('@statements_list') cy.get('[data-item-key=sum_cop_task_num]').should( 'have.text', 'Total Coprocessor Tasks' ) }) // FIXME: the next contains should be performed over the popup only // cy.contains('Total Coprocessor Tasks') // .within(() => { // cy.get('[data-e2e=columns_selector_field_sum_cop_task_num]').uncheck() // }) // .then(() => { // cy.wait('@statements_list') // cy.get('[data-item-key=sum_cop_task_num]').should('not.exist') // }) }) it('Check SHOW_FULL_QUERY_TEXT', () => { cy.get('[data-e2e=columns_selector_popover]') .trigger('mouseover', { force: true }) .then(() => { cy.get('[data-e2e=statement_show_full_sql]') .check() .then(() => { cy.get('[data-automation-key=digest_text]') .eq(0) .find('[data-e2e=syntax_highlighter_original]') }) cy.get('[data-e2e=statement_show_full_sql]') .uncheck() .then(() => { cy.get('[data-automation-key=digest_text]') .eq(0) .trigger('mouseover', { force: true }) .find('[data-e2e=syntax_highlighter_compact]') }) }) }) }) describe('Reload statement', () => { it('Reload statement table shows new query', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@statements_list') cy.get('[data-e2e=syntax_highlighter_compact]') .contains('SELECT count (?) FROM tidb') .should('not.exist') // send a query now let queryData = { query: 'select count(*) from tidb;', database: 'mysql' } cy.task('queryDB', { ...queryData }) // refresh! cy.get('[data-e2e=sql_statements_search]').type('{enter}') cy.wait('@statements_list') cy.get('[data-e2e=syntax_highlighter_compact]').contains( 'SELECT count (?) FROM tidb' ) }) }) const calcStmtHistorySize = (refreshInterval, historySize) => { const totalMins = refreshInterval * historySize const day = Math.floor(totalMins / (24 * 60)) const hour = Math.floor((totalMins - day * 24 * 60) / 60) const min = totalMins - day * 24 * 60 - hour * 60 return `${day} day ${hour} hour ${min} min` } describe('Statement Setting', function () { it('Close setting panel', () => { // close panel by clicking mask cy.get('[data-e2e=statement_setting]') .click() .then(() => { cy.get('.ant-drawer-mask') .click() .then(() => { cy.get('.ant-drawer-content').should('not.be.visible') }) }) // close panel by clicking close icon cy.get('[data-e2e=statement_setting]') .click() .then(() => { cy.get('.ant-drawer-close') .click() .then(() => { cy.get('.ant-drawer-content').should('not.be.visible') }) }) }) const switchStatement = (isCurrentlyEnabled) => { cy.get('[data-e2e=statement_setting]') .click() .then(() => { cy.get('.ant-drawer-content').should('exist') cy.get('[data-e2e=statemen_enbale_switcher]') // the current of switcher is isEnabled .should('have.attr', 'aria-checked', isCurrentlyEnabled) .click() cy.get('[data-e2e=submit_btn]').click() }) } it('Disable statement feature', () => { switchStatement('true') cy.contains('Current statement history will be cleared.') cy.get('.ant-modal-confirm-btns').find('.ant-btn-dangerous').click() cy.get('[data-e2e=statements_table]').should('not.exist') }) it('Save again when statement feature is disabled', () => { cy.get('[data-e2e=statement_setting]') .click() .then(() => { cy.get('.ant-drawer-content').should('exist') cy.get('[data-e2e=submit_btn]').click() }) // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500) cy.contains('Current statement history will be cleared.').should( 'not.exist' ) }) it('Enable statement feature', () => { switchStatement('false') cy.get('[data-e2e=statements_table]').should('exist') }) describe('Default statement setting', () => { beforeEach(() => { cy.intercept(`${Cypress.env('apiBasePath')}statements/config`).as( 'statements_config' ) cy.get('[data-e2e=statement_setting]').click() // get refresh_interval value cy.get(`[data-e2e=statement_setting_refresh_interval]`).within(() => { cy.get('.ant-slider-handle') .invoke('attr', 'aria-valuenow') .as('refreshIntervalVal') }) // get history_size value cy.get(`[data-e2e=statement_setting_history_size]`).within(() => { cy.get('.ant-slider-handle') .invoke('attr', 'aria-valuenow') .as('historySizeVal') }) }) const checkSilder = (sizeList, defaultValueNow, dataE2EValue) => { cy.wait('@statements_config').then(() => { cy.get(`[data-e2e=${dataE2EValue}]`).within(() => { cy.get('.ant-slider-handle').should( 'have.attr', 'aria-valuenow', defaultValueNow ) cy.get('.ant-slider-mark-text') .then(($marks) => { return Cypress.$.makeArray($marks).map((mark) => mark.innerText) }) // make sure there exists the default executed statements .should('to.deep.eq', sizeList) }) }) } it('Default statement setting max size', () => { const sizeList = ['200', '1000', '2000', '5000'] const defaultMaxSizeValue = Cypress.env('TIDB_VERSION') === 'v5.0.0' ? '200' : '3000' checkSilder(sizeList, defaultMaxSizeValue, 'statement_setting_max_size') }) it('Default statement setting window size', () => { const sizeList = ['1', '5', '15', '30', '60'] checkSilder(sizeList, '30', 'statement_setting_refresh_interval') }) it('Default Statement setting number of windows', () => { const sizeList = ['1', '255'] checkSilder(sizeList, '24', 'statement_setting_history_size') }) it('Default Check History Size', function () { const stmtHistorySize = calcStmtHistorySize( this.refreshIntervalVal, this.historySizeVal ) cy.get('[data-e2e=statement_setting_keep_duration]').within(() => { cy.get('.ant-form-item-control-input-content').should( 'have.text', stmtHistorySize ) }) }) }) describe('Update statement setting', function () { beforeEach(function () { cy.get('[data-e2e=statement_setting]').click() }) it('Update window size and number of windows', function () { // change window size cy.get('[data-e2e=statement_setting_refresh_interval]').within(() => { cy.get('.ant-slider-step') .find('.ant-slider-dot') .eq(2) .click() .then(() => { cy.get('.ant-slider-handle') .invoke('attr', 'aria-valuenow') .as('refreshIntervalVal') }) }) // change number of windows cy.get('[data-e2e=statement_setting_history_size]').within(() => { cy.get('.ant-slider-step') .find('.ant-slider-dot') .eq(1) .click() .then(() => { cy.get('.ant-slider-handle') .invoke('attr', 'aria-valuenow') .as('historySizeVal') }) }) cy.get('@refreshIntervalVal').then((refreshIntervalVal) => { cy.get('@historySizeVal').then((historySizeVal) => { cy.get('[data-e2e=statement_setting_keep_duration]').within(() => { // check statement history size by calculating window size and # windows const stmtHistorySize = calcStmtHistorySize( refreshIntervalVal, historySizeVal ) cy.get('.ant-form-item-control-input-content').should( 'have.text', stmtHistorySize ) }) cy.intercept( 'POST', `${Cypress.env('apiBasePath')}statements/config` ).as('update_config') cy.get('[data-e2e=submit_btn]').click() cy.wait('@update_config').then(() => { // check configuration whether come to effect or not cy.visit(this.uri.configuration) cy.url().should('include', this.uri.configuration) cy.get('[data-e2e=search_config]').type( 'tidb_stmt_summary_refresh_interval' ) // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000) cy.get('[data-automation-key=key]').contains( 'tidb_stmt_summary_refresh_interval' ) cy.get('[data-automation-key=value]').contains( refreshIntervalVal * 60 ) cy.get('[data-e2e=search_config]') .clear() .type('tidb_stmt_summary_history_size') // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000) cy.get('[data-automation-key=key]').contains( 'tidb_stmt_summary_history_size' ) cy.get('[data-automation-key=value]').contains(historySizeVal) }) }) }) }) it('Failed to save config list', () => { cy.on('uncaught:exception', function () { return false }) const staticResponse = { statusCode: 400, body: { code: 'common.bad_request', error: true, message: 'common.bad_request' } } // stub out a response body cy.intercept( 'POST', `${Cypress.env('apiBasePath')}statements/config`, staticResponse ).as('statements_config') cy.get('[data-e2e=submit_btn]').click() cy.wait('@statements_config').then(() => { // get error notifitcation on modal cy.get('.ant-modal-confirm-content').should( 'has.text', staticResponse.body.message ) }) }) }) }) describe('Simulate bad request', () => { beforeEach(() => { const staticResponse = { statusCode: 400, body: { code: 'common.bad_request', error: true, message: 'common.bad_request' } } // stub out a response body cy.intercept( `${Cypress.env('apiBasePath')}statements/config`, staticResponse ).as('failed_to_get_statements_config') cy.get('[data-e2e=statement_setting]').click() }) it('Get config list bad request', () => { cy.wait('@failed_to_get_statements_config').then(() => { // get error alert on panel cy.get('.ant-drawer-body').within(() => { cy.get('[data-e2e=alert_error_bar]').should( 'has.text', 'common.bad_request' ) }) }) }) }) describe('Slow network condition', () => { const slowNetworkText = 'On-the-fly update is disabled' it('Does not show slow information when network is fast', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.wait('@statements_list') // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500) cy.contains(slowNetworkText).should('not.exist') }) it('Show slow information', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`, (req) => { req.on('response', (res) => { res.setDelay(3000) }) }).as('statements_list') cy.wait('@statements_list') cy.contains(slowNetworkText) }) it('Does not send request automatically when network is slow', () => { cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`, (req) => { req.on('response', (res) => { res.setDelay(3000) }) }).as('statements_list') cy.wait('@statements_list') cy.contains(slowNetworkText) cy.get('[data-e2e=sql_statements_search]').type('SELECT version') // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000) cy.get('[data-e2e=syntax_highlighter_compact]').contains('SHOW DATABASES') // request is sent only after a manual refresh cy.get('[data-e2e=sql_statements_search]').type('{enter}') cy.wait('@statements_list') cy.get('[data-e2e=syntax_highlighter_compact]').contains( 'SELECT `version` ()' ) cy.get('[data-e2e=syntax_highlighter_compact]') .contains('SHOW DATABASES') .should('not.exist') }) it('Updates the info when network is no longer slow', () => { let shouldDelay = true cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`, (req) => { req.on('response', (res) => { if (shouldDelay) { res.setDelay(3000) } }) }).as('statements_list') cy.wait('@statements_list') cy.contains(slowNetworkText) cy.get('[data-e2e=syntax_highlighter_compact]') .contains('SHOW DATABASES') .then(() => { shouldDelay = false }) cy.get('[data-e2e=sql_statements_search]').type('{enter}') cy.wait('@statements_list') // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500) cy.contains(slowNetworkText).should('not.exist') // On-the-fly request should be recovered cy.get('[data-e2e=sql_statements_search]').type('SELECT version') cy.wait('@statements_list') cy.get('[data-e2e=syntax_highlighter_compact]').contains( 'SELECT `version` ()' ) cy.get('[data-e2e=syntax_highlighter_compact]') .contains('SHOW DATABASES') .should('not.exist') }) }) describe('Export statement CSV ', () => { it('validate CSV File', () => { const downloadsFolder = Cypress.config('downloadsFolder') let downloadedFilename cy.get('[data-e2e=statement_export_menu]') .trigger('mouseover') .then(() => { cy.window() .document() .then(function (doc) { doc.addEventListener('click', () => { setTimeout(function () { doc.location?.reload() }, 5000) }) // Make sure the file exists cy.intercept( `${Cypress.env('apiBasePath')}statements/download?token=*` ).as('download_statement') cy.get('[data-e2e=statement_export_btn]').click() }) }) .then(() => { cy.wait('@download_statement').then((res) => { // join downloadFolder with CSV filename const filenameRegx = /"(.*)"/ downloadedFilename = path.join( downloadsFolder, res.response.headers['content-disposition'].match(filenameRegx)[1] ) cy.readFile(downloadedFilename, { timeout: 15000 }) // parse CSV text into objects .then(neatCSV) .then(validateStatementCSVList) }) }) }) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/statement/02-detail.spec.js ================================================ describe('Statement detail page E2E test', () => { before(() => { const workloads = [ 'DROP TABLE IF EXISTS mysql.t;', 'CREATE TABLE `t` (`a` bigint(20) DEFAULT NULL, `b` bigint(20) DEFAULT NULL, `c` timestamp(6) DEFAULT CURRENT_TIMESTAMP(6), `d` varchar(50) DEFAULT NULL, UNIQUE KEY `idx0` (`a`), KEY `idx1` (`b`), KEY `idx2` (`b`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;', 'select /*+ USE_INDEX(t, idx1) */ count(*) from t where b < 100;', 'select /*+ USE_INDEX(t, idx2) */ count(*) from t where b < 100;' ] workloads.forEach((query) => { cy.task('queryDB', { query }) }) cy.fixture('uri.json').then(function (uri) { this.uri = uri }) }) beforeEach(function () { cy.login('root') cy.intercept( `${Cypress.env('apiBasePath')}statements/plans?begin_time=*` ).as('statements_plans') cy.intercept(`${Cypress.env('apiBasePath')}statements/plan/detail?*`).as( 'statements_plan_detail' ) cy.intercept(`${Cypress.env('apiBasePath')}statements/list*`).as( 'statements_list' ) cy.visit(this.uri.statement) cy.url().should('include', this.uri.statement) cy.wait('@statements_list') cy.get('[data-automation-key=plan_count]') .contains(2) .eq(0) .click({ force: true }) }) describe('Statement Template', () => { it('Check sql and default format', () => { // sql is collapsed by default cy.get('[data-e2e=expandText]').eq(0).should('have.text', 'Expand') cy.get('[data-e2e=statement_query_detail_page_query]') .eq(0) .find('[data-e2e=syntax_highlighter_compact]') .and('have.text', 'SELECT count (?) FROM `t` WHERE `b` < ?;') }) it('Expand sql', () => { // expand sql cy.get('[data-e2e=expandText]').eq(0).click() // sql is collapsed by default cy.get('[data-e2e=collapseText]').eq(0).should('have.text', 'Collapse') cy.get('[data-e2e=statement_query_detail_page_query]') .eq(0) .find('[data-e2e=syntax_highlighter_original]') .and('have.text', 'SELECT\n count (?)\nFROM\n `t`\nWHERE\n `b` < ?;') }) it('Copy formatted sql to clipboard', () => { cy.window().then((win) => { cy.stub(win, 'prompt').returns(win.prompt).as('copyToClipboardPrompt') }) cy.get('[data-e2e=copy_formatted_sql_to_clipboard]') .realClick() .then(() => { cy.task('getClipboard').should( 'eq', 'SELECT\n count (?)\nFROM\n `t`\nWHERE\n `b` < ?;' ) }) cy.get('[data-e2e=copied_success]').should('exist') }) it('Copy original sql to clipboard', () => { cy.window().then((win) => { cy.stub(win, 'prompt').returns(win.prompt).as('copyToClipboardPrompt') }) cy.get('[data-e2e=copy_original_sql_to_clipboard]') .realClick() .then(() => { cy.task('getClipboard').should( 'eq', 'select count ( ? ) from `t` where `b` < ? ;' ) }) cy.get('[data-e2e=copied_success]').should('exist') }) }) describe('Query Template', () => { it('Check sql and default format', () => { cy.wait('@statements_plan_detail').then((res) => { const response = res.response.body cy.get('.ant-descriptions-row') .eq(3) .within(() => { cy.get('.ant-descriptions-item') .eq(0) .and('have.text', response.digest) }) }) }) }) describe('Plans', () => { it('Has multiple execution plans', () => { cy.wait('@statements_plans').then((res) => { const response = res.response.body const plansDigest = [] response.forEach((plan) => plansDigest.push(plan.plan_digest)) cy.get('[data-e2e=statement_multiple_execution_plans]') .should('be.visible') .within(() => { // check digest of each plan cy.get('[data-automation-key=plan_digest]') .should('have.length', 2) .then(($plans) => { return Cypress.$.makeArray($plans).map((plan) => plan.innerText) }) .should('to.deep.equal', plansDigest) // all plans are checked cy.get('.ms-DetailsList-headerWrapper').within(() => { cy.get('.ant-checkbox').should( 'have.class', 'ant-checkbox-checked' ) }) }) }) }) }) describe('Detail tabs', () => { it('Check tabs list', () => { const tabList = [ 'Basic', 'Time', 'Coprocessor Read', 'Transaction', 'Slow Query' ] cy.get('[data-e2e=tabs]') .find('.ant-tabs-tab') .should('have.length', 5) .each(($tab, index) => { cy.wrap($tab).should('have.text', tabList[index]) }) }) }) describe('Detail table tabs', () => { it('Basic table rows count', () => { cy.wait('@statements_plan_detail').then(() => { cy.get('[data-e2e=statement_pages_detail_tabs_basic]').within(() => { cy.get('.ms-List-cell').should('have.length', 13) }) }) }) it('Time table rows count', () => { cy.get('.ant-tabs-tab').eq(1).click() cy.wait('@statements_plan_detail').then(() => { cy.get('[data-e2e=statement_pages_detail_tabs_time]').within(() => { cy.get('.ms-List-cell').should('have.length', 12) }) }) }) it('Coprocessor table rows count', () => { cy.get('.ant-tabs-tab').eq(2).click() cy.wait('@statements_plan_detail').then(() => { cy.get('[data-e2e=statement_pages_detail_tabs_copr]').within(() => { cy.get('.ms-List-cell').should('have.length', 15) }) }) }) it('Transaction table rows count', () => { cy.get('.ant-tabs-tab').eq(3).click() cy.wait('@statements_plan_detail').then(() => { cy.get('[data-e2e=statement_pages_detail_tabs_txn]').within(() => { cy.get('.ms-List-cell').should('have.length', 10) }) }) }) it('Slow query table rows count', () => { cy.get('.ant-tabs-tab').eq(4).click() cy.wait('@statements_plan_detail').then(() => { cy.get('[data-e2e=detail_tabs_slow_query]').within(() => { cy.get('.ms-List-cell').should('have.length', 0) }) }) }) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/statement/list.compat_spec.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import { skipOn } from '@cypress/skip-test' describe('Read-only user open statement config setting', () => { skipOn(Cypress.env('FEATURE_VERSION') !== '6.0.0', () => { // Create read only user before(() => { const workloads = [ 'DROP USER IF EXISTS "readOnlyUser"@"%"', 'CREATE USER "readOnlyUser"@"%" IDENTIFIED BY "test";', 'GRANT PROCESS, CONFIG ON *.* TO "readOnlyUser"@"%";', 'GRANT SHOW DATABASES ON *.* TO "readOnlyUser"@"%";', 'GRANT DASHBOARD_CLIENT ON *.* TO "readOnlyUser"@"%";' ] workloads.forEach((query) => { cy.task('queryDB', { query }) }) cy.fixture('uri.json').then(function (uri) { this.uri = uri }) }) beforeEach(function () { // login with readOnlyUser cy.visit(this.uri.login) cy.get('[data-e2e=signin_username_input]').clear().type('readOnlyUser') cy.get('[data-e2e="signin_password_input"]').type('test{enter}') cy.visit(this.uri.statement) cy.url().should('include', this.uri.statement) }) it('Unable to modify statement settings', function () { cy.get('[data-e2e=statement_setting]').click({ force: true }) // switch is disabled cy.get('[data-e2e=statemen_enbale_switcher]').should( 'have.class', 'ant-switch-disabled' ) // max size is disabled cy.get('[data-e2e=statement_setting_max_size]').within(() => { cy.get('.ant-slider').should('have.class', 'ant-slider-disabled') }) // refresh interval is disabled cy.get('[data-e2e=statement_setting_refresh_interval]').within(() => { cy.get('.ant-slider').should('have.class', 'ant-slider-disabled') }) // internal query is disabled cy.get('[data-e2e=statement_setting_internal_query]').within(() => { cy.get('.ant-switch').should('have.class', 'ant-switch-disabled') }) // save button is disableds cy.get('[data-e2e=submit_btn]').should('have.attr', 'disabled') }) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/topsql/topsql.spec.ts ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import dayjs from 'dayjs' import { skipOn, onlyOn } from '@cypress/skip-test' function setCustomTimeRange(timeRange) { if (!document.querySelector('[data-e2e="timerange_selector_dropdown"]')) { cy.getByTestId('timerange-selector').click() } cy.getByTestId('timerange_selector_dropdown').should('be.visible') cy.getByTestId('timerange_selector_dropdown') .find('.ant-picker.ant-picker-range') .type(timeRange) cy.getByTestId('timerange_selector_dropdown').should('be.not.visible') } function clearCustomTimeRange() { if (!document.querySelector('[data-e2e="timerange_selector_dropdown"]')) { cy.getByTestId('timerange-selector').click() } cy.getByTestId('timerange_selector_dropdown').should('be.visible') cy.getByTestId('timerange_selector_dropdown') .find('.ant-picker-clear') .click() cy.getByTestId('timerange_selector_dropdown').should('be.not.visible') } function enableTopSQL() { cy.getByTestId('topsql_settings').click() cy.wait('@getTopsqlConfig') cy.getByTestId('topsql_settings_enable').click() cy.getByTestId('topsql_settings_save').click() // confirm the tips which about the data will be delayed cy.get('.ant-modal-body').should('be.visible') cy.get('.ant-modal-body .ant-btn-primary').click() } skipOn(Cypress.env('TIDB_VERSION') !== 'latest', () => { describe('Top SQL page', function () { before(() => { cy.intercept(`${Cypress.env('apiBasePath')}/topsql/config`).as( 'getTopsqlConfig' ) cy.fixture('uri.json').then((uri) => { this.uri = uri cy.login('root') cy.visit(this.uri.topsql) cy.wait('@getTopsqlConfig').then((interception) => { if (!interception.response?.body.enable) { enableTopSQL() } }) cy.visit(this.uri.overview) }) }) beforeEach(() => { cy.login('root') cy.intercept(`${Cypress.env('apiBasePath')}/topsql/config`).as( 'getTopsqlConfig' ) // mock summary and instance data from 2022-01-12 00:00:00 to 2022-01-12 05:00:00 cy.intercept(`${Cypress.env('apiBasePath')}/topsql/summary?*`, { fixture: 'topsql_summary:end=1641934800&instance=127.0.0.1%3A10080&instance_type=tidb&start=1641916800&top=5&window=123s.json' }).as('getTopsqlSummary') cy.intercept( { url: `${Cypress.env('apiBasePath')}/topsql/instances?*` }, { fixture: 'topsql_instance:end=1641934800&start=1641916800.json' } ) // clear the user preference before visit top sql page cy.window().then((win) => win.sessionStorage.clear()) cy.visit(this.uri.topsql) // consume the first screen intercepted request when page loaded cy.wait('@getTopsqlSummary') cy.wait('@getTopsqlConfig') }) describe('Update time range', () => { it('custom the time range, chart displays the data within the time range', () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('topsql_list_chart').matchImageSnapshot() }) it('zoom out the time range, chart displays the data that extends the 50% time range', () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.get('.anticon-zoom-out').click() cy.getByTestId('timerange-selector').should( 'contain', '01-11 21:30:00 ~ 01-12 07:30:00' ) cy.getByTestId('topsql_list_chart').matchImageSnapshot() }) }) describe('Select instance', () => { it('default time range with instance', () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('instance-selector').should( 'contain', 'tidb - 127.0.0.1:10080' ) }) it('change time range, keep the selected instance', () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('instance-selector').should( 'contain', 'tidb - 127.0.0.1:10080' ) // No `tidb - 127.0.0.1:10080` data in the time range clearCustomTimeRange() cy.wait('@getTopsqlSummary') setCustomTimeRange( '1970-01-01 08:00:00{enter}1970-01-01 09:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('instance-selector').should( 'contain', 'tidb - 127.0.0.1:10080' ) }) }) describe('Refresh', () => { it('click refresh button with the recent x time range, fetch the recent x time range data', () => { cy.getByTestId('timerange-selector').click() cy.getByTestId('timerange_selector_dropdown').should('be.visible') const recent = 300 const now = dayjs().unix() cy.clock(now * 1000) cy.getByTestId(`timerange-${recent}`).click({ force: true }) cy.wait('@getTopsqlSummary') .its('request.url') .should('include', `start=${now - recent}`) cy.getByTestId('auto-refresh-button').first().click() cy.wait('@getTopsqlSummary') .its('request.url') .should('include', `start=${now - recent}`) }) it("click refresh button after custom the time range, the data won't change", () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('auto-refresh-button').first().click() cy.getByTestId('timerange-selector').should( 'contain', '01-12 00:00:00 ~ 01-12 05:00:00' ) cy.wait('@getTopsqlSummary') cy.getByTestId('topsql_list_chart').matchImageSnapshot() }) it('set auto refresh, show auto refresh secs aside button', () => { cy.getByTestId('auto-refresh-button').children().eq(1).click() cy.getByTestId('auto_refresh_time_30').click() cy.getByTestId('auto-refresh-button').should('contain', '30 s') }) it('set auto refresh, it will be refreshed automatically after the time', () => { cy.getByTestId('auto-refresh-button').children().eq(1).click() cy.getByTestId('auto_refresh_time_30').should('be.visible') // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000) // wait auto_refresh_time_30 item can be clicked before clock freeze the animate cy.clock() cy.getByTestId('auto_refresh_time_30').click() for (let i = 0; i < 35; i++) { cy.tick(1000) // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(0) // yield to react hooks } cy.clock().invoke('restore') cy.wait('@getTopsqlSummary') .its('response.statusCode') .should('eq', 200) }) }) describe('Chart and table', () => { it('when the time range is large, the chart interval is large', () => { cy.intercept(`${Cypress.env('apiBasePath')}/topsql/summary?*`, { fixture: 'topsql_summary_large_timerange:end=1641916800&instance=127.0.0.1%3A10080&instance_type=tidb&start=1641484800&top=5&window=2929s.json' }).as('getTopsqlSummaryLargeTimerange') setCustomTimeRange( '2022-01-07 00:00:00{enter}2022-01-12 00:00:00{enter}' ) cy.wait('@getTopsqlSummaryLargeTimerange') cy.getByTestId('topsql_list_chart').matchImageSnapshot() }) it('when the time range is small, the chart interval is small', () => { cy.intercept(`${Cypress.env('apiBasePath')}/topsql/summary?*`, { fixture: 'topsql_summary_small_timerange:end=1641920460&instance=127.0.0.1%3A10080&instance_type=tidb&start=1641920400&top=5&window=1s.json' }).as('getTopsqlSummarySmallTimerange') setCustomTimeRange( '2022-01-12 01:00:00{enter}2022-01-12 01:01:00{enter}' ) cy.wait('@getTopsqlSummarySmallTimerange') cy.getByTestId('topsql_list_chart').matchImageSnapshot() }) it('the last item in the table list is others', () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('topsql_list_table') .find('.ms-List-cell') .children() .eq(5) .find('[data-e2e="topsql_listtable_row_others"]') }) it('table has top 5 records and the others record', () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('topsql_list_table') .find('.ms-List-cell') .children() .should('have.length', 6) cy.getByTestId('topsql_list_table') .find('.ms-List-cell') .each((item, index) => { cy.wrap(item).trigger('mouseover') cy.getByTestId('topsql_list_chart').matchImageSnapshot( `Top SQL page -- Chart and table -- table has top 5 records and the others record - ${index}` ) }) }) it('table can only be single selected', () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('topsql_list_table') .find('.ms-List-cell') .each((item) => { cy.wrap(item).click() cy.getByTestId('topsql_list_table') .find('.ms-DetailsRow-check[aria-checked="true"]') .should('have.length', 1) }) }) }) describe('Top SQL settings', () => { it('close Top SQL by settings panel, the chart and table will still work', () => { cy.getByTestId('topsql_settings').click() cy.wait('@getTopsqlConfig') cy.getByTestId('topsql_settings_enable').click() cy.getByTestId('topsql_settings_save').click() cy.get('.ant-btn-primary.ant-btn-dangerous').click() cy.getByTestId('topsql_not_enabled_alert').should('exist') setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') .its('response.statusCode') .should('eq', 200) enableTopSQL() cy.wait('@getTopsqlConfig') cy.getByTestId('topsql_not_enabled_alert').should('not.exist') }) }) describe('SQL statement details', () => { it('click one table row, show the list detail table and information contents', () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('topsql_list_table').find('.ms-List-cell').eq(0).click() cy.getByTestId('topsql_listdetail_table').should('exist') // content cy.getByTestId('sql_text').should('exist') cy.getByTestId('sql_digest').should('exist') cy.getByTestId('plan_text').should('not.exist') cy.getByTestId('plan_digest').should('not.exist') // table columns cy.get('[data-item-key="cpuTime"]').should('exist') cy.get('[data-item-key="plan"]').should('exist') cy.get('[data-item-key="exec_count_per_sec"]').should('exist') cy.get('[data-item-key="latency"]').should('exist') }) it('if the list detail table has more than one plan, only the real plans can be selected', () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('topsql_list_table').find('.ms-List-cell').eq(0).click() cy.getByTestId('topsql_listdetail_table') .find('.ms-List-cell') .eq(0) .click() cy.getByTestId('topsql_listdetail_table') .find('.ms-DetailsRow-check[aria-checked="true"]') .should('have.length', 0) cy.getByTestId('topsql_listdetail_table') .find('.ms-List-cell') .eq(1) .click() cy.getByTestId('topsql_listdetail_table') .find('.ms-DetailsRow-check[aria-checked="true"]') .should('have.length', 0) cy.getByTestId('topsql_listdetail_table') .find('.ms-List-cell') .eq(2) .click() cy.getByTestId('topsql_listdetail_table') .find('.ms-DetailsRow-check[aria-checked="true"]') .should('have.length', 1) cy.getByTestId('sql_text').should('exist') cy.getByTestId('sql_digest').should('exist') cy.getByTestId('plan_text').should('exist') cy.getByTestId('plan_digest').should('exist') }) it("if there's only one plan in the list detail table, show the plan information directly", () => { setCustomTimeRange( '2022-01-12 00:00:00{enter}2022-01-12 05:00:00{enter}' ) cy.wait('@getTopsqlSummary') cy.getByTestId('topsql_list_table').find('.ms-List-cell').eq(1).click() cy.getByTestId('sql_text').should('exist') cy.getByTestId('sql_digest').should('exist') cy.getByTestId('plan_text').should('exist') cy.getByTestId('plan_digest').should('exist') }) }) }) }) onlyOn(Cypress.env('TIDB_VERSION') === '5.0.0', () => { describe('Ngm not supported', function () { before(() => { cy.fixture('uri.json').then((uri) => (this.uri = uri)) }) beforeEach(() => { cy.login('root') }) it('can not see top sql menu', () => { cy.getByTestId('menu_item_topsql').should('not.exist') }) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/topsql/topsql.without_ngm_spec.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import { skipOn } from '@cypress/skip-test' describe('TopSQL without ngm', function () { skipOn(Cypress.env('TIDB_VERSION') === '^5.0', () => { before(() => { cy.fixture('uri.json').then((uri) => (this.uri = uri)) }) beforeEach(() => { cy.login('root') cy.visit(this.uri.topsql) }) describe('Ngm not deployed', () => { it('show global notification about ngm not deployed', () => { cy.get('.ant-notification-notice-message').should( 'contain', 'System Health Check Failed' ) cy.get('[data-e2e="ngm_not_started"]').should('exist') }) }) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/topsql/topsql_security.spec.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. describe('Top SQL security', function () { it("can't access the Top SQL page without login, then redirect to login page", function () { cy.on('uncaught:exception', function () { return false }) cy.fixture('uri.json').then(function (uri) { cy.visit(uri.topsql) cy.url().should('include', uri.login) }) }) }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/integration/utils.js ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. /** * Delete the downloads folder to make sure the test has "clean" * slate before starting. */ export const deleteDownloadsFolder = () => { const downloadsFolder = Cypress.config('downloadsFolder') cy.task('deleteFolder', downloadsFolder) } /** * @param {string[]} list List parsed from CSV file */ export const validateSlowQueryCSVList = (list) => { expect(list).to.have.length(4) // FIXME: this check makes it extremely hard for adding new tests. expect(list[0].query).to.equal('SELECT sleep(1.2);') expect(list[1].query).to.equal('SELECT sleep(1.5);') expect(list[2].query).to.equal('SELECT sleep(2);') expect(list[3].query).to.equal('SELECT sleep(1);') } export const validateStatementCSVList = (allStatementList) => { const defaultExecStmtList = [ 'show databases', 'select distinct `stmt_type` from `information_schema` . `cluster_statements_summary_history` order by `stmt_type` asc', 'select `version` ( )' ] const allStatementDigestText = [] allStatementList.forEach((stmt) => { allStatementDigestText.push(stmt.digest_text) }) expect(allStatementDigestText).to.include.members(defaultExecStmtList) } export const restartTiUP = () => { // Restart tiup cy.exec( `bash ../../../scripts/start_tiup.sh ${Cypress.env( 'TIDB_VERSION' )} false restart`, { log: true } ) // Wait TiUP Playground cy.exec( 'bash ../../../scripts/wait_tiup_playground.sh 1 300 &> wait_tiup.log', { timeout: 300000 } ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/plugins/index.js ================================================ /// // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) import mysql from 'mysql2' import { rmdir } from 'fs' import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin' import codecovTaskPlugin from '@cypress/code-coverage/task' import clipboardy from 'clipboardy' function queryTestDB(query, password, database) { const dbConfig = { host: '127.0.0.1', port: '4000', user: 'root', database: database, password: password } // creates a new mysql connection const connection = mysql.createConnection(dbConfig) // exec query + disconnect to db as a Promise return new Promise((resolve, reject) => { connection.query(query, (error, results) => { setTimeout(() => { if (error) { reject(error) } else { connection.end() return resolve(results) } }, 500) // wait a few more moments for statements and slow query to finish. }) }) } function deleteTestFolder(folderPath) { return new Promise((resolve, reject) => { rmdir(folderPath, { maxRetries: 10, recursive: true }, (err) => { if (err && err.code !== 'ENOENT') { console.error(err) return reject(err) } resolve(null) }) }) } /** * @type {Cypress.PluginConfig} */ // eslint-disable-next-line no-unused-vars module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config codecovTaskPlugin(on, config) addMatchImageSnapshotPlugin(on, config) config.baseUrl = (process.env.SERVER_URL || 'http://localhost:3001/dashboard') + '#' config.env.apiBasePath = '/dashboard/api/' on('task', { // Usage: cy.task('queryDB', { ...queryData }) queryDB: ({ query, password = '', database = 'mysql' }) => { return queryTestDB(query, password, database) }, // Usage: cy.task('deleteFolder', deleteFolderPath) deleteFolder: (folderPath) => { return deleteTestFolder(folderPath) }, // Usage: cy.task('getClipboard') getClipboard: () => { return clipboardy.readSync() } }) return config } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/support/commands.js ================================================ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite // existing commands. // // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** // // // -- This is a parent command -- Cypress.Commands.add('login', (username, password = '') => { // cy.login will be called inside beforeEach, // cy.session stores cookies and localStorage when user first login, // the cookies and localStorage will be reused in the feature beforeEach test. cy.session( [username, password], () => { // root login cy.visit('/') cy.get('[data-e2e=signin_submit]').click() // Wait for the post-login redirect to ensure that the // session actually exists to be cached cy.url().should('include', '/overview') }, { validate() { cy.request('/whoami').its('status').should('eq', 200) } } ) }) // -- This will overwrite an existing command -- Cypress.Commands.overwrite('request', (originalFn, ...options) => { const optionsObject = options[0] const token = localStorage.getItem('dashboard_auth_token') if (!!token && optionsObject === Object(optionsObject)) { optionsObject.headers = { authorization: 'Bearer ' + token, ...optionsObject.headers } return originalFn(optionsObject) } return originalFn(...options) }) // We overwrite the command, so it does not take a sceenshot if we run the tests inside the test runner Cypress.Commands.overwrite( 'matchImageSnapshot', (originalFn, snapshotName, options) => { if (Cypress.env('ALLOW_SCREENSHOT')) { originalFn(snapshotName, options) } else { cy.log(`Screenshot comparison is disabled`) } } ) Cypress.Commands.add('getByTestId', (selector, ...args) => { return cy.get(`[data-e2e="${selector}"]`, ...args) }) Cypress.Commands.add('getByTestIdLike', (selector, ...args) => { return cy.get(`[data-e2e*="${selector}"]`, ...args) }) // // // -- This is a child command -- // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) // // ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/support/index.js ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import '@cypress/code-coverage/support' import '@cypress/skip-test/support' import 'cypress-real-events/support' import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command' addMatchImageSnapshotCommand() require('./commands') // https://github.com/cypress-io/cypress/issues/8418 Cypress.on( 'uncaught:exception', (err) => !err.message.includes('ResizeObserver loop limit exceeded') ) ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/tsconfig.json ================================================ { "extends": "../tsconfig.json", "include": ["./**/*.ts"], "compilerOptions": { "types": ["cypress"], "lib": ["es2015", "dom"], "isolatedModules": false, "allowJs": true, "noEmit": true } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/types/global.d.ts ================================================ /// declare namespace Cypress { interface Chainable { login(username: string, password?: string): void getByTestId(dataTestAttribute: string, args?: any): Chainable getByTestIdLike( dataTestPrefixAttribute: string, args?: any ): Chainable matchImageSnapshot(nameOrOptions?: string | Options): void matchImageSnapshot(name: string, options: Options): void } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress/types/mocha.d.ts ================================================ /// import 'mocha' declare module 'mocha' { interface Suite { uri: any } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/cypress.json ================================================ { "defaultCommandTimeout": 10000, "responseTimeout": 60000, "requestTimeout": 60000, "screenshotOnRunFailure": true, "video": true, "env": { "FEATURE_VERSION": "6.0.0", "TIDB_VERSION": "latest", "ALLOW_SCREENSHOT": false }, "experimentalSessionSupport": true, "ignoreTestFiles": ["**/__snapshots__/*", "**/__image_snapshots__/*"] } ================================================ FILE: ui/packages/tidb-dashboard-for-op/gulpfile.js ================================================ const { task, series, parallel } = require('gulp') const shell = require('gulp-shell') task('distro:gen', shell.task('../../../scripts/distro/write_strings.sh')) task( 'speedscope:copy', shell.task( 'mkdir -p public/speedscope && cp node_modules/@duorou_xu/speedscope/dist/release/* public/speedscope/' ) ) task('tsc:watch', shell.task('tsc --watch')) task('tsc:check', shell.task('tsc')) // https://www.npmjs.com/package/eslint-watch task('lint:watch', shell.task('esw --watch --cache --ext .tsx,.ts .')) task('lint:check', shell.task('esw --cache --ext tsx,ts .')) task('esbuild:dev', shell.task('NODE_ENV=development node builder.js')) task('esbuild:build', shell.task('NODE_ENV=production node builder.js')) task( 'dev', series( parallel('distro:gen', 'speedscope:copy'), parallel('tsc:watch', 'lint:watch', 'esbuild:dev') ) ) task( 'build', series( parallel('distro:gen', 'speedscope:copy'), parallel('tsc:check', 'lint:check', 'esbuild:build') ) ) ================================================ FILE: ui/packages/tidb-dashboard-for-op/package.json ================================================ { "name": "@pingcap/tidb-dashboard-for-op", "private": "true", "version": "1.0.0", "scripts": { "dev": "gulp dev", "build": "gulp build", "run:e2e-test:compat-features": "TZ=Asia/Shanghai cypress run --spec cypress/integration/**/*.compat_spec.[jt]s", "run:e2e-test:common-features": "TZ=Asia/Shanghai cypress run --spec cypress/integration/**/*.spec.[jt]s", "run:e2e-test:without-ngm": "TZ=Asia/Shanghai cypress run --spec cypress/integration/**/*.without_ngm_spec.[jt]s", "run:e2e-test:specify": "TZ=Asia/Shanghai cypress run", "open:cypress": "TZ=Asia/Shanghai cypress open" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@ant-design/icons": "^4.7.0", "@duorou_xu/speedscope": "1.14.1", "@fortawesome/fontawesome-free": "^6.1.1", "@g07cha/flexbox-react": "^5.0.0", "@pingcap/tidb-dashboard-client": "workspace:^1.0.0", "@pingcap/tidb-dashboard-lib": "workspace:^1.0.0", "ahooks": "^3.1.9", "antd": "^4.18.7", "axios": "^1.12.0", "bulma": "^0.9.4", "classnames": "^2.3.1", "compare-versions": "^5.0.1", "eventemitter2": "^6.4.5", "i18next": "^23.7.11", "jsencrypt": "^3.3.2", "nprogress": "^0.2.0", "rc-animate": "^3.1.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-i18next": "^11.15.4", "react-markdown": "^8.0.3", "react-router-dom": "6", "react-spring": "^8.0.27", "react-use": "^15.3.3", "single-spa": "^5.9.4", "single-spa-react": "^4.6.1" }, "devDependencies": { "@baurine/esbuild-plugin-babel": "^0.3.0", "@baurine/esbuild-plugin-postcss3": "^0.4.3", "@cypress/code-coverage": "^3.9.12", "@cypress/skip-test": "^2.6.1", "@types/node": "^16.9.1", "@types/react": "^17.0.20", "@types/react-dom": "^17.0.9", "autoprefixer": "^10.4.2", "babel-plugin-istanbul": "^6.1.1", "chalk": "4.1.2", "chokidar": "^3.5.2", "clipboardy": "2.3.0", "cypress": "8.5.0", "cypress-image-snapshot": "^4.0.1", "cypress-real-events": "^1.7.0", "dayjs": "^1.10.8", "dotenv": "^16.0.1", "esbuild": "^0.14.23", "esbuild-plugin-svgr": "^1.0.0", "esbuild-plugin-yaml": "^0.0.1", "eslint-plugin-cypress": "^2.12.1", "eslint-watch": "^8.0.0", "fs-extra": "^10.0.0", "gulp": "^4.0.2", "gulp-shell": "^0.8.0", "http-proxy-middleware": "^2.0.6", "live-server": "^1.2.1", "mysql2": "^3.15.0", "neat-csv": "5.1.0", "typescript": "^4.7.3" } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/process-shim.js ================================================ export let process = { // cwd: () => '', env: {} // to avoid `process.env` undefined in runtime } ================================================ FILE: ui/packages/tidb-dashboard-for-op/public/.gitignore ================================================ /speedscope ================================================ FILE: ui/packages/tidb-dashboard-for-op/public/diagnoseReport.html ================================================
================================================ FILE: ui/packages/tidb-dashboard-for-op/public/index.html ================================================
================================================ FILE: ui/packages/tidb-dashboard-for-op/public/test-portal/index.html ================================================ TiDB Dashboard iframe Test

iframe test

================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/ClusterInfo/context.ts ================================================ import { IClusterInfoDataSource, IClusterInfoContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' class DataSource implements IClusterInfoDataSource { clusterInfoGetHostsInfo(options?: ReqConfig) { return client.getInstance().clusterInfoGetHostsInfo(options) } getStoreLocationTopology(options?: ReqConfig) { return client.getInstance().getStoreLocationTopology(options) } getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } getTiCDCTopology(options?: ReqConfig) { return client.getInstance().getTiCDCTopology(options) } getTiProxyTopology(options?: ReqConfig) { return client.getInstance().getTiProxyTopology(options) } getTSOTopology(options?: ReqConfig) { return client.getInstance().getTSOTopology(options) } getSchedulingTopology(options?: ReqConfig) { return client.getInstance().getSchedulingTopology(options) } topologyTidbAddressDelete(address: string, options?: ReqConfig) { return client.getInstance().topologyTidbAddressDelete({ address }, options) } clusterInfoGetStatistics(options?: ReqConfig) { return client.getInstance().clusterInfoGetStatistics(options) } } const ds = new DataSource() export const ctx: IClusterInfoContext = { ds, cfg: { apiPathBase: client.getBasePath() } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/ClusterInfo/index.tsx ================================================ import React from 'react' import { ClusterInfoApp, ClusterInfoProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/ClusterInfo/meta.ts ================================================ import { ClusterOutlined } from '@ant-design/icons' export default { id: 'cluster_info', routerPrefix: '/cluster_info', icon: ClusterOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Configuration/context.ts ================================================ import { IConfigurationDataSource, IConfigurationContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { ConfigurationEditRequest } from '~/client' class DataSource implements IConfigurationDataSource { configurationEdit(request: ConfigurationEditRequest, options?: ReqConfig) { return client.getInstance().configurationEdit({ request }, options) } configurationGetAll(options?: ReqConfig) { return client.getInstance().configurationGetAll(options) } } const ds = new DataSource() export const ctx: IConfigurationContext = { ds } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Configuration/index.tsx ================================================ import React from 'react' import { ConfigurationApp, ConfigurationProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Configuration/meta.ts ================================================ import { ToolOutlined } from '@ant-design/icons' export default { id: 'configuration', routerPrefix: '/configuration', icon: ToolOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/ContinuousProfiling/context.ts ================================================ import { IConProfilingDataSource, IConProfilingContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { ConprofNgMonitoringConfig } from '~/client' import publicPathBase from '~/utils/publicPathPrefix' class DataSource implements IConProfilingDataSource { continuousProfilingActionTokenGet(q: string, options?: ReqConfig) { return client .getInstance() .continuousProfilingActionTokenGet({ q }, options) } continuousProfilingComponentsGet(options?: ReqConfig) { return client.getInstance().continuousProfilingComponentsGet(options) } continuousProfilingConfigGet(options?: ReqConfig) { return client.getInstance().continuousProfilingConfigGet(options) } continuousProfilingConfigPost( request: ConprofNgMonitoringConfig, options?: ReqConfig ) { return client .getInstance() .continuousProfilingConfigPost({ request }, options) } continuousProfilingDownloadGet(ts: number, options?: ReqConfig) { return client.getInstance().continuousProfilingDownloadGet({ ts }, options) } continuousProfilingEstimateSizeGet(options?: ReqConfig) { return client.getInstance().continuousProfilingEstimateSizeGet(options) } continuousProfilingGroupProfileDetailGet(ts: number, options?: ReqConfig) { return client .getInstance() .continuousProfilingGroupProfileDetailGet({ ts }, options) } continuousProfilingGroupProfilesGet( beginTime?: number, endTime?: number, options?: ReqConfig ) { return client .getInstance() .continuousProfilingGroupProfilesGet({ beginTime, endTime }, options) } continuousProfilingSingleProfileViewGet( address?: string, component?: string, profileType?: string, ts?: number, options?: ReqConfig ) { return client .getInstance() .continuousProfilingSingleProfileViewGet( { address, component, profileType, ts }, options ) } getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } } const ds = new DataSource() export const ctx: IConProfilingContext = { ds, cfg: { apiPathBase: client.getBasePath(), publicPathBase } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/ContinuousProfiling/index.tsx ================================================ import React from 'react' import { ConProfilingApp, ConProfilingProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/ContinuousProfiling/meta.ts ================================================ import { AimOutlined } from '@ant-design/icons' export default { id: 'conprof', routerPrefix: '/continuous_profiling', icon: AimOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Deadlock/context.ts ================================================ import { IDeadlockDataSource, IDeadlockContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' class DataSource implements IDeadlockDataSource { deadlockListGet(options?: ReqConfig) { return client.getInstance().deadlockListGet(options) } } const ds = new DataSource() export const ctx: IDeadlockContext = { ds } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Deadlock/index.tsx ================================================ import React from 'react' import { DeadlockApp, DeadlockProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Deadlock/meta.ts ================================================ import { SyncOutlined } from '@ant-design/icons' export default { id: 'deadlock', routerPrefix: '/deadlock', icon: SyncOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/DebugAPI/context.ts ================================================ import { IDebugAPIDataSource, IDebugAPIContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { EndpointRequestPayload } from '~/client' class DataSource implements IDebugAPIDataSource { debugAPIGetEndpoints(options?: ReqConfig) { return client.getInstance().debugAPIGetEndpoints(options) } debugAPIRequestEndpoint(req: EndpointRequestPayload, options?: ReqConfig) { return client.getInstance().debugAPIRequestEndpoint( { req: { ...req, // To compatible with the old tidb-dashboard backend api before 5.4.0 // By PR https://github.com/pingcap/tidb-dashboard/pull/1103 (release to v2021.12.30.1 and PD 5.4.0) // It changes `id` to `api_id`, `params` to `param_values` id: req.api_id, params: req.param_values } as any }, options ) } infoListDatabases(options?: ReqConfig) { return client.getInstance().infoListDatabases(options) } infoListTables(databaseName?: string, options?: ReqConfig) { return client.getInstance().infoListTables({ databaseName }, options) } getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } getTiProxyTopology(options?: ReqConfig) { return client.getInstance().getTiProxyTopology(options) } } const ds = new DataSource() export const ctx: IDebugAPIContext = { ds, cfg: { apiPathBase: client.getBasePath() } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/DebugAPI/index.tsx ================================================ import React from 'react' import { DebugAPIApp, DebugAPIProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/DebugAPI/meta.ts ================================================ import { ApiOutlined } from '@ant-design/icons' export default { id: 'debug_api', routerPrefix: '/debug_api', icon: ApiOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Diagnose/context.ts ================================================ import { IDiagnoseDataSource, IDiagnoseContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { DiagnoseGenDiagnosisReportRequest } from '~/client' class DataSource implements IDiagnoseDataSource { diagnoseDiagnosisPost( request: DiagnoseGenDiagnosisReportRequest, options?: ReqConfig ) { return client.getInstance().diagnoseDiagnosisPost({ request }, options) } } const ds = new DataSource() export const ctx: IDiagnoseContext = { ds } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Diagnose/index.tsx ================================================ import React from 'react' import { DiagnoseApp, DiagnoseProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Diagnose/meta.ts ================================================ import { SafetyCertificateOutlined } from '@ant-design/icons' export default { id: 'diagnose', routerPrefix: '/diagnose', icon: SafetyCertificateOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/InstanceProfiling/context.ts ================================================ import { IInstanceProfilingDataSource, IInstanceProfilingContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { ProfilingStartRequest } from '~/client' import publicPathBase from '~/utils/publicPathPrefix' class DataSource implements IInstanceProfilingDataSource { getActionToken(id?: string, action?: string, options?: ReqConfig) { return client.getInstance().getActionToken({ id, action }, options) } getProfilingGroupDetail(groupId: string, options?: ReqConfig) { return client.getInstance().getProfilingGroupDetail({ groupId }, options) } getProfilingGroups(options?: ReqConfig) { return client.getInstance().getProfilingGroups(options) } startProfiling(req: ProfilingStartRequest, options?: ReqConfig) { return client.getInstance().startProfiling({ req }, options) } continuousProfilingConfigGet(options?: ReqConfig) { return client.getInstance().continuousProfilingConfigGet(options) } getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } getTiCDCTopology(options?: ReqConfig) { return client.getInstance().getTiCDCTopology(options) } getTiProxyTopology(options?: ReqConfig) { return client.getInstance().getTiProxyTopology(options) } getTSOTopology(options?: ReqConfig) { return client.getInstance().getTSOTopology(options) } getSchedulingTopology(options?: ReqConfig) { return client.getInstance().getSchedulingTopology(options) } } const ds = new DataSource() export const ctx: IInstanceProfilingContext = { ds, cfg: { apiPathBase: client.getBasePath(), publicPathBase } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/InstanceProfiling/index.tsx ================================================ import React from 'react' import { InstanceProfilingApp, InstanceProfilingProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/InstanceProfiling/meta.ts ================================================ import { AimOutlined } from '@ant-design/icons' export default { id: 'instance_profiling', routerPrefix: '/instance_profiling', icon: AimOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/KeyViz/context.ts ================================================ import { IKeyVizDataSource, IKeyVizContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { ConfigKeyVisualConfig } from '~/client' class DataSource implements IKeyVizDataSource { keyvisualConfigGet(options?: ReqConfig) { return client.getInstance().keyvisualConfigGet(options) } keyvisualConfigPut(request: ConfigKeyVisualConfig, options?: ReqConfig) { return client.getInstance().keyvisualConfigPut({ request }, options) } keyvisualHeatmapsGet( startkey?: string, endkey?: string, starttime?: number, endtime?: number, type?: | 'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration', options?: ReqConfig ) { return client.getInstance().keyvisualHeatmapsGet( { startkey, endkey, starttime, type }, options ) } } const ds = new DataSource() export const ctx: IKeyVizContext = { ds } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/KeyViz/index.tsx ================================================ import React from 'react' import { KeyVizApp, KeyVizProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/KeyViz/meta.ts ================================================ import { EyeOutlined } from '@ant-design/icons' export default { id: 'keyviz', routerPrefix: '/keyviz', icon: EyeOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Monitoring/context.ts ================================================ import { IMonitoringDataSource, IMonitoringContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' import { getMonitoringItems } from './metricsQueries' class DataSource implements IMonitoringDataSource { metricsQueryGet(params: { endTimeSec?: number query?: string startTimeSec?: number stepSec?: number }) { return client .getInstance() .metricsQueryGet(params, { handleError: 'custom' } as ReqConfig) .then((res) => res.data) } } const ds = new DataSource() export const ctx: IMonitoringContext = { ds, cfg: { getMetricsQueries: (pdVersion: string | undefined) => getMonitoringItems(pdVersion), promAddrConfigurable: true, metricsReferenceLink: 'https://docs.pingcap.com/tidb/stable/dashboard-monitoring' } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Monitoring/index.tsx ================================================ import React from 'react' import { MonitoringApp, MonitoringProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Monitoring/meta.ts ================================================ import { LineChartOutlined } from '@ant-design/icons' export default { id: 'monitoring', routerPrefix: '/monitoring', icon: LineChartOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Monitoring/metricsQueries.ts ================================================ import { ColorType, TransformNullValue, MetricsQueryType } from '@pingcap/tidb-dashboard-lib' import { compare } from 'compare-versions' function transformColorBySQLType(legendLabel: string) { switch (legendLabel) { case 'Select': return ColorType.BLUE_3 case 'Commit': return ColorType.GREEN_2 case 'Insert': return ColorType.GREEN_3 case 'Update': return ColorType.GREEN_4 case 'general': return ColorType.PINK default: return undefined } } function transformColorByExecTimeOverview(legendLabel: string) { switch (legendLabel) { case 'tso_wait': return ColorType.RED_5 case 'Commit': return ColorType.GREEN_4 case 'Prewrite': return ColorType.GREEN_3 case 'PessimisticLock': return ColorType.RED_4 case 'Get': return ColorType.BLUE_3 case 'BatchGet': return ColorType.BLUE_4 case 'Cop': return ColorType.BLUE_1 case 'ScanLock': case 'Scan': return ColorType.PURPLE case 'execute time': return ColorType.YELLOW default: return undefined } } const getMonitoringItems = ( pdVersion: string | undefined ): MetricsQueryType[] => { function loadTiKVStoragePromql() { const PDVersion = pdVersion?.replace('v', '') if (PDVersion && PDVersion !== 'N/A' && compare(PDVersion, '5.4.1', '<')) { return 'sum(tikv_engine_size_bytes) by (instance)' } return 'sum(tikv_store_size_bytes{type="used"}) by (instance)' } const monitoringItems: MetricsQueryType[] = [ { category: 'database_time', metrics: [ { title: 'Database Time by SQL Types', queries: [ { promql: `sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval]))`, name: 'database time', color: ColorType.YELLOW, type: 'line' }, { promql: `sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval])) by (sql_type)`, name: '{sql_type}', color: (seriesName: string) => transformColorBySQLType(seriesName), type: 'bar_stacked' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Database Time by SQL Phase', queries: [ { promql: `sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval]))`, name: 'database time', color: ColorType.YELLOW, type: 'line' }, { promql: `sum(rate(tidb_session_parse_duration_seconds_sum{sql_type="general"}[$__rate_interval]))`, name: 'parse', color: ColorType.RED_2, type: 'bar_stacked' }, { promql: `sum(rate(tidb_session_compile_duration_seconds_sum{sql_type="general"}[$__rate_interval]))`, name: 'compile', color: ColorType.ORANGE, type: 'bar_stacked' }, { promql: `sum(rate(tidb_session_execute_duration_seconds_sum{sql_type="general"}[$__rate_interval]))`, name: 'execute', color: ColorType.GREEN_3, type: 'bar_stacked' }, { promql: `sum(rate(tidb_server_get_token_duration_seconds_sum[$__rate_interval]))/1000000`, name: 'get token', color: ColorType.RED_3, type: 'bar_stacked' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'SQL Execute Time Overview', queries: [ { promql: 'sum(rate(tidb_tikvclient_request_seconds_sum{store!="0"}[$__rate_interval])) by (type)', name: '{type}', color: (seriesName: string) => transformColorByExecTimeOverview(seriesName), type: 'bar_stacked' }, { promql: 'sum(rate(pd_client_cmd_handle_cmds_duration_seconds_sum{type="wait"}[$__rate_interval]))', name: 'tso_wait', color: ColorType.RED_5, type: 'bar_stacked' } ], unit: 's' } ] }, { category: 'application_connection', metrics: [ { title: 'Connection Count', queries: [ { promql: 'sum(tidb_server_connections)', name: 'Total', type: 'line' }, { promql: 'sum(tidb_server_tokens)', name: 'active connections', type: 'line' } ], unit: 'short', nullValue: TransformNullValue.AS_ZERO }, { title: 'Disconnection', queries: [ { promql: 'sum(rate(tidb_server_disconnection_total[$__rate_interval])) by (instance, result)', name: '{instance}-{result}', type: 'area_stack' } ], unit: 'short', nullValue: TransformNullValue.AS_ZERO } ] }, { category: 'sql_count', metrics: [ { title: 'Query Per Second', queries: [ { promql: 'sum(rate(tidb_executor_statement_total[$__rate_interval]))', name: 'Total', type: 'line' }, { promql: 'sum(rate(tidb_executor_statement_total[$__rate_interval])) by (type)', name: '{type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'short' }, { title: 'Failed Queries', queries: [ { promql: 'increase(tidb_server_execute_error_total[$__rate_interval])', name: '{type} @ {instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'short' }, { title: 'Command Per Second', queries: [ { promql: 'sum(rate(tidb_server_query_total[$__rate_interval])) by (type)', name: '{type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'short' }, { title: 'Queries Using Plan Cache OPS', queries: [ { promql: 'sum(rate(tidb_server_plan_cache_total[$__rate_interval])) by (type)', name: 'avg - hit', type: 'line' }, { promql: 'sum(rate(tidb_server_plan_cache_miss_total[$__rate_interval]))', name: 'avg - miss', type: 'line' } ], unit: 'short', nullValue: TransformNullValue.AS_ZERO } ] }, { category: 'latency_break_down', metrics: [ { title: 'Query Duration', queries: [ { promql: 'sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval])) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!="internal"}[$__rate_interval]))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{sql_type!="internal"}[$__rate_interval])) by (le))', name: '99', type: 'line' }, { promql: 'sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval])) by (sql_type) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!="internal"}[$__rate_interval])) by (sql_type)', name: 'avg-{sql_type}', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{sql_type!="internal"}[$__rate_interval])) by (le,sql_type))', name: '99-{sql_type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average Idle Connection Duration', queries: [ { promql: `(sum(rate(tidb_server_conn_idle_duration_seconds_sum{in_txn='1'}[$__rate_interval])) / sum(rate(tidb_server_conn_idle_duration_seconds_count{in_txn='1'}[$__rate_interval])))`, name: 'avg-in-txn', type: 'line' }, { promql: `(sum(rate(tidb_server_conn_idle_duration_seconds_sum{in_txn='0'}[$__rate_interval])) / sum(rate(tidb_server_conn_idle_duration_seconds_count{in_txn='0'}[$__rate_interval])))`, name: 'avg-not-in-txn', type: 'line' } ], unit: 's', nullValue: TransformNullValue.AS_ZERO }, { title: 'Get Token Duration', queries: [ { promql: 'sum(rate(tidb_server_get_token_duration_seconds_sum[$__rate_interval])) / sum(rate(tidb_server_get_token_duration_seconds_count[$__rate_interval]))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_server_get_token_duration_seconds_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'µs' }, { title: 'Parse Duration', queries: [ { promql: '(sum(rate(tidb_session_parse_duration_seconds_sum{sql_type="general"}[$__rate_interval])) / sum(rate(tidb_session_parse_duration_seconds_count{sql_type="general"}[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_session_parse_duration_seconds_bucket{sql_type="general"}[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Compile Duration', queries: [ { promql: '(sum(rate(tidb_session_compile_duration_seconds_sum{sql_type="general"}[$__rate_interval])) / sum(rate(tidb_session_compile_duration_seconds_count{sql_type="general"}[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_session_compile_duration_seconds_bucket{sql_type="general"}[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Execute Duration', queries: [ { promql: '(sum(rate(tidb_session_execute_duration_seconds_sum{sql_type="general"}[$__rate_interval])) / sum(rate(tidb_session_execute_duration_seconds_count{sql_type="general"}[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_session_execute_duration_seconds_bucket{sql_type="general"}[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' } ] }, { category: 'transaction', metrics: [ { title: 'Transaction Per Second', queries: [ { promql: 'sum(rate(tidb_session_transaction_duration_seconds_count{scope=~"general"}[$__rate_interval])) by (type, txn_mode)', name: '{type}-{txn_mode}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'short' }, { title: 'Transaction Duration', queries: [ { promql: 'sum(rate(tidb_session_transaction_duration_seconds_sum{scope=~"general"}[$__rate_interval])) by (txn_mode)/ sum(rate(tidb_session_transaction_duration_seconds_count{scope=~"general"}[$__rate_interval])) by (txn_mode)', name: 'avg-{txn_mode}', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_session_transaction_duration_seconds_bucket[$__rate_interval])) by (le, txn_mode))', name: '99-{txn_mode}', type: 'line' } ], unit: 's' } ] }, { category: 'core_path_duration', metrics: [ { title: 'Avg TiDB KV Request Duration', queries: [ { promql: 'sum(rate(tidb_tikvclient_request_seconds_sum{store!="0"}[$__rate_interval])) by (type)/ sum(rate(tidb_tikvclient_request_seconds_count{store!="0"}[$__rate_interval])) by (type)', name: '{type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Avg TiKV GRPC Duration', queries: [ { promql: 'sum(rate(tikv_grpc_msg_duration_seconds_sum{store!="0"}[$__rate_interval])) by (type)/ sum(rate(tikv_grpc_msg_duration_seconds_count{store!="0"}[$__rate_interval])) by (type)', name: '{type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 PD TSO Wait/RPC Duration', queries: [ { promql: '(sum(rate(pd_client_cmd_handle_cmds_duration_seconds_sum{type="wait"}[$__rate_interval])) / sum(rate(pd_client_cmd_handle_cmds_duration_seconds_count{type="wait"}[$__rate_interval])))', name: 'wait - avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(pd_client_cmd_handle_cmds_duration_seconds_bucket{type="wait"}[$__rate_interval])) by (le))', name: 'wait - 99', type: 'line' }, { promql: '(sum(rate(pd_client_request_handle_requests_duration_seconds_sum{type="tso"}[$__rate_interval])) / sum(rate(pd_client_request_handle_requests_duration_seconds_count{type="tso"}[$__rate_interval])))', name: 'rpc - avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(pd_client_request_handle_requests_duration_seconds_bucket{type="tso"}[$__rate_interval])) by (le))', name: 'rpc - 99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Storage Async Write Duration', queries: [ { promql: 'sum(rate(tikv_storage_engine_async_request_duration_seconds_sum{type="write"}[$__rate_interval])) / sum(rate(tikv_storage_engine_async_request_duration_seconds_count{type="write"}[$__rate_interval]))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_storage_engine_async_request_duration_seconds_bucket{type="write"}[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Store Duration', queries: [ { promql: 'sum(rate(tikv_raftstore_store_duration_secs_sum[$__rate_interval])) / sum(rate(tikv_raftstore_store_duration_secs_count[$__rate_interval]))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_raftstore_store_duration_secs_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Apply Duration', queries: [ { promql: '(sum(rate(tikv_raftstore_apply_duration_secs_sum[$__rate_interval])) / sum(rate(tikv_raftstore_apply_duration_secs_count[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_raftstore_apply_duration_secs_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Append Log Duration', queries: [ { promql: '(sum(rate(tikv_raftstore_append_log_duration_seconds_sum[$__rate_interval])) / sum(rate(tikv_raftstore_append_log_duration_seconds_count[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_raftstore_append_log_duration_seconds_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Commit Log Duration', queries: [ { promql: '(sum(rate(tikv_raftstore_commit_log_duration_seconds_sum[$__rate_interval])) / sum(rate(tikv_raftstore_commit_log_duration_seconds_count[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_raftstore_commit_log_duration_seconds_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'Average / P99 Apply Log Duration', queries: [ { promql: '(sum(rate(tikv_raftstore_apply_log_duration_seconds_sum[$__rate_interval])) / sum(rate(tikv_raftstore_apply_log_duration_seconds_count[$__rate_interval])))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tikv_raftstore_apply_log_duration_seconds_bucket[$__rate_interval])) by (le))', name: '99', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' } ] }, { category: 'server', metrics: [ { title: 'TiDB Uptime', queries: [ { promql: '(time() - process_start_time_seconds{job="tidb"})', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'TiDB CPU Usage', queries: [ { promql: 'rate(process_cpu_seconds_total{job="tidb"}[30s])', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'percentunit' }, { title: 'TiDB Memory Usage', queries: [ { promql: 'process_resident_memory_bytes{job="tidb"}', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'bytes' }, { title: 'TiKV Uptime', queries: [ { promql: '(time() - process_start_time_seconds{job="tikv"})', name: '{instance}', type: 'line' } ], unit: 's' }, { title: 'TiKV CPU Usage', queries: [ { promql: 'sum(rate(tikv_thread_cpu_seconds_total[$__rate_interval])) by (instance)', name: '{instance}', type: 'line' } ], unit: 'percentunit' }, { title: 'TiKV Memory Usage', queries: [ { promql: 'process_resident_memory_bytes{job=~".*tikv"}', name: '{instance}', type: 'line' } ], unit: 'bytes' }, { title: 'TiKV IO MBps', queries: [ { promql: 'sum(rate(tikv_engine_flow_bytes{db="kv", type="wal_file_bytes"}[$__rate_interval])) by (instance) + sum(rate(tikv_engine_flow_bytes{db="raft", type="wal_file_bytes"}[$__rate_interval])) by (instance) + sum(rate(raft_engine_write_size_sum[$__rate_interval])) by (instance)', name: '{instance}-write', type: 'line' }, { promql: 'sum(rate(tikv_engine_flow_bytes{db="kv", type=~"bytes_read|iter_bytes_read"}[$__rate_interval])) by (instance)', name: '{instance}-read', type: 'line' } ], unit: 'Bps' }, { title: 'TiKV Storage Usage', queries: [ { promql: loadTiKVStoragePromql(), name: '{instance}', type: 'area_stack' } ], unit: 'decbytes' }, { title: 'TiFlash Uptime', queries: [ { promql: 'tiflash_system_asynchronous_metric_Uptime', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'TiFlash CPU Usage', queries: [ { promql: 'rate(tiflash_proxy_process_cpu_seconds_total{job="tiflash"}[$__rate_interval])', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'percentunit' }, { title: 'TiFlash Memory', queries: [ { promql: 'tiflash_proxy_process_resident_memory_bytes{job="tiflash"}', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'bytes' }, { title: 'TiFlash IO MBps', queries: [ { promql: 'sum(rate(tiflash_system_profile_event_WriteBufferFromFileDescriptorWriteBytes[$__rate_interval])) by (instance) + sum(rate(tiflash_system_profile_event_PSMWriteBytes[$__rate_interval])) by (instance) + sum(rate(tiflash_system_profile_event_WriteBufferAIOWriteBytes[$__rate_interval])) by (instance)', name: '{instance}-write', type: 'line' }, { promql: 'sum(rate(tiflash_system_profile_event_ReadBufferFromFileDescriptorReadBytes[$__rate_interval])) by (instance) + sum(rate(tiflash_system_profile_event_PSMReadBytes[$__rate_interval])) by (instance) + sum(rate(tiflash_system_profile_event_ReadBufferAIOReadBytes[$__rate_interval])) by (instance)', name: '{instance}-read', type: 'line' } ], unit: 'Bps' }, { title: 'TiFlash Storage Usage', queries: [ { promql: 'sum(tiflash_system_current_metric_StoreSizeUsed) by (instance)', name: '{instance}', type: 'area_stack' } ], unit: 'bytes' }, { title: 'TiProxy Uptime', queries: [ { promql: '(time() - process_start_time_seconds{job="tiproxy"})', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'TiProxy CPU Usage', queries: [ { promql: 'rate(process_cpu_seconds_total{job="tiproxy"}[30s])', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'percentunit' }, { title: 'TiProxy Memory Usage', queries: [ { promql: 'process_resident_memory_bytes{job="tiproxy"}', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'bytes' } ] } ] return monitoringItems } export { getMonitoringItems } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/OptimizerTrace/context.ts ================================================ import { IOptimizerTraceDataSource, IOptimizerTraceContext // ReqConfig } from '@pingcap/tidb-dashboard-lib' // import client from '~/client' class DataSource implements IOptimizerTraceDataSource {} export const ctx: IOptimizerTraceContext = { ds: new DataSource() } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/OptimizerTrace/index.tsx ================================================ import React from 'react' import { OptimizerTraceApp, OptimizerTraceProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/OptimizerTrace/meta.ts ================================================ export default { id: 'optimizer_trace', routerPrefix: '/optimizer_trace', reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Overview/context.ts ================================================ import { IOverviewDataSource, IOverviewContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' import { overviewMetrics } from './metricsQueries' class DataSource implements IOverviewDataSource { getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } getTiCDCTopology(options?: ReqConfig) { return client.getInstance().getTiCDCTopology(options) } getTiProxyTopology(options?: ReqConfig) { return client.getInstance().getTiProxyTopology(options) } getTSOTopology(options?: ReqConfig) { return client.getInstance().getTSOTopology(options) } getSchedulingTopology(options?: ReqConfig) { return client.getInstance().getSchedulingTopology(options) } metricsQueryGet(params: { endTimeSec?: number query?: string startTimeSec?: number stepSec?: number }) { return client .getInstance() .metricsQueryGet(params, { handleError: 'custom' } as ReqConfig) .then((res) => res.data) } getGrafanaTopology(options?: ReqConfig) { return client.getInstance().getGrafanaTopology(options) } getAlertManagerTopology(options?: ReqConfig) { return client.getInstance().getAlertManagerTopology(options) } getAlertManagerCounts(address: string, options?: ReqConfig) { return client.getInstance().getAlertManagerCounts({ address }, options) } } const ds = new DataSource() export const ctx: IOverviewContext = { ds, cfg: { apiPathBase: client.getBasePath(), metricsQueries: overviewMetrics, promAddrConfigurable: true, showMetrics: true, metricsReferenceLink: 'https://docs.pingcap.com/tidb/stable/dashboard-monitoring' } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Overview/index.tsx ================================================ import React from 'react' import { OverviewApp, OverviewProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Overview/meta.ts ================================================ import { AppstoreOutlined } from '@ant-design/icons' export default { id: 'overview', routerPrefix: '/overview', icon: AppstoreOutlined, isDefaultRouter: true, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Overview/metricsQueries.ts ================================================ import { TransformNullValue, OverviewMetricsQueryType } from '@pingcap/tidb-dashboard-lib' const overviewMetrics: OverviewMetricsQueryType[] = [ { title: 'total_requests', queries: [ { promql: 'sum(rate(tidb_executor_statement_total[$__rate_interval]))', name: 'Total', type: 'line' }, { promql: 'sum(rate(tidb_executor_statement_total[$__rate_interval])) by (type)', name: '{type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'short' }, { title: 'latency', queries: [ { promql: 'sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval])) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!="internal"}[$__rate_interval]))', name: 'avg', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{sql_type!="internal"}[$__rate_interval])) by (le))', name: '99', type: 'line' }, { promql: 'sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!="internal"}[$__rate_interval])) by (sql_type) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!="internal"}[$__rate_interval])) by (sql_type)', name: 'avg-{sql_type}', type: 'line' }, { promql: 'histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{sql_type!="internal"}[$__rate_interval])) by (le,sql_type))', name: '99-{sql_type}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 's' }, { title: 'cpu', queries: [ { promql: 'rate(process_cpu_seconds_total{job="tidb"}[$__rate_interval])', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'percentunit' }, { title: 'memory', queries: [ { promql: 'process_resident_memory_bytes{job="tidb"}', name: '{instance}', type: 'line' } ], nullValue: TransformNullValue.AS_ZERO, unit: 'bytes' }, { title: 'io', queries: [ { promql: 'sum(rate(tikv_engine_flow_bytes{db="kv", type="wal_file_bytes"}[$__rate_interval])) by (instance) + sum(rate(tikv_engine_flow_bytes{db="raft", type="wal_file_bytes"}[$__rate_interval])) by (instance) + sum(rate(raft_engine_write_size_sum[$__rate_interval])) by (instance)', name: '{instance}-write', type: 'line' }, { promql: 'sum(rate(tikv_engine_flow_bytes{db="kv", type=~"bytes_read|iter_bytes_read"}[$__rate_interval])) by (instance)', name: '{instance}-read', type: 'line' } ], unit: 'Bps' } ] export { overviewMetrics } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/QueryEditor/context.ts ================================================ import { IQueryEditorDataSource, IQueryEditorContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { QueryeditorRunRequest } from '~/client' class DataSource implements IQueryEditorDataSource { queryEditorRun(request: QueryeditorRunRequest, options?: ReqConfig) { return client.getInstance().queryEditorRun({ request }, options) } } const ds = new DataSource() export const ctx: IQueryEditorContext = { ds } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/QueryEditor/index.tsx ================================================ import React from 'react' import { QueryEditorApp, QueryEditorProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/QueryEditor/meta.ts ================================================ import { ConsoleSqlOutlined } from '@ant-design/icons' export default { id: 'query_editor', routerPrefix: '/query_editor', icon: ConsoleSqlOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/context-impl.ts ================================================ import { IResourceManagerDataSource, IResourceManagerContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import { AxiosPromise } from 'axios' import client, { ResourcemanagerCalibrateResponse, ResourcemanagerGetConfigResponse, ResourcemanagerResourceInfoRowDef } from '~/client' class DataSource implements IResourceManagerDataSource { getConfig( options?: ReqConfig ): AxiosPromise { return client.getInstance().resourceManagerConfigGet(options) } getInformation( options?: ReqConfig ): AxiosPromise { return client.getInstance().resourceManagerInformationGet(options) } getCalibrateByHardware( params: { workload: string }, options?: ReqConfig | undefined ): AxiosPromise { return client .getInstance() .resourceManagerCalibrateHardwareGet(params, options) } getCalibrateByActual( params: { startTime: number; endTime: number }, options?: ReqConfig | undefined ): AxiosPromise { return client .getInstance() .resourceManagerCalibrateActualGet(params, options) } metricsQueryGet(params: { endTimeSec?: number query?: string startTimeSec?: number stepSec?: number }) { return client .getInstance() .metricsQueryGet(params, { handleError: 'custom' } as ReqConfig) .then((res) => res.data) } } export const getResourceManagerContext: () => IResourceManagerContext = () => { return { ds: new DataSource(), cfg: {} } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/index.tsx ================================================ import React from 'react' import { ResourceManagerApp, ResourceManagerProvider } from '@pingcap/tidb-dashboard-lib' import { getResourceManagerContext } from './context-impl' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/meta.ts ================================================ import { HddOutlined } from '@ant-design/icons' export default { id: 'resource_manager', routerPrefix: '/resource_manager', icon: HddOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/SearchLogs/context.ts ================================================ import { ISearchLogsDataSource, ISearchLogsContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { LogsearchCreateTaskGroupRequest } from '~/client' class DataSource implements ISearchLogsDataSource { logsDownloadAcquireTokenGet(id?: Array, options?: ReqConfig) { return client.getInstance().logsDownloadAcquireTokenGet({ id }, options) } // logsDownloadGet(token: string, options?: ReqConfig) { // return client.getInstance().logsDownloadGet({ token }, options) // } logsTaskgroupPut( request: LogsearchCreateTaskGroupRequest, options?: ReqConfig ) { return client.getInstance().logsTaskgroupPut({ request }, options) } logsTaskgroupsGet(options?: ReqConfig) { return client.getInstance().logsTaskgroupsGet(options) } logsTaskgroupsIdCancelPost(id: string, options?: ReqConfig) { return client.getInstance().logsTaskgroupsIdCancelPost({ id }, options) } logsTaskgroupsIdDelete(id: string, options?: ReqConfig) { return client.getInstance().logsTaskgroupsIdDelete({ id }, options) } logsTaskgroupsIdGet(id: string, options?: ReqConfig) { return client.getInstance().logsTaskgroupsIdGet({ id }, options) } logsTaskgroupsIdPreviewGet(id: string, options?: ReqConfig) { return client.getInstance().logsTaskgroupsIdPreviewGet({ id }, options) } logsTaskgroupsIdRetryPost(id: string, options?: ReqConfig) { return client.getInstance().logsTaskgroupsIdRetryPost({ id }, options) } getTiDBTopology(options?: ReqConfig) { return client.getInstance().getTiDBTopology(options) } getStoreTopology(options?: ReqConfig) { return client.getInstance().getStoreTopology(options) } getPDTopology(options?: ReqConfig) { return client.getInstance().getPDTopology(options) } getTiCDCTopology(options?: ReqConfig) { return client.getInstance().getTiCDCTopology(options) } getTiProxyTopology(options?: ReqConfig) { return client.getInstance().getTiProxyTopology(options) } getTSOTopology(options?: ReqConfig) { return client.getInstance().getTSOTopology(options) } getSchedulingTopology(options?: ReqConfig) { return client.getInstance().getSchedulingTopology(options) } } const ds = new DataSource() export const ctx: ISearchLogsContext = { ds, cfg: { apiPathBase: client.getBasePath() } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/SearchLogs/index.tsx ================================================ import React from 'react' import { SearchLogsApp, SearchLogsProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/SearchLogs/meta.ts ================================================ import { FileSearchOutlined } from '@ant-design/icons' export default { id: 'search_logs', routerPrefix: '/search_logs', icon: FileSearchOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/SlowQuery/context.ts ================================================ import { ISlowQueryDataSource, ISlowQueryContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' class DataSource implements ISlowQueryDataSource { getDatabaseList(beginTime: number, endTime: number, options?: ReqConfig) { return client.getInstance().infoListDatabases(options) } infoListResourceGroupNames(options?: ReqConfig) { return client.getInstance().resourceManagerInformationGroupNamesGet(options) } slowQueryAvailableFieldsGet(options?: ReqConfig) { return client.getInstance().slowQueryAvailableFieldsGet(options) } slowQueryListGet( beginTime?: number, db?: Array, desc?: boolean, digest?: string, endTime?: number, fields?: string, limit?: number, orderBy?: string, plans?: Array, resourceGroup?: Array, text?: string, showInternal?: boolean, options?: ReqConfig ) { return client.getInstance().slowQueryListGet( { beginTime, db, desc, digest, endTime, fields, limit, orderBy, plans, resourceGroup, text }, options ) } slowQueryDetailGet( connectId?: string, digest?: string, timestamp?: number, options?: ReqConfig ) { return client.getInstance().slowQueryDetailGet( { connectId, digest, timestamp }, options ) } slowQueryDownloadTokenPost(request: any, options?: ReqConfig) { return client.getInstance().slowQueryDownloadTokenPost({ request }, options) } } const ds = new DataSource() export const ctx: ISlowQueryContext = { ds, cfg: { apiPathBase: client.getBasePath(), enableExport: true, showDBFilter: true, showDigestFilter: false, showResourceGroupFilter: true // instantQuery: false, // timeRangeSelector: { // recentSeconds: [3 * 24 * 60 * 60], // customAbsoluteRangePicker: true // } } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/SlowQuery/index.tsx ================================================ import React from 'react' import { SlowQueryApp, SlowQueryProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/SlowQuery/meta.ts ================================================ import { RocketOutlined } from '@ant-design/icons' export default { id: 'slow_query', routerPrefix: '/slow_query', icon: RocketOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Statement/context.ts ================================================ import { IStatementDataSource, IStatementContext, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { StatementEditableConfig, StatementGetStatementsRequest } from '~/client' import auth from '~/utils/auth' class DataSource implements IStatementDataSource { getDatabaseList( beginTime: number, endTime: number, options?: ReqConfig | undefined ) { return client.getInstance().infoListDatabases(options) } infoListResourceGroupNames(options?: ReqConfig) { return client.getInstance().resourceManagerInformationGroupNamesGet(options) } statementsAvailableFieldsGet(options?: ReqConfig) { return client.getInstance().statementsAvailableFieldsGet(options) } statementsConfigGet(options?: ReqConfig) { return client.getInstance().statementsConfigGet(options) } statementsConfigPost(request: StatementEditableConfig, options?: ReqConfig) { return client.getInstance().statementsConfigPost({ request }, options) } statementsDownloadGet(token: string, options?: ReqConfig) { return client.getInstance().statementsDownloadGet({ token }, options) } statementsDownloadTokenPost( request: StatementGetStatementsRequest, options?: ReqConfig ) { return client .getInstance() .statementsDownloadTokenPost({ request }, options) } statementsListGet( beginTime?: number, endTime?: number, fields?: string, schemas?: Array, resourceGroups?: Array, stmtTypes?: Array, text?: string, options?: ReqConfig ) { return client.getInstance().statementsListGet( { beginTime, endTime, fields, schemas, resourceGroups, stmtTypes, text }, options ) } statementsPlanDetailGet( beginTime?: number, digest?: string, endTime?: number, plans?: Array, schemaName?: string, options?: ReqConfig ) { return client.getInstance().statementsPlanDetailGet( { beginTime, digest, endTime, plans, schemaName }, options ) } statementsPlansGet( beginTime?: number, digest?: string, endTime?: number, schemaName?: string, options?: ReqConfig ) { return client.getInstance().statementsPlansGet( { beginTime, digest, endTime, schemaName }, options ) } statementsStmtTypesGet(options?: ReqConfig) { return client.getInstance().statementsStmtTypesGet(options) } statementsTimeRangesGet(options?: ReqConfig) { return client.getAxiosInstance().get('/statements/time_ranges', { ...options, headers: { ...options?.headers, Authorization: auth.getAuthTokenAsBearer() || '' } }) } statementsPlanBindStatusGet( sqlDigest: string, beginTime: number, endTime: number, options?: ReqConfig ) { return client.getInstance().statementsPlanBindingGet( { sqlDigest, beginTime, endTime }, options ) } statementsPlanBindCreate(planDigest: string, options?: ReqConfig) { return client.getInstance().statementsPlanBindingPost( { planDigest }, options ) } statementsPlanBindDelete(sqlDigest: string, options?: ReqConfig) { return client.getInstance().statementsPlanBindingDelete( { sqlDigest }, options ) } // slow query slowQueryAvailableFieldsGet(options?: ReqConfig) { return client.getInstance().slowQueryAvailableFieldsGet(options) } slowQueryListGet( beginTime?: number, db?: Array, desc?: boolean, digest?: string, endTime?: number, fields?: string, limit?: number, orderBy?: string, plans?: Array, resourceGroup?: Array, text?: string, showInternal?: boolean, options?: ReqConfig ) { return client.getInstance().slowQueryListGet( { beginTime, db, desc, digest, endTime, fields, limit, orderBy, plans, resourceGroup, text }, options ) } slowQueryDetailGet( connectId?: string, digest?: string, timestamp?: number, options?: ReqConfig ) { return client.getInstance().slowQueryDetailGet( { connectId, digest, timestamp }, options ) } slowQueryDownloadTokenPost(request: any, options?: ReqConfig) { return client.getInstance().slowQueryDownloadTokenPost({ request }, options) } } const ds = new DataSource() export const ctx: IStatementContext = { ds, cfg: { apiPathBase: client.getBasePath(), enableExport: true, enablePlanBinding: true // instantQuery: false } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Statement/index.tsx ================================================ import React from 'react' import { StatementApp, StatementProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/Statement/meta.ts ================================================ import { ThunderboltOutlined } from '@ant-design/icons' export default { id: 'statement', routerPrefix: '/statement', icon: ThunderboltOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/SystemReport/context.ts ================================================ import { ISystemReportDataSource, ISystemReportContext, ReqConfig, ISystemReportConfig } from '@pingcap/tidb-dashboard-lib' import client, { DiagnoseGenerateReportRequest, DiagnoseGenerateMetricsRelationRequest } from '~/client' import publicPathBase from '~/utils/publicPathPrefix' class DataSource implements ISystemReportDataSource { diagnoseReportsGet(options?: ReqConfig) { return client.getInstance().diagnoseReportsGet(options) } diagnoseReportsPost( request: DiagnoseGenerateReportRequest, options?: ReqConfig ) { return client.getInstance().diagnoseReportsPost({ request }, options) } diagnoseGenerateMetricsRelationship( request: DiagnoseGenerateMetricsRelationRequest, options?: ReqConfig ) { return client .getInstance() .diagnoseGenerateMetricsRelationship({ request }, options) } diagnoseReportsIdStatusGet(id: string, options?: ReqConfig) { return client.getInstance().diagnoseReportsIdStatusGet({ id }, options) } } class SystemReportConfig implements ISystemReportConfig { public apiPathBase = client.getBasePath() public publicPathBase = publicPathBase public fullReportLink(reportId: string): string { /* Not using client basePath intentionally so that it can be handled by dev server */ return `${publicPathBase}/api/diagnose/reports/${reportId}/detail` } } export const ctx: ISystemReportContext = { ds: new DataSource(), cfg: new SystemReportConfig() } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/SystemReport/index.tsx ================================================ import React from 'react' import { SystemReportApp, SystemReportProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/SystemReport/meta.ts ================================================ import { SnippetsOutlined } from '@ant-design/icons' export default { id: 'system_report', routerPrefix: '/system_report', icon: SnippetsOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/TopSQL/context.ts ================================================ import { ITopSQLDataSource, ITopSQLContext, ITopSQLConfig, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { TopsqlEditableConfig } from '~/client' import auth from '~/utils/auth' type TikvNetworkIoCollectionConfig = { enable: boolean is_multi_value?: boolean } type TikvNetworkIoCollectionUpdateResponse = { warnings: any[] } class DataSource implements ITopSQLDataSource { topsqlConfigGet(options?: ReqConfig) { return client.getInstance().topsqlConfigGet(options) } topsqlConfigPost(request: TopsqlEditableConfig, options?: ReqConfig) { return client.getInstance().topsqlConfigPost({ request }, options) } topsqlTikvNetworkIoCollectionGet(options?: ReqConfig) { return client .getAxiosInstance() .get( '/topsql/tikv_network_io_collection', { ...options, headers: { ...options?.headers, Authorization: auth.getAuthTokenAsBearer() || '' } } as any ) } topsqlTikvNetworkIoCollectionPost( request: TikvNetworkIoCollectionConfig, options?: ReqConfig ) { return client .getAxiosInstance() .post( '/topsql/tikv_network_io_collection', request, { ...options, headers: { ...options?.headers, Authorization: auth.getAuthTokenAsBearer() || '' } } as any ) } topsqlInstancesGet( end?: string, start?: string, dataSource?: string, options?: ReqConfig ) { const requestParameters: any = { start, end } if (dataSource !== undefined) { requestParameters.dataSource = dataSource } return client.getInstance().topsqlInstancesGet(requestParameters, options) } topsqlSummaryGet( end?: string, groupBy?: string, instance?: string, instanceType?: string, orderBy?: string, start?: string, top?: string, window?: string, dataSource?: string, options?: ReqConfig ) { return client.getInstance().topsqlSummaryGet( { end, groupBy, instance, instanceType, orderBy, start, top, window, dataSource }, options ) } } const ds = new DataSource() export const ctx: ITopSQLContext = { ds, cfg: { checkNgm: true, showSetting: true, showLimit: true, showGroupBy: true, showGroupByRegion: true, showOrderBy: true } as ITopSQLConfig } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/TopSQL/index.tsx ================================================ import React from 'react' import { TopSQLApp, TopSQLProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/TopSQL/meta.ts ================================================ import { BarChartOutlined } from '@ant-design/icons' export default { id: 'topsql', routerPrefix: '/topsql', icon: BarChartOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/UserProfile/context.ts ================================================ import { IUserProfileDataSource, IUserProfileContext, ReqConfig, IUserProfileEvent } from '@pingcap/tidb-dashboard-lib' import client, { SsoCreateImpersonationRequest, SsoSetConfigRequest, CodeShareRequest, MetricsPutCustomPromAddressRequest } from '~/client' import auth from '~/utils/auth' class DataSource implements IUserProfileDataSource { userGetSignOutInfo(redirectUrl?: string, options?: ReqConfig) { return client.getInstance().userGetSignOutInfo({ redirectUrl }, options) } userSSOCreateImpersonation( request: SsoCreateImpersonationRequest, options?: ReqConfig ) { return client.getInstance().userSSOCreateImpersonation({ request }, options) } userSSOGetConfig(options?: ReqConfig) { return client.getInstance().userSSOGetConfig(options) } userSSOListImpersonations(options?: ReqConfig) { return client.getInstance().userSSOListImpersonations(options) } userSSOSetConfig(request: SsoSetConfigRequest, options?: ReqConfig) { return client.getInstance().userSSOSetConfig({ request }, options) } userShareSession(request: CodeShareRequest, options?: ReqConfig) { return client.getInstance().userShareSession({ request }, options) } userRevokeSession(options?: ReqConfig) { return client.getInstance().userRevokeSession(options) } metricsGetPromAddress(options?: ReqConfig) { return client.getInstance().metricsGetPromAddress(options) } metricsSetCustomPromAddress( request: MetricsPutCustomPromAddressRequest, options?: ReqConfig ) { return client .getInstance() .metricsSetCustomPromAddress({ request }, options) } } class EventHandler implements IUserProfileEvent { logOut(): void { auth.clearAuthToken() } } export const ctx: IUserProfileContext = { ds: new DataSource(), event: new EventHandler() } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/UserProfile/index.tsx ================================================ import React from 'react' import { UserProfileApp, UserProfileProvider } from '@pingcap/tidb-dashboard-lib' import { ctx } from './context' export default function () { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/apps/UserProfile/meta.ts ================================================ import { UserOutlined } from '@ant-design/icons' export default { id: 'user_profile', routerPrefix: '/user_profile', icon: UserOutlined, reactRoot: () => import('.') } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/client/apiBasePath.ts ================================================ import publicPathPrefix from '~/utils/publicPathPrefix' export const API_HOST = (function () { let apiPrefix if (process.env.NODE_ENV === 'development') { if (process.env.REACT_APP_DASHBOARD_API_URL) { apiPrefix = `${process.env.REACT_APP_DASHBOARD_API_URL}/dashboard` } else { apiPrefix = `http://${window.location.hostname}:12333/dashboard` } } else { apiPrefix = publicPathPrefix } return apiPrefix })() export function getApiBasePath(): string { return `${API_HOST}/api` } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/client/index.tsx ================================================ import React from 'react' import i18next from 'i18next' import axios, { AxiosInstance } from 'axios' import { message, Modal, notification } from 'antd' import * as singleSpa from 'single-spa' import { routing, i18n } from '@pingcap/tidb-dashboard-lib' import { Configuration, DefaultApi as DashboardApi } from '@pingcap/tidb-dashboard-client' import auth from '~/utils/auth' import { getApiBasePath } from './apiBasePath' import translations from './translations' export * from '@pingcap/tidb-dashboard-client' ////////////////////////////// const client = { _init( apiBasePath: string, apiInstance: DashboardApi, axiosInstance: AxiosInstance ) { this.apiBasePath = apiBasePath this.apiInstance = apiInstance this.axiosInstance = axiosInstance }, getInstance(): DashboardApi { return this.apiInstance }, getBasePath(): string { return this.apiBasePath }, getAxiosInstance(): AxiosInstance { return this.axiosInstance } } export default client ////////////////////////////// type HandleError = 'default' | 'custom' function applyErrorHandlerInterceptor(instance: AxiosInstance) { instance.interceptors.response.use(undefined, async function (err) { const { response, config } = err const handleError = config.handleError as HandleError const method = (config.method as string).toLowerCase() let errCode: string let content: string if (err.message === 'Network Error') { errCode = 'common.network' } else { errCode = response?.data?.code } if (i18next.exists(`error.${errCode ?? ''}`)) { // If there is a translation for the code, use the translation. // TODO: Better to display error details somewhere. content = i18next.t(`error.${errCode}`) } else { content = String( response?.data?.message || err.message || 'Internal error' ) } err.message = content err.errCode = errCode if ( errCode === 'common.unauthenticated' || errCode === 'error.api.unauthorized' // compatible with old tidb-dashboard backend ) { // Handle unauthorized error in a unified way if (!routing.isLocationMatch('/') && !routing.isSignInPage()) { message.error({ content, key: errCode }) } auth.clearAuthToken() singleSpa.navigateToUrl('#' + routing.signInRoute) err.handled = true } else if (handleError === 'default') { if (method === 'get') { const fullUrl = config.url as string const API = fullUrl.replace(client.getBasePath(), '').split('?')[0] notification.error({ key: API, message: i18next.t('error.title'), description: ( API: {API}
{content}
) }) } else if (['post', 'put', 'delete', 'patch'].includes(method)) { Modal.error({ title: i18next.t('error.title'), content: content, zIndex: 2000 // higher than popover }) } err.handled = true } return Promise.reject(err) }) } function initAxios(apiBasePath: string) { const instance = axios.create({ baseURL: apiBasePath }) applyErrorHandlerInterceptor(instance) return instance } function init() { i18n.addTranslations(translations) const apiBasePath = getApiBasePath() const axiosInstance = initAxios(apiBasePath) const dashboardApi = new DashboardApi( new Configuration({ apiKey: () => auth.getAuthTokenAsBearer() || '', baseOptions: { handleError: 'default' } }), '', axiosInstance ) client._init(apiBasePath, dashboardApi, axiosInstance) } init() ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/client/translations/en.yaml ================================================ error: title: Error common: network: Network connection error unauthenticated: Please sign in again (session is expired) forbidden: The current user is not authorized to perform this action api: user: signin: invalid_code: Authorization Code is invalid or expired insufficient_priv: The user does not have sufficient privileges to access {{distro.tidb}} Dashboard. slow_query: export_no_data: No slow queires can be exported statement: export_no_data: No statements can be exported continuous_profiling: ng_monitoring_not_ready: | To use or learn more about "Continuous Profiling" feature, please search for "Continuous Profiling" in the {{distro.tidb}} official docs for more information. If it doesn't resove the issue, please contact the product's technical support. feature_not_supported: The cluster of this version doesn't support or can't use this feature, please contact with technical support to get more information. tidb: no_alive_tidb: No alive {{distro.tidb}} instance pd_access_failed: Failed to access {{distro.pd}} node tidb_conn_failed: Failed to connect to {{distro.tidb}} tidb_auth_failed: '{{distro.tidb}} authentication failed' ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/client/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/client/translations/zh.yaml ================================================ error: title: 错误 common: network: 网络连接失败 unauthenticated: 会话已过期,请重新登录 forbidden: 当前用户没有权限进行该操作 api: user: signin: invalid_code: 授权码无效或已过期 insufficient_priv: 该用户缺少足够的权限访问 {{distro.tidb}} Dashboard。 slow_query: export_no_data: 没有可导出的慢查询日志 statement: export_no_data: 没有可导出的语句 continuous_profiling: ng_monitoring_not_ready: | 想使用或深入了解“持续性能分析”功能,请在 {{distro.tidb}} 官方文档搜索“持续性能分析”查看更多内容。 若未能解决问题,请联系本产品技术支持。 feature_not_supported: 当前版本的集群不支持或无法使用该功能,请联系技术支持了解详细情况。 tidb: no_alive_tidb: 没有正在运行的 {{distro.tidb}} 实例 pd_access_failed: 无法访问 {{distro.pd}} 节点 tidb_conn_failed: 无法连接到 {{distro.tidb}} tidb_auth_failed: '{{distro.tidb}} 登录验证失败' ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/index.ts ================================================ import '../styles/style.less' import '@pingcap/tidb-dashboard-lib/dist/index.css' import './main' import '../styles/override.less' ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/main/Sider/Banner.module.less ================================================ @import 'antd/es/style/themes/default.less'; .banner { background: @primary-color; color: #fff; cursor: pointer; overflow: hidden; user-select: none; position: relative; margin-bottom: 20px; flex-shrink: 0; } .bannerLeft { padding: 20px 16px 20px 24px; } .bannerRight { position: absolute; top: 0; height: 100%; transition: background-color 0.2s @ease-out; display: flex; } .banner:hover .bannerRight { background: lighten(@primary-color, 5%); } .bannerLogo { margin: 5px 0; } .bannerContent { margin-left: 15px; } .bannerTitle { font-size: 1rem; } .bannerVersion { font-size: 0.9rem; opacity: 0.7; } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/main/Sider/Banner.tsx ================================================ import React, { useMemo, useRef } from 'react' import { CaretRightOutlined, CaretLeftOutlined } from '@ant-design/icons' import { useSize } from 'ahooks' import Flexbox from '@g07cha/flexbox-react' import { useSpring, animated } from 'react-spring' import { useTranslation, TFunction } from 'react-i18next' import { InfoInfoResponse } from '~/client' import { // store store } from '@pingcap/tidb-dashboard-lib' import { lightLogoSvg } from '~/utils/distro/assetsRes' import styles from './Banner.module.less' const toggleWidth = 40 const toggleHeight = 50 function parseVersion(i: InfoInfoResponse, t: TFunction) { if (!i.version) { return null } if (i.version.standalone !== 'No') { // For Standalone == Yes / Unknown, display internal version if (i.version.internal_version === 'nightly') { let vPrefix = i.version.internal_version if (i.version.build_git_hash) { vPrefix += `-${i.version.build_git_hash.substr(0, 8)}` } // e.g. nightly-xxxxxxxx return vPrefix } if (i.version.internal_version) { // e.g. v2020.07.01.1 if (i.version.internal_version.startsWith('v')) { return i.version.internal_version } else { return `v${i.version.internal_version}` } } return null } if (i.version.pd_version) { // e.g. PD v4.0.1 return `${t('distro.pd')} ${i.version.pd_version}` } } export default function ToggleBanner({ fullWidth, collapsedWidth, collapsed, onToggle }) { const { t } = useTranslation() const bannerRef = useRef(null) const bannerSize = useSize(bannerRef) const transBanner = useSpring({ opacity: collapsed ? 0 : 1, height: collapsed ? toggleHeight : bannerSize?.height ?? 0 }) const transButton = useSpring({ left: collapsed ? 0 : fullWidth - toggleWidth, width: collapsed ? collapsedWidth : toggleWidth }) const appInfo = store.useState((s) => s.appInfo) const version = useMemo(() => { if (appInfo) { return parseVersion(appInfo, t) } return null }, [appInfo, t]) return (
{t('distro.tidb')} Dashboard
{version || 'Version unknown'}
{collapsed ? ( ) : ( )}
) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/main/Sider/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; @sider-background: rgb(246, 246, 246); @sider-highlight-height: 36px; @sider-ribbon-height: 30px; .sider { position: fixed; left: 0; top: 0; height: 100%; z-index: 1; background: @sider-background; overflow-x: hidden; overflow-y: auto; transition: none; user-select: none; :global { /* cancel text animations */ .ant-menu-item .ant-menu-item-icon, .ant-menu-submenu-title .ant-menu-item-icon, .ant-menu-item .anticon, .ant-menu-submenu-title .anticon { transition-duration: 0.1s; } .ant-menu-item .ant-menu-item-icon + span, .ant-menu-submenu-title .ant-menu-item-icon + span, .ant-menu-item .anticon + span, .ant-menu-submenu-title .anticon + span { transition-duration: 0.1s; } .ant-menu-item, .ant-menu-submenu-title { transition-duration: 0.1s; } .ant-menu-title-content { transition-duration: 0.1s; } .ant-layout-sider-children { display: flex; flex-direction: column; padding-bottom: 20px; .ant-menu.ant-menu-inline-collapsed { width: @menu-collapsed-width; // 80px } } .ant-menu { border-right: 0; background: none; } .ant-menu-submenu-selected { color: #000; } .ant-menu-item { background: none !important; &::after { left: 0; top: 0; height: @sider-ribbon-height; margin-top: -(@sider-ribbon-height / 2); top: 50%; border: 0px; width: 5px; border-radius: 0 5px 5px 0; background: @primary-color; } &::before { content: ''; position: absolute; left: 0; right: 20px; height: @sider-highlight-height; top: 50%; margin-top: -(@sider-highlight-height / 2); border-radius: 0 (@sider-highlight-height / 2) (@sider-highlight-height / 2) 0; z-index: -1; } &:hover::before { background: rgba(lighten(@primary-color, 20%), 0.2); } a { color: #666; transition-duration: 0s; &:hover { color: #000; } } &.ant-menu-item-selected { &::before { background: rgba(darken(@sider-background, 30%), 0.15); } a { color: #000; } } } .ant-menu-submenu-title { background: none !important; } .ant-menu-inline-collapsed .ant-menu-item { &::before { right: 10px; left: 10px; border-radius: (@sider-highlight-height / 2); } } } } :global { .ant-menu-inline-collapsed-tooltip { a { color: @text-color-dark; // hsla(0, 0%, 100%, 0.85) } .anticon { display: none; } } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/main/Sider/index.tsx ================================================ import React, { useState, useMemo } from 'react' import { ExperimentOutlined, BugOutlined, AimOutlined // PullRequestOutlined } from '@ant-design/icons' import { Layout, Menu } from 'antd' import { Link } from 'react-router-dom' import { useEventListener } from 'ahooks' import { useTranslation } from 'react-i18next' import { useSpring, animated } from 'react-spring' import Banner from './Banner' import styles from './index.module.less' import { store, useIsFeatureSupport } from '@pingcap/tidb-dashboard-lib' function useAppMenuItem( registry, appId, enable: boolean = true, title?: string, hideIcon?: boolean ) { const { t } = useTranslation() const app = registry.apps[appId] if (!enable || !app) { return null } return ( {!hideIcon && app.icon ? : null} {title ? title : t(`${appId}.nav_title`, appId)} ) } function useActiveAppId(registry) { const [appId, set] = useState('') useEventListener( 'single-spa:routing-event', () => { const activeApp = registry.getActiveApp() if (activeApp) { set(activeApp.id) } }, { target: window } ) return appId } function Sider({ registry, fullWidth, defaultCollapsed, collapsed, collapsedWidth, onToggle, animationDelay }) { const { t } = useTranslation() const activeAppId = useActiveAppId(registry) const whoAmI = store.useState((s) => s.whoAmI) const appInfo = store.useState((s) => s.appInfo) const supportConProf = useIsFeatureSupport('conprof') const profilingSubMenuItems = [ useAppMenuItem(registry, 'instance_profiling', true, '', true), useAppMenuItem(registry, 'conprof', supportConProf, '', true) ] const profilingSubMenu = ( {t('profiling.nav_title')} } > {profilingSubMenuItems} ) const debugSubMenuItems = [ profilingSubMenu, useAppMenuItem(registry, 'debug_api') ] const debugSubMenu = ( {t('nav.sider.debug')} } > {debugSubMenuItems} ) // const conflictSubMenuItems = [useAppMenuItem(registry, 'deadlock')] // const conflictSubMenu = ( // // // {t('nav.sider.conflict')} // // } // > // {conflictSubMenuItems} // // ) const experimentalSubMenuItems = [ useAppMenuItem(registry, 'query_editor'), useAppMenuItem(registry, 'configuration') ] const experimentalSubMenu = ( {t('nav.sider.experimental')} } > {experimentalSubMenuItems} ) const supportTopSQL = useIsFeatureSupport('topsql') const supportResourceManager = useIsFeatureSupport('resource_manager') const menuItems = [ useAppMenuItem(registry, 'overview'), useAppMenuItem(registry, 'cluster_info'), // topSQL useAppMenuItem(registry, 'topsql', supportTopSQL), useAppMenuItem(registry, 'statement'), useAppMenuItem(registry, 'slow_query'), useAppMenuItem(registry, 'keyviz'), useAppMenuItem(registry, 'system_report'), // warning: "diagnose" app doesn't release yet // useAppMenuItem(registry, 'diagnose'), useAppMenuItem(registry, 'monitoring'), useAppMenuItem(registry, 'search_logs'), useAppMenuItem(registry, 'resource_manager', supportResourceManager), // useAppMenuItem(registry, '__APP_NAME__'), // NOTE: Don't remove above comment line, it is a placeholder for code generator debugSubMenu // conflictSubMenu ] if (appInfo?.enable_experimental) { menuItems.push(experimentalSubMenu) } let displayName = whoAmI?.display_name || '...' const extraMenuItems = [ useAppMenuItem(registry, 'dashboard_settings'), useAppMenuItem(registry, 'user_profile', true, displayName) ] const transSider = useSpring({ width: collapsed ? collapsedWidth : fullWidth }) const defaultOpenKeys = useMemo(() => { if (defaultCollapsed) { return [] } else { return ['debug', 'experimental', 'profiling'] } }, [defaultCollapsed]) return ( {menuItems} {extraMenuItems} ) } export default Sider ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/main/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .container { height: 100vh; } .content { position: relative; z-index: 3; background: #fff; min-height: 100vh; &:before, &:after { // Handle margin collapse content: ' '; display: table; } } .contentBack { position: fixed; z-index: 2; background: #fff; top: 0; height: 100%; right: 0; box-shadow: 0 0 20px rgba(#000, 0.1); } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/main/index.tsx ================================================ import React, { useState, useCallback, useEffect } from 'react' // import { Root } from '@lib/components' import { HashRouter as Router } from 'react-router-dom' import { useSpring, animated } from 'react-spring' // import { useVersionedLocalStorageState } from '@lib/utils/useVersionedLocalStorageState' import { Root, useVersionedLocalStorageState } from '@pingcap/tidb-dashboard-lib' import Sider from './Sider' import styles from './index.module.less' const siderWidth = 260 const siderCollapsedWidth = 80 const collapsedContentOffset = siderCollapsedWidth - siderWidth const contentOffsetTrigger = collapsedContentOffset * 0.99 function triggerResizeEvent() { const event = document.createEvent('HTMLEvents') event.initEvent('resize', true, false) window.dispatchEvent(event) } const useContentLeftOffset = (collapsed) => { const [offset, setOffset] = useState(siderWidth) const onAnimationStart = useCallback(() => { if (!collapsed) { setOffset(siderWidth) } }, [collapsed]) const onAnimationFrame = useCallback( ({ x }) => { if (collapsed && x < contentOffsetTrigger) { setOffset(siderCollapsedWidth) } }, [collapsed] ) useEffect(triggerResizeEvent, [offset]) return { contentLeftOffset: offset, onAnimationStart, onAnimationFrame } } export default function App({ registry }) { const [collapsed, setCollapsed] = useVersionedLocalStorageState( 'layout.sider.collapsed', { defaultValue: false } ) const [defaultCollapsed] = useState(collapsed) const { contentLeftOffset, onAnimationStart, onAnimationFrame } = useContentLeftOffset(collapsed) const transContentBack = useSpring({ x: collapsed ? collapsedContentOffset : 0, onStart: onAnimationStart, onFrame: onAnimationFrame }) const transContainer = useSpring({ opacity: 1, from: { opacity: 0 }, delay: 100 }) const handleToggle = useCallback(() => { setCollapsed((c) => !c) }, [setCollapsed]) const { appOptions } = registry return ( {!appOptions.hideNav && ( <> `translate3d(${x}px, 0, 0)` ) }} > )}
) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/signin/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; @import 'antd/es/button/style/mixin.less'; @content-width: 400px; .container { position: fixed; left: 0; top: 0; width: 100%; height: 100%; display: flex; flex-direction: row; align-items: stretch; } .contantContainer { min-width: @content-width; width: 38%; background: #fff; position: relative; } .dialogContainer { height: 100%; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 30px 65px; overflow-y: auto; } .dialog { max-width: 300px; width: 100%; margin: auto; } .landingContainer { flex-grow: 1; } .landing { background-size: cover; background-position: center left; height: 100%; } .logo { height: 40px; margin-bottom: 80px; } .signInButton { margin-top: 10px; margin-bottom: 20px; font-size: 1rem; } .extraLink { font-size: 0.9rem; margin: 15px 0; a { color: @gray-7; &:hover { color: @gray-7; } } &.clickable { a:hover { color: @link-hover-color; } } } .alternativeFormLayer { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: #fff; z-index: 2; } .alternativeCloseButton { font-size: 1rem; border: 0; background: #fff; color: @text-color; cursor: pointer; padding: @padding-xss; padding-right: 0; &:hover, &:active, &:focus { color: @link-hover-color; outline: 0; } } .alternativeButton { .btn; .btn-default; height: auto; width: 100%; color: @text-color; margin-bottom: @padding-sm; text-align: left; padding: @padding-md; padding-right: 50px + @padding-md; position: relative; word-wrap: normal; white-space: normal; line-height: 1; &:hover, &:active, &:focus { color: @text-color; } .title { color: @heading-color; margin-bottom: @padding-sm; } .icon { font-size: 1.3rem; position: absolute; right: 0; width: 50px; text-align: center; opacity: 0; transform: translateX(-5px); transition: opacity 0.2s linear, transform 0.2s linear; color: @gray-6; // For vertical align top: 50%; margin-top: -20px; line-height: 40px; } &:hover, &:active, &:focus { .icon { opacity: 1; transform: none; } } } .container :global { .formAnimation { animation: 0.2s @ease-out-circ 0.5s antZoomBigIn; animation-fill-mode: both; animation-iteration-count: 1; } .landingAnimation { animation: 0.5s linear 0 antFadeIn; animation-fill-mode: both; animation-iteration-count: 1; } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/signin/index.tsx ================================================ import CSSMotion from 'rc-animate/es/CSSMotion' import cx from 'classnames' import * as singleSpa from 'single-spa' import React, { useState, useEffect, useRef, useCallback, useMemo, ReactNode } from 'react' import { DownOutlined, GlobalOutlined, LockOutlined, UserOutlined, KeyOutlined, ArrowRightOutlined, CloseOutlined } from '@ant-design/icons' import { Form, Input, InputRef, Button, message, Typography, Modal } from 'antd' import { useTranslation } from 'react-i18next' import { useMount } from 'react-use' import Flexbox from '@g07cha/flexbox-react' import { useMemoizedFn } from 'ahooks' import JSEncrypt from 'jsencrypt' import { // distro isDistro, // store useIsFeatureSupport, // components Root, AppearAnimate, LanguageDropdown } from '@pingcap/tidb-dashboard-lib' import client, { UserAuthenticateForm } from '~/client' import auth from '~/utils/auth' import { getAuthURL } from '~/utils/authSSO' import { landingSvg, logoSvg } from '~/utils/distro/assetsRes' import styles from './index.module.less' enum DisplayFormType { uninitialized, tidbCredential, shareCode, sso } function AlternativeAuthLink({ onClick }) { const { t } = useTranslation() return ( ) } function LanguageDrop() { return ( ) } interface IAlternativeFormButtonProps extends React.ButtonHTMLAttributes { title: string description: string className?: string } function AlternativeFormButton({ title, description, className, ...restProps }: IAlternativeFormButtonProps) { return ( ) } function AlternativeAuthForm({ className, onClose, onSwitchForm, supportedAuthTypes, ...restProps }) { const { t } = useTranslation() return (

{t('signin.form.alternative.title')}

onSwitchForm(DisplayFormType.tidbCredential)} /> onSwitchForm(DisplayFormType.shareCode)} /> {Boolean(supportedAuthTypes.indexOf(auth.AuthTypes.SSO) > -1) && ( onSwitchForm(DisplayFormType.sso)} /> )}
) } function useSignInSubmit( successRoute, fnLoginForm: (form) => UserAuthenticateForm, onSuccess: (form) => void, onFailure: () => void ) { const { t } = useTranslation() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const clearErrorMsg = useCallback(() => { setError(null) }, []) const handleSubmit = useMemoizedFn(async (form) => { try { clearErrorMsg() setLoading(true) const r = await client .getInstance() .userLogin({ message: fnLoginForm(form) }, { handleError: 'custom' } as any) auth.setAuthToken(r.data.token) message.success(t('signin.message.success')) singleSpa.navigateToUrl(successRoute) onSuccess(form) } catch (e) { const { handled, message, errCode } = e as any if (!handled) { const errMsg = t('signin.message.error', { msg: message }) if (errCode !== 'api.user.signin.insufficient_priv') { setError(errMsg) } else { // only add help link for TiDB distro when meeting insufficient_privileges error const errComp = ( <> {errMsg} {!isDistro() && ( <> {' '} {t('signin.message.access_doc')} )} ) setError(errComp) } onFailure() } } finally { setLoading(false) } }) return { handleSubmit, loading, errorMsg: error, clearErrorMsg } } const LAST_LOGIN_USERNAME_KEY = 'dashboard_last_login_username' function TiDBSignInForm({ successRoute, onClickAlternative, publicKey }) { const supportNonRootLogin = useIsFeatureSupport('nonRootLogin') const { t } = useTranslation() const [refForm] = Form.useForm() const refPassword = useRef(null) const { handleSubmit, loading, errorMsg, clearErrorMsg } = useSignInSubmit( successRoute, (form) => { let password = form.password ?? '' if (!!publicKey) { const jsEncrypt = new JSEncrypt() if (publicKey.startsWith('-----BEGIN PUBLIC KEY-----')) { // if publicKey is generated by `ExportPublicKeyAsString(s.rsaPublicKey)`, it has header and footer, so we use it directly jsEncrypt.setPublicKey(publicKey) } else { // if publicKey is generated by `DumpPublicKeyBase64(s.rsaPublicKey)`, it has no header and footer, so we need to add them jsEncrypt.setPublicKey( '-----BEGIN PUBLIC KEY-----' + publicKey + '-----END PUBLIC KEY-----' ) } password = jsEncrypt.encrypt(password) } return { username: form.username, password, type: auth.AuthTypes.SQLUser } }, (form) => { localStorage.setItem(LAST_LOGIN_USERNAME_KEY, form.username) }, () => { refForm.setFieldsValue({ password: '' }) setTimeout(() => { refPassword.current?.focus() }, 0) } ) useMount(() => { refPassword?.current?.focus() }) const lastLoginUsername = useMemo(() => { return localStorage.getItem(LAST_LOGIN_USERNAME_KEY) || '' }, []) return (

{t('signin.form.tidb_auth.title')}

} disabled={!supportNonRootLogin} /> } type="password" disabled={loading} onInput={clearErrorMsg} ref={refPassword} data-e2e="signin_password_input" />
) } function CodeSignInForm({ successRoute, onClickAlternative }) { const { t } = useTranslation() const [refForm] = Form.useForm() const refPassword = useRef(null) const { handleSubmit, loading, errorMsg, clearErrorMsg } = useSignInSubmit( successRoute, (form) => ({ password: form.code, type: auth.AuthTypes.SharingCode }), () => {}, () => { refForm.setFieldsValue({ code: '' }) setTimeout(() => { refPassword.current?.focus() }, 0) } ) useMount(() => { refPassword?.current?.focus() }) return (

{t('signin.form.code_auth.title')}

} type="password" onInput={clearErrorMsg} disabled={loading} ref={refPassword} allowClear />
) } function SSOSignInForm({ successRoute, onClickAlternative }) { const { t } = useTranslation() const [isLoading, setIsLoading] = useState(false) const handleSignIn = useCallback(async () => { setIsLoading(true) try { const url = await getAuthURL() window.location.href = url // Do not hide loading status when url is resolved, since we are now jumping } catch (e) { setIsLoading(false) } }, []) return (
) } function App({ registry }) { const successRoute = useMemo( () => `#${registry.getDefaultRouter()}`, [registry] ) const [alternativeVisible, setAlternativeVisible] = useState(false) const [formType, setFormType] = useState(DisplayFormType.uninitialized) const [supportedAuthTypes, setSupportedAuthTypes] = useState>([ 0 ]) const [publicKey, setPublicKey] = useState('') const handleClickAlternative = useCallback(() => { setAlternativeVisible(true) }, []) const handleAlternativeClose = useCallback(() => { setAlternativeVisible(false) }, []) const handleSwitchForm = useCallback((k: DisplayFormType) => { setFormType(k) setAlternativeVisible(false) }, []) useEffect(() => { async function run() { try { const resp = await client .getInstance() .userGetLoginInfo({ handleError: 'custom' } as any) const loginInfo = resp.data if ( (loginInfo.supported_auth_types?.indexOf(auth.AuthTypes.SSO) ?? -1) > -1 ) { setFormType(DisplayFormType.sso) } else { setFormType(DisplayFormType.tidbCredential) } setSupportedAuthTypes(loginInfo.supported_auth_types ?? []) if (!!loginInfo.sql_auth_public_key) { setPublicKey(loginInfo.sql_auth_public_key) } } catch (e) { if ((e as any).response?.status === 404) { setFormType(DisplayFormType.tidbCredential) } else { Modal.error({ title: 'Initialize Sign in failed', content: '' + e, okText: 'Reload', onOk: () => window.location.reload() }) } } } run() }, []) return (
{({ style, className }) => ( )} {formType === DisplayFormType.tidbCredential && ( )} {formType === DisplayFormType.shareCode && ( )} {formType === DisplayFormType.sso && ( )}
) } export default App ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/translations/en.yaml ================================================ signin: message: error: 'Sign in failed: {{ msg }}' success: Sign in successfully access_doc: Help access_doc_link: https://docs.pingcap.com/tidb/stable/dashboard-user form: username: Username username_tooltip: Sign in user can be customized in TiDB 5.3 or later versions password: Password button: Sign In tidb_auth: title: SQL User Sign In switch: title: SQL User description: I know the username and password to connect to the database code_auth: title: Authorization Code Sign In switch: title: Authorization Code description: I was invited by others with an authorization code code: Code sso: button: Sign In via Company Account (SSO) switch: title: SSO description: I want to sign in use my company account use_alternative: Use Alternative Authentication alternative: title: Select Authentication nav: user: signout: Sign Out sider: debug: Advanced Debugging conflict: Conflict Diagnosing experimental: Experimental Features health_check: failed_notification_title: System Health Check Failed ngm_not_started: A required component `NgMonitoring` is not started in this cluster. Some features may not work. help_text: Help help_url: https://docs.pingcap.com/tidb/dev/dashboard-faq#a-required-component-ngmonitoring-is-not-started-error-is-shown ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/translations/zh.yaml ================================================ signin: message: error: '登录失败: {{ msg }}' success: 登录成功 access_doc: 帮助 access_doc_link: https://docs.pingcap.com/zh/tidb/stable/dashboard-user form: username: 用户名 username_tooltip: 升级到 TiDB 5.3 及更高版本后可自定义登录用户 password: 密码 button: 登录 tidb_auth: title: SQL 用户登录 switch: title: SQL 用户 description: 我知道数据库的登录用户名和密码 code_auth: title: 授权码登录 switch: title: 授权码 description: 其他人通过授权码邀请我使用 code: 授权码 sso: button: 使用公司账号 SSO 登录 switch: title: SSO description: 使用公司账号登录 use_alternative: 使用其他登录方式 alternative: title: 选择登录方式 nav: user: signout: 登出 sider: debug: 高级调试 conflict: 冲突诊断 experimental: 实验性功能 health_check: failed_notification_title: 系统健康检查失败 ngm_not_started: 集群中未启动必要组件 `NgMonitoring`,部分功能将不可用。 help_text: 帮助 help_url: https://docs.pingcap.com/zh/tidb/dev/dashboard-faq#界面提示-集群中未启动必要组件-ngmonitoring ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/main.tsx ================================================ import React from 'react' import * as singleSpa from 'single-spa' import i18next from 'i18next' import { Modal, notification } from 'antd' import NProgress from 'nprogress' import './nprogress.less' import { routing, i18n, // telemetry telemetry, // store NgmState, // distro distro, isDistro } from '@pingcap/tidb-dashboard-lib' import { InfoInfoResponse } from '~/client' import auth from '~/utils/auth' import { handleSSOCallback, isSSOCallback } from '~/utils/authSSO' import { mustLoadAppInfo, reloadWhoAmI } from '~/utils/store' import { loadAppOptions, saveAppOptions } from '~/utils/appOptions' import AppRegistry from '~/utils/registry' import AppOverview from '~/apps/Overview/meta' import AppMonitoring from '~/apps/Monitoring/meta' import AppClusterInfo from '~/apps/ClusterInfo/meta' import AppTopSQL from '~/apps/TopSQL/meta' import AppSlowQuery from '~/apps/SlowQuery/meta' import AppStatement from '~/apps/Statement/meta' import AppKeyViz from '~/apps/KeyViz/meta' import AppSystemReport from '~/apps/SystemReport/meta' import AppSearchLogs from '~/apps/SearchLogs/meta' import AppInstanceProfiling from '~/apps/InstanceProfiling/meta' import AppConProfiling from '~/apps/ContinuousProfiling/meta' import AppDebugAPI from '~/apps/DebugAPI/meta' import AppQueryEditor from '~/apps/QueryEditor/meta' import AppConfiguration from '~/apps/Configuration/meta' import AppUserProfile from '~/apps/UserProfile/meta' import AppDiagnose from '~/apps/Diagnose/meta' import AppOptimizerTrace from '~/apps/OptimizerTrace/meta' import AppDeadlock from '~/apps/Deadlock/meta' import AppResourceManager from '~/apps/ResourceManager/meta' import LayoutMain from './layout/main' import LayoutSignIn from './layout/signin' import translations from './layout/translations' // for update distro strings resource import '~/utils/distro/stringsRes' function removeSpinner() { const spinner = document.getElementById('dashboard_page_spinner') if (spinner) { spinner.remove() } } async function webPageStart() { const options = loadAppOptions() if (options.lang) { i18next.changeLanguage(options.lang) } i18n.addTranslations(translations) let info: InfoInfoResponse try { info = await mustLoadAppInfo() } catch (e) { Modal.error({ title: `Failed to connect to server`, content: '' + e, okText: 'Reload', onOk: () => window.location.reload() }) removeSpinner() return } telemetry.init( process.env.REACT_APP_MIXPANEL_HOST, process.env.REACT_APP_MIXPANEL_TOKEN ) if (info?.enable_telemetry) { // mixpanel // close mixpanel telemetry for tidb-dashboard op // telemetry.enable(info.version?.internal_version!) let preRoute = '' window.addEventListener('single-spa:routing-event', () => { const curRoute = routing.getPathInLocationHash() if (curRoute !== preRoute) { telemetry.trackRouteChange(curRoute) preRoute = curRoute } }) } if (!options.skipNgmCheck && info?.ngm_state === NgmState.NotStarted) { notification.error({ key: 'ngm_not_started', message: i18next.t('health_check.failed_notification_title'), description: ( {i18next.t('health_check.ngm_not_started')} {!isDistro() && ( <> {' '} {i18next.t('health_check.help_text')} )} ), duration: null }) } const registry = new AppRegistry(options) NProgress.configure({ showSpinner: false }) window.addEventListener('single-spa:before-routing-event', () => { NProgress.set(0.2) }) window.addEventListener('single-spa:routing-event', () => { NProgress.done(true) }) singleSpa.registerApplication( 'layout', AppRegistry.newReactSpaApp(() => LayoutMain, 'root'), () => { return !routing.isSignInPage() }, { registry } ) singleSpa.registerApplication( 'signin', AppRegistry.newReactSpaApp(() => LayoutSignIn, 'root'), () => { return routing.isSignInPage() }, { registry } ) registry .register(AppUserProfile) .register(AppOverview) .register(AppClusterInfo) .register(AppKeyViz) .register(AppTopSQL) .register(AppStatement) .register(AppSystemReport) .register(AppSlowQuery) .register(AppDiagnose) .register(AppMonitoring) .register(AppSearchLogs) .register(AppInstanceProfiling) .register(AppConProfiling) .register(AppQueryEditor) .register(AppConfiguration) .register(AppDebugAPI) .register(AppOptimizerTrace) .register(AppDeadlock) .register(AppResourceManager) try { const ok = await reloadWhoAmI() if (routing.isLocationMatch('/') && ok) { singleSpa.navigateToUrl('#' + registry.getDefaultRouter()) } } catch (e) { // If there are auth errors, redirection will happen any way. So we continue. } window.addEventListener('single-spa:app-change', () => { if (!routing.isSignInPage()) { if (!auth.getAuthTokenAsBearer()) { singleSpa.navigateToUrl('#' + routing.signInRoute) } } }) window.addEventListener('single-spa:first-mount', () => { removeSpinner() }) singleSpa.start() } async function main() { document.title = `${distro().tidb} Dashboard` if (routing.isPortalPage()) { // the portal page is only used to receive options function handlePortalEvent(event) { const { type, token, lang, hideNav, skipNgmCheck, redirectPath } = event.data // the event type must be "DASHBOARD_PORTAL_EVENT" if (type !== 'DASHBOARD_PORTAL_EVENT') { return } auth.setAuthToken(token) saveAppOptions({ hideNav, lang, skipNgmCheck }) window.location.hash = `#${redirectPath}` window.location.reload() window.removeEventListener('message', handlePortalEvent) } window.addEventListener('message', handlePortalEvent) return } if (isSSOCallback()) { await handleSSOCallback() return } await webPageStart() } main() ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/dashboardApp/nprogress.less ================================================ @progress-color: #ffc53d; #nprogress { pointer-events: none; } #nprogress .bar { background: @progress-color; position: fixed; z-index: 1031; top: 0; left: 0; width: 100%; height: 2px; } /* Fancy blur effect */ #nprogress .peg { display: block; position: absolute; right: 0px; width: 100px; height: 100%; box-shadow: 0 0 10px @progress-color, 0 0 5px @progress-color; opacity: 1; transform: rotate(3deg) translate(0px, -4px); } /* Remove these to get rid of the spinner */ #nprogress .spinner { display: block; position: fixed; z-index: 1031; top: 15px; right: 15px; } #nprogress .spinner-icon { width: 18px; height: 18px; box-sizing: border-box; border: solid 2px transparent; border-top-color: @progress-color; border-left-color: @progress-color; border-radius: 50%; animation: nprogress-spinner 400ms linear infinite; } .nprogress-custom-parent { overflow: hidden; position: relative; } .nprogress-custom-parent #nprogress .spinner, .nprogress-custom-parent #nprogress .bar { position: absolute; } @keyframes nprogress-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/diagnoseReportApp/components/DiagnosisReport.tsx ================================================ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { i18n } from '@pingcap/tidb-dashboard-lib' import DiagnosisTable from './DiagnosisTable' import { ExpandContext, TableDef } from '../types' function LangDropdown() { const { i18n: i18next } = useTranslation() return (
) } type Props = { diagnosisTables: TableDef[] } function TablesNavMenu({ diagnosisTables }: Props) { const { t } = useTranslation() return (
{diagnosisTables.map((item) => (

{item.category[0] && t(`diagnosis.tables.category.${item.category[0]}`)}

{t(`diagnosis.tables.title.${item.title}`)}
))}
) } export default function DiagnosisReport({ diagnosisTables }: Props) { const [expandAll, setExpandAll] = useState(false) const { t } = useTranslation() return (

{t('diagnosis.title')}

{diagnosisTables.map((item, idx) => ( ))}
) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/diagnoseReportApp/components/DiagnosisTable.tsx ================================================ import React, { useContext, useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import ReactMarkdown from 'react-markdown' import { distro } from '@pingcap/tidb-dashboard-lib' import { TableDef, ExpandContext, TableRowDef } from '../types' const lowerDistro = Object.keys(distro).reduce((accu, cur) => { if (typeof distro[cur] === 'string') { accu[cur] = distro[cur].toLowerCase() } return accu }, {}) const distroRegs = Object.keys(lowerDistro).reduce((accu, cur) => { accu[cur] = new RegExp(cur, 'ig') return accu }, {}) function replaceDistro(oriStr: string): string { let retStr = oriStr Object.keys(lowerDistro).forEach((key) => { retStr = retStr.replace(distroRegs[key], lowerDistro[key]) }) return retStr } function DiagnosisRow({ row }: { row: TableRowDef }) { const outsideExpand = useContext(ExpandContext) const [internalExpand, setInternalExpand] = useState(false) const { t, i18n } = useTranslation() // when outsideExpand changes, reset the internalExpand to the same as outsideExpand useEffect(() => { setInternalExpand(outsideExpand) }, [outsideExpand]) function showRowName(rowName: string) { const i18nKey = `diagnosis.tables.table.name.${rowName}` if (i18n.exists(i18nKey)) { return t(i18nKey) } return replaceDistro(rowName) } function showOthers(val: string | number) { if (typeof val === 'string') { return replaceDistro(val) } return val } return ( <> {(row.values || []).map((val, valIdx) => ( {valIdx === 0 ? showRowName(val) : showOthers(val)} {valIdx === 0 && t(`diagnosis.tables.table.comment.${val}`, '') !== '' && (

{t(`diagnosis.tables.table.comment.${val}`)}

)} {valIdx === 0 && (row.sub_values || []).length > 0 && ( <>     setInternalExpand(!internalExpand)} > {internalExpand ? t('diagnosis.fold') : t('diagnosis.expand')} )} ))} {(row.sub_values || []).map((subVals, subValsIdx) => ( {subVals.map((subVal, subValIdx) => ( {subValIdx === 0 && '|-- '} {showOthers(subVal)} ))} ))} ) } type Props = { diagnosis: TableDef } export default function DiagnosisTable({ diagnosis }: Props) { const { category, title, column, rows } = diagnosis const { t } = useTranslation() return (
{(category || []).map((c, idx) => (

{c && t(`diagnosis.tables.category.${c}`)}

))}

{t(`diagnosis.tables.title.${title}`)}

{t(`diagnosis.tables.comment.${title}`, '')} {column.map((col, colIdx) => ( ))} {(rows || []).map((row, rowIdx) => ( ))}
{col}
) } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/diagnoseReportApp/index.css ================================================ .report-container { margin-bottom: 16px; } tr.subvalues { background-color: lightcyan; } tr.subvalues.fold { display: none; } .subvalues-toggle { display: inline-block; width: 60px; cursor: pointer; color: #2160c4; } .actions { padding: 8px 0; background-color: white; position: sticky; top: 0; z-index: 2; } .table-header-row { position: sticky; top: 55px; z-index: 1; background-color: #888; color: white !important; } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/diagnoseReportApp/index.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom' import 'bulma/css/bulma.css' import '@fortawesome/fontawesome-free/js/all.js' import { i18n, distro } from '@pingcap/tidb-dashboard-lib' import DiagnosisReport from './components/DiagnosisReport' import translations from './translations' // for update distro strings resource import '~/utils/distro/stringsRes' import './index.css' function refineDiagnosisData() { const diagnosisData = window.__diagnosis_data__ || [] let preCategory = '' diagnosisData.forEach((d) => { if (d.category.join('') === preCategory) { d.category = [] } else { preCategory = d.category.join('') } }) return diagnosisData } i18n.addTranslations(translations) document.title = `${distro().tidb} Dashboard Diagnosis Report` ReactDOM.render( , document.getElementById('root') ) ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/diagnoseReportApp/react-app-env.d.ts ================================================ /// // https://stackoverflow.com/questions/12709074/how-do-you-explicitly-set-a-new-property-on-window-in-typescript interface Window { __diagnosis_data__: any } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/diagnoseReportApp/translations/en.yaml ================================================ diagnosis: title: '{{distro.tidb}} Cluster System Report' expand_all: Expand All fold_all: Collapse All expand: Expand fold: Collapse all_tables: Report Overview tables: category: header: Basic Info diagnose: Diagnose load: Load Info overview: Component Info TiDB: '{{distro.tidb}} Component' PD: '{{distro.pd}} Component' TiKV: '{{distro.tikv}} Component' config: Configuration Info error: Error Info title: compare_diagnose: Diagnostics Comparison compare_report_time_range: Comparison Time Range top_10_slow_query_in_time_range_t1: Top 10 Slow Queries in Time Range t1 top_10_slow_query_in_time_range_t2: top 10 Slow Queries in Time Range t2 top_10_slow_query_group_by_digest_in_time_range_t1: Top 10 Slow Queries Group by Digest in Time Range t1 top_10_slow_query_group_by_digest_in_time_range_t2: Top 10 slow queries group by digest in Time Range t2 slow_query_with_diff_plan in_in_time_range_t1: Slow Queries with Different Plan in Time Range t1 slow_query_with_diff_plan in_in_time_range_t2: Slow queries with Different Plan in Time Range t2 diagnose_in_time_range_t1: Diagnostics in Time Range t1 diagnose_in_time_range_t2: Diagnostics in Time Range t2 max_diff_item: Maximum Different Item slow_query_t2: Slow Queries In Time Range t2 generate_report_error: Report Generation Error report_time_range: Report Time Range diagnose: Diagnosis Result total_time_consume: Time Consumed by Each Component total_error: Errors Occurred in Each Component time_consume: Time Consumed tidb_time_consume: Time Consumed by {{distro.tidb}} Component transaction: '{{distro.tidb}} Transaction' tidb_connection_count: '{{distro.tidb}} Server Connections' statistics_info: Statistics Info ddl_owner: DDL Owner scheduler_initial_config: Scheduler Initial Config scheduler_change_config: Scheduler Config Change History tidb_gc_initial_config: '{{distro.tidb}} GC Initial Config' tidb_gc_change_config: '{{distro.tidb}} GC Config Change History' tikv_rocksdb_initial_config: '{{distro.tikv}} RocksDB Initial Config' tikv_rocksdb_change_config: '{{distro.tikv}} RocksDB Config Change History' tikv_raftstore_initial_config: '{{distro.tikv}} RaftStore Initial Config' tikv_raftstore_change_config: '{{distro.tikv}} RaftStore Config Change History' pd_time_consume: Time Consumed by {{distro.pd}} Component balance_leader_region: Scheduled Leader/Region Count approximate_region_size: Approximate Region Size tikv_engine_size: '{{distro.tikv}} Engine Size' tikv_time_consume: Time Consumed by {{distro.tikv}} Component scheduler_info: Scheduler Info gc_info: GC Info task_info: Task Info snapshot_info: Snapshot Info coprocessor_info: Coprocessor Info raft_info: Raft Info tikv_error: '{{distro.tikv}} Error' tidb_current_config: "{{distro.tidb}}'s Current Config" pd_current_config: "{{distro.pd}}'s Current Config" tikv_current_config: "{{distro.tikv}}'s Current Config" node_load_info: Server Load Info process_cpu_usage: Instance CPU Usage process_memory_usage: Instance Memory Usage tidb/pd_goroutines_count: '{{distro.tidb}}/{{distro.pd}} Goroutines Count' tikv_thread_cpu_usage: '{{distro.tikv}} Thread CPU Usage' store_status: Store Status cluster_status: Cluster Status etcd_status: etcd Status cluster_info: Cluster Topology Info cache_hit: Cache Hit cluster_hardware: Cluster Hardware Info rocksdb_time_consume: Time Consumed by RocksDB top_10_slow_query: Top 10 Slow Queries top_10_slow_query_group_by_digest: Top 10 Slow Queries Group By Digest slow_query_with_diff_plan: Slow Queries with Different Plan comment: compare_diagnose: Automatically diagnose the cluster problem by comparing with the reference time. max_diff_item: The maximum different metrics between two time ranges. diagnose: Automatically diagnose the cluster problem and record the problem in the table below. total_time_consume: This table contains the event time consumed in {{distro.tidb}}/{{distro.tikv}}/{{distro.pd}}. METRIC_NAME is the event name; LABEL is the event label, such as instance, event type, etc; TIME_RATIO is the TOTAL_TIME of this event divided by the TOTAL_TIME of the events whose TIME_RATIO is 1; TOTAL_TIME is the total time cost of this event; TOTAL_COUNT is the total count of this event; P999 is the max time of 0.999 quantile; P99 is the max time of 0.99 quantile; P90 is the max time of 0.90 quantile; P80 is the max time of 0.80 quantile. total_error: This table contains the total count of each error event. METRIC_NAME is the error event name; LABEL is the event label, such as instance, event type, etc; TOTAL_COUNT is the total count of this event. tidb_time_consume: This table contains the event time consumed in {{distro.tidb}}. METRIC_NAME is the event name; LABEL is the event label, such as instance, event type, etc; TIME_RATIO is the TOTAL_TIME of this event divided by the TOTAL_TIME of the events whose TIME_RATIO is 1; TOTAL_TIME is the total time cost of this event; TOTAL_COUNT is the total count of this event; P999 is the max time of 0.999 quantile; P99 is the max time of 0.99 quantile; P90 is the max time of 0.90 quantile; P80 is the max time of 0.80 quantile. transaction: This table contains the {{distro.tidb}} transaction statistics information. METRIC_NAME is the object name; LABEL is the object label, such as instance, event type, etc; TOTAL_VALUE is the total size/value of this object; TOTAL_COUNT is the total count of this object; P999 is the max size/value of 0.999 quantile; P99 is the max size/value of 0.99 quantile; P90 is the max size/value of 0.90 quantile; P80 is the max size/value of 0.80 quantile. tidb_connection_count: The number of connections of each {{distro.tidb}} server. ddl_owner: This table contains the DDL Owner info. Note that if no DDL request has been executed, the Owner info maybe null below, but it doesn't indicate that no DDL Owner exists. scheduler_initial_config: The initial config value of {{distro.pd}} scheduler. The initial time is the start time of this report. scheduler_change_config: The config change history of {{distro.pd}} scheduler. APPROXIMATE_CHANGE_TIME is the minimum start effective time. tidb_gc_initial_config: The initial config value of {{distro.tidb}} GC. The initial time is the start time of this report. tidb_gc_change_config: The config change history of {{distro.tidb}} GC. APPROXIMATE_CHANGE_TIME is the minimum start effective time. tikv_rocksdb_initial_config: The initial config value of {{distro.tikv}} RocksDB. The initial time is the start time of this report. tikv_rocksdb_change_config: The config change history of {{distro.tikv}} RocksDB. APPROXIMATE_CHANGE_TIME is the minimum start effective time. tikv_raftstore_initial_config: The initial config value of {{distro.tikv}} RaftStore. The initial time is the start time of this report. tikv_raftstore_change_config: The config change history of {{distro.tikv}} RaftStore. APPROXIMATE_CHANGE_TIME is the minimum start effective time. pd_time_consume: This table contains the event time consumed in {{distro.pd}}. METRIC_NAME is the event name; LABEL is the event label, such as instance, event type, etc; TIME_RATIO is the TOTAL_TIME of this event divided by the TOTAL_TIME of the events whose TIME_RATIO is 1; TOTAL_TIME is the total time cost of this event; TOTAL_COUNT is the total count of this event; P999 is the max time of 0.999 quantile; P99 is the max time of 0.99 quantile; P90 is the max time of 0.90 quantile; P80 is the max time of 0.80 quantile. tikv_time_consume: This table contains the event time consumed in {{distro.tikv}}. METRIC_NAME is the event name; LABEL is the event label, such as instance, event type, etc; TIME_RATIO is the TOTAL_TIME of this event divided by the TOTAL_TIME of the events whose TIME_RATIO is 1; TOTAL_TIME is the total time cost of this event; TOTAL_COUNT is the total count of this event; P999 is the max time of 0.999 quantile; P99 is the max time of 0.99 quantile; P90 is the max time of 0.90 quantile; P80 is the max time of 0.80 quantile. table: name: tidb_transaction: Transaction tidb_kv_request: KV request tidb_slow_query: Slow query tidb_ddl_handle_job: DDL job tidb_ddl_batch_add_index: Batch add index tidb_load_schema: Schema load tidb_meta_operation: '{{distro.tidb}} meta operation' tidb_auto_id_request: '{{distro.tidb}} auto ID request' tidb_statistics_auto_analyze: '{{distro.tidb}} auto analyze' tidb_gc: '{{distro.tidb}} GC' pd_client_cmd: '{{distro.pd}} client cmd' pd_handle_request: '{{distro.pd}} request' pd_handle_transactions: etcd transactions tikv_cop_request: Coprocessor request tikv_cop_handle: Coprocessor handling request tikv_handle_snapshot: Snapshot handling tikv_send_snapshot: Snapshot sending tikv_commit_log: Raft commit log tidb_transaction_retry_num: '{{distro.tidb}} transaction retry' tidb_txn_region_num: Transaction Region count tidb_txn_kv_write_num: Transaction KV write count tidb_txn_kv_write_size: Transaction KV write size tidb_load_safepoint_total_num: Safepoint load tikv_scheduler_stage_total_num: Scheduler stage tikv_worker_handled_tasks_total_num: '{{distro.tikv}} worker handled tasks' tikv_worker_pending_tasks_total_num: '{{distro.tikv}} worker pending tasks' tikv_futurepool_handled_tasks_total_num: future_pool handled tasks tikv_futurepool_pending_tasks_total_num: future_pool pending tasks tikv_snapshot_kv_count: Snapshot KV tikv_snapshot_size: Snapshot size tikv_cop_scan_keys_num: '{{distro.tikv}} Coprocessor scan keys' tikv_cop_total_response_total_size: '{{distro.tikv}} Coprocessor response' tikv_cop_scan_num: '{{distro.tikv}} Coprocessor scan' tikv_raft_sent_messages_total_num: Raft sent messages tikv_flush_messages_total_num: Raft flush messages tikv_receive_messages_total_num: Raft receive messages tikv_raft_dropped_messages_total: Raft dropped messages tikv_raft_proposals_total_num: Raft proposals tikv_grpc_error_total_count: gRPC errors tikv_critical_error_total_count: '{{distro.tikv}} critical errors' tikv_coprocessor_request_error_total_count: Coprocessor request errors node_disk_write_latency: Disk write latency node_disk_read_latency: Disk read latency sched_worker: Scheduler worker tikv_memtable_hit: memtable hit tikv_block_all_cache_hit: All block cache hit tikv_block_index_cache_hit: Index block cache hit tikv_block_filter_cache_hit: Filter block cache hit tikv_block_data_cache_hit: Data block cache hit tikv_block_bloom_prefix_cache_hit: Bloom prefix block cache hit comment: tidb_query: The time cost of SQL queries. The label is [sql_type]. tidb_get_token(us): The time cost of a session getting token to execute the SQL query. The label is [instance]. tidb_parse: The time cost of parsing SQL queries. The label is [sql_type]. tidb_compile: The time cost of building the query plan. The label is [sql_type]. tidb_execute: The time cost of executing the SQL query, which does not include the time to get the results of the query. The label is [sql_type]. tidb_distsql_execution: The time cost of distsql execution. The label is [type]. tidb_cop: The processing time of KV storage Coprocessor. The label is [instance]. tidb_transaction: The time cost of a transaction executing durations, including retry. The label is [sql_type]. tidb_transaction_local_latch_wait: The time cost of waiting for local latch. The label is [instance]. tidb_kv_backoff: The time cost of {{distro.tidb}} transaction latch waiting for key value storage. The label is [type]. tidb_kv_request: The time cost of KV requests durations. The label is [type]. tidb_slow_query: The time cost of {{distro.tidb}} slow queries. The label is [instance]. tidb_slow_query_cop_process: The total Coprocessor processing time of {{distro.tidb}} slow queries. The label is [instance]. tidb_slow_query_cop_wait: The total Coprocessor waiting time of {{distro.tidb}} slow queries. The label is [instance]. tidb_ddl_handle_job: The time cost of processing {{distro.tidb}} DDL jobs. The label is [type]. tidb_ddl_worker: The time cost of DDL worker handling jobs. The label is [action]. tidb_ddl_update_self_version: The time cost of updating {{distro.tidb}} schema syncer version. The label is [result]. tidb_owner_handle_syncer: The time cost of {{distro.tidb}} DDL owner operations on etcd. The label is [type]. tidb_ddl_batch_add_index: The time cost of {{distro.tidb}} batch adding index. The label is [type]. tidb_ddl_deploy_syncer: The time cost of {{distro.tidb}} DDL schema syncer statistics, including init, start, watch, and clear. The label is [type]. tidb_load_schema: The time cost of {{distro.tidb}} loading schema. The label is [type]. tidb_meta_operation: The time cost of {{distro.tidb}} meta operations, including get/set schema and DDL jobs. The label is [instance]. tidb_auto_id_request: The time cost of handling requests for {{distro.tidb}} auto ID. The label is [type]. tidb_statistics_auto_analyze: The time cost of {{distro.tidb}} auto analyze. The label is [type]. tidb_gc: The time cost of KV storage garbage collection. The label is [instance]. tidb_gc_push_task: The time cost of KV storage range worker processing one task. The label is [instance]. tidb_batch_client_unavailable: The time cost of KV storage batch processing unavailable. The label is [type]. tidb_batch_client_wait: The time cost of {{distro.tidb}} KV storage batch processing client requests that are waiting. The label is [instance]. pd_start_tso_wait: The time cost of waiting for the start timestamp oracle. The label is [instance]. pd_tso_rpc: The time cost from sending TSO request to receiving the response. The label is [instance]. pd_tso_wait: The time cost from the client starting to wait for the timestamp to receiving the timestamp. The label is [instance]. pd_client_cmd: The time cost of {{distro.pd}} client command. The label is [type]. pd_handle_request: The time cost of {{distro.pd}} handling request. The label is [type]. pd_grpc_completed_commands: The time cost of {{distro.pd}} completing each kind of gRPC commands. The label is [grpc_method]. pd_operator_finish: The time cost of {{distro.pd}} completing each kind of scheduling commands. The label is [type]. pd_operator_step_finish: The time cost of {{distro.pd}} completing operating steps. The label is [type]. pd_handle_transactions: The time cost of {{distro.pd}} handling etcd transactions. The label is [result]. pd_region_heartbeat: The time cost of heartbeats in each {{distro.tikv}} instance. The label is [address]. etcd_wal_fsync: The time cost of etcd writing WAL into the persistent storage. The label is [instance]. pd_peer_round_trip: The latency of the network. The label is [To]. tikv_grpc_message: The time cost of handling {{distro.tikv}} gRPC messages. The label is [type]. tikv_cop_request: The time cost of Coprocessor handling read requests. The label is [req]. tikv_cop_handle: The time cost of handling Coprocessor requests. The label is [req]. tikv_cop_wait: The time cost of Coprocessor requests that wait for being handled. The label is [req]. tikv_scheduler_command: The time cost of executing commit command. The label is [type]. tikv_scheduler_latch_wait: The waiting time of {{distro.tikv}} latch in commit command. The label is [type]. tikv_handle_snapshot: The time cost of handling snapshots. The label is [type]. tikv_send_snapshot: The time cost of sending snapshots. The label is [instance]. tikv_storage_async_request: The time cost of processing asynchronous snapshot requests. The label is [type]. tikv_raft_append_log: The time cost of Raft appends log. The label is [instance]. tikv_raft_apply_log: The time cost of Raft apply log. The label is [instance]. tikv_raft_apply_wait: The time cost of Raft apply wait. The label is [instance]. tikv_raft_process: The time cost of peer processes in Raft. The label is [instance]. tikv_raft_propose_wait: The waiting time of each proposal. The label is [type]. tikv_raft_store_events: The time cost of raftstore events. The label is [type]. tikv_commit_log: The time cost of Raft commits log. The label is [instance]. tikv_check_split: The time cost of running split check. The label is [instance]. tikv_ingest_sst: The time cost of ingesting SST files. The label is [instance]. tikv_gc_tasks: The time cost of executing GC tasks. The label is [task]. tikv_pd_request: The time cost of {{distro.tikv}} sending requests to {{distro.pd}}. The label is [type]. tikv_lock_manager_deadlock_detect: tikv_lock_manager_waiter_lifetime: tikv_backup_range: tikv_backup: tidb_transaction_retry_num: '{{distro.tidb}} transaction retry count. The label is [instance].' tidb_transaction_statement_num: The total number of {{distro.tidb}} statements within a transaction. Internal means the internal transaction of {{distro.tidb}}. The label is [sql_type]. tidb_txn_region_num: The number of Regions that each transaction operates. The label is [instance]. tidb_txn_kv_write_num: The number of KV writes per transaction execution. The label is [instance]. tidb_txn_kv_write_size: The KV write size per transaction execution. The label is [instance]. tidb_load_safepoint_total_num: The total count of safe point loading. The label is [instance]. tidb_lock_resolver_total_num: The total count of lock resolve. The label is [instance]. pseudo_estimation_total_count: The total count of {{distro.tidb}} Optimizer using pseudo estimation. The label is [instance, type]. dump_feedback_total_count: The total count of operations that {{distro.tidb}} dumping statistics back to KV storage. The label is [instance, type]. store_query_feedback_total_count: The total count of {{distro.tidb}} store querying feedback. The label is [instance, type]. update_stats_total_count: The total count of {{distro.tidb}} updating statistics using feed back. The label is [instance]. balance-leader-in: balance-leader-in is the total count of Leader moving into the {{distro.tikv}} store. The label is [address]. balance-leader-out: balance-leader-out is the total count of Leader moving out of the {{distro.tikv}} store. The label is [address]. balance-region-in: balance-region-in is the total count of Regions moving into the {{distro.tikv}} store. The label is [address]. balance-region-out: balance-region-in is the total count of Regions moving into the {{distro.tikv}} store. The label is [address]. Approximate Region size: The approximate Region size. The label is [instance]. store size: The storage size. The label is [instance, type]. tikv_scheduler_keys_read: The number of keys read by a command. The label is [instance, type]. tikv_scheduler_keys_written: The number of keys written by a command. The label is [instance, type]. tikv_scheduler_scan_details_total_num: The keys scan details of each CF when executing a command. The label is [instance,req,tag]. tikv_scheduler_stage_total_num: The total number of scheduler states. The label is [instance,type,stage]. tikv_gc_keys_total_num: The total number of keys in CF affected during GC. The label is [instance,cf,tag]. tidb_gc_worker_action_total_num: The total count of KV storage garbage collection. The label is [instance,type]. tikv_worker_handled_tasks_total_num: The total number of tasks handled by worker. The label is [instance,name]. tikv_worker_pending_tasks_total_num: The total number of pending and running tasks of worker. The label is [instance,name]. tikv_futurepool_handled_tasks_total_num: The total number of tasks handled by future_pool. The label is [instance,name]. tikv_futurepool_pending_tasks_total_num: The total number of pending and running tasks of future_pool. The label is [instance,name]. tikv_snapshot_kv_count: tikv_snapshot_kv_count. The label is [instance]. tikv_snapshot_size: The number of KV pairs within a snapshot. The label is [instance]. tikv_snapshot_state_total_count: tikv_snapshot_size. The label is [instance,type]. tikv_cop_scan_keys_num: The total number of {{distro.tikv}} Coprocessor scan keys. The label is [instance,req]. tikv_cop_total_response_total_size: '{{distro.tikv}} coprocessor response total size. The label is [instance].' tikv_cop_scan_num: The total number of {{distro.tikv}} coprocessor scan operations. The label is [instance,req,tag,cf]. tikv_raft_sent_messages_total_num: The total number of sent Raft messages. The label is [instance,type]. tikv_flush_messages_total_num: The total number of flushed Raft messages. The label is [instance]. tikv_receive_messages_total_num: The total number of received Raft messages. The label is [instance]. tikv_raft_dropped_messages_total: The total number of dropped Raft messages. The label is [instance,type]. tikv_raft_proposals_total_num: The total number of raft proposals. The label is [instance,type]. tikv_grpc_error_total_count: The total number of the gRPC message failures. The label is [instance,type]. tikv_critical_error_total_count: The total number of the {{distro.tikv}} critical errors. The label is [instance,type]. tikv_scheduler_is_busy_total_count: The total number of Scheduler Busy events that make the {{distro.tikv}} instance temporarily unavailable. The label is [instance,db,type,stage]. tikv_channel_full_total_count: The total number of channel full errors, which will make the {{distro.tikv}} instance temporarily unavailable. The label is [instance,db,type]. tikv_coprocessor_request_error_total_count: The total number of Coprocessor errors. The label is [instance,reason]. tikv_engine_write_stall: Indicates occurrences of Write Stall events that make the {{distro.tikv}} instance temporarily unavailable. The label is [instance,db]. tikv_server_report_failures_total_count: The total number of reported failure messages. The label is [instance]. tikv_storage_async_request_error: The total number of storage request errors. The label is [instance,status,type]. tikv_lock_manager_detect_error_total_count: The total number of {{distro.tikv}} lock manager detect error. The label is [instance,type]. tikv_backup_errors_total_count: The total number of {{distro.tikv}} lock manager detected errors. The label is [instance,error]. node_disk_write_latency: The disk write latency in each node. The label is [instance,device]. node_disk_read_latency: The disk read latency in each node. The label is [instance,device]. grpc: The CPU utilization of each {{distro.tikv}} gRPC. The label is [instance]. raftstore: The CPU utilization of {{distro.tikv}} raftstore thread. The label is [instance]. Async apply: The CPU utilization of {{distro.tikv}} async apply thread. The label is [instance]. sched_worker: The CPU utilization of {{distro.tikv}} scheduler worker thread. The label is [instance]. snapshot: The CPU utilization of {{distro.tikv}} snapshot. The label is [instance]. unified read pool: The CPU utilization of {{distro.tikv}} unified read pool thread. The label is [instance]. storage read pool: The CPU utilization of {{distro.tikv}} storage read pool thread. The label is [instance]. storage read pool normal: The CPU utilization of {{distro.tikv}} storage read pool normal thread. The label is [instance]. storage read pool high: The CPU utilization of {{distro.tikv}} storage read pool high thread. The label is [instance]. storage read pool low: The CPU utilization of {{distro.tikv}} storage read pool low thread. The label is [instance]. cop: The CPU utilization of {{distro.tikv}} Coprocessor. The label is [instance]. cop normal: The CPU utilization of {{distro.tikv}} Coprocessor normal thread. The label is [instance]. cop high: The CPU utilization of {{distro.tikv}} Coprocessor high thread. The label is [instance]. cop low: The CPU utilization of {{distro.tikv}} Coprocessor low thread. The label is [instance]. rocksdb: The CPU utilization {{distro.tikv}} RocksDB. The label is [instance]. gc: The CPU utilization of {{distro.tikv}} GC. The label is [instance]. split_check: The CPU utilization of {{distro.tikv}} split_check. The label is [instance]. region_score: The Region score of store. The label is [address]. leader_score: The Leader score of store. The label is [address]. region_count: The Region count of store. The label is [address]. leader_count: The Leader score of store. The label is [address]. region_size: The Region size of store. The label is [address]. leader_size: The Leader size of store. The label is [address]. tikv_memtable_hit: The hit rate of memtable. The label is [instance]. tikv_block_all_cache_hit: The hit rate of all block cache. The label is [instance]. tikv_block_index_cache_hit: The hit rate of index block cache. The label is [instance]. tikv_block_filter_cache_hit: The hit rate of filter block cache. The label is [instance]. tikv_block_data_cache_hit: The hit rate of data block cache. The label is [instance]. tikv_block_bloom_prefix_cache_hit: The hit rate of bloom_prefix block cache. The label is [instance]. get duration: The time consumed when RocksDB executing get operations. The label is [instance]. seek duration: The time consumed when RocksDB executing seek operations. The label is [instance]. write duration: The time consumed when RocksDB executing write operations. The label is [instance]. WAL sync duration: The time consumed when RocksDB executing WAL sync operations. The label is [instance]. compaction duration: The time consumed when RocksDB executing compaction operations. The label is [instance]. SST read duration: The time consumed when RocksDB reading SST files. The label is [instance]. write stall duration: The time cost of write stall. The label is [instance]. ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/diagnoseReportApp/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/diagnoseReportApp/translations/zh.yaml ================================================ diagnosis: title: '{{distro.tidb}} 集群系统报告' expand_all: 展开所有 fold_all: 收起所有 expand: 展开 fold: 收起 all_tables: 报告信息总览 tables: category: header: 基本信息 diagnose: 诊断 load: 负载 overview: 各组件信息总览 TiDB: '{{distro.tidb}} 组件' PD: '{{distro.pd}} 组件' TiKV: '{{distro.tikv}} 组件' config: 配置 error: 错误 title: compare_diagnose: 诊断对比 compare_report_time_range: 对比报告区间 top_10_slow_query_in_time_range_t1: t1 中的 Top 10 慢查询 top_10_slow_query_in_time_range_t2: t2 中的 Top 10 慢查询 top_10_slow_query_group_by_digest_in_time_range_t1: 按 SQL 指纹聚合的 t1 Top 10 慢查询 top_10_slow_query_group_by_digest_in_time_range_t2: 按 SQL 指纹聚合的 t2 Top 10 慢查询 slow_query_with_diff_plan_in_time_range_t1: t1 中的不同执行计划的慢查询 slow_query_with_diff_plan_in_time_range_t2: t2 中的不同执行计划的慢查询 diagnose_in_time_range_t1: t1 中的诊断信息 diagnose_in_time_range_t2: t2 中的诊断信息 max_diff_item: 最大不同项 slow_query_t2: t2 中的慢查询 generate_report_error: 生成报告的报错 report_time_range: 报告区间 diagnose: 诊断结果 total_time_consume: 各组件总耗时 total_error: 各组件总报错数 time_consume: 耗时 tidb_time_consume: '{{distro.tidb}} 中事件耗时' transaction: '{{distro.tidb}} 事务' tidb_connection_count: '{{distro.tidb}} 连接数' statistics_info: 统计信息 ddl_owner: DDL Owner scheduler_initial_config: 调度器初始配置 scheduler_change_config: 调度器配置修改历史 tidb_gc_initial_config: '{{distro.tidb}} GC 初始配置' tidb_gc_change_config: '{{distro.tidb}} GC 配置修改历史' tikv_rocksdb_initial_config: '{{distro.tikv}} RocksDB 初始配置' tikv_rocksdb_change_config: '{{distro.tikv}} RocksDB 配置修改历史' tikv_raftstore_initial_config: '{{distro.tikv}} RaftStore 初始配置' tikv_raftstore_change_config: '{{distro.tikv}} RaftStore 配置修改历史' pd_time_consume: '{{distro.pd}} 中事件耗时' balance_leader_region: Leader/Region 调度数 approximate_region_size: Approximate Region 大小 tikv_engine_size: '{{distro.tikv}} 实例存储大小' tikv_time_consume: '{{distro.tikv}} 中事件耗时' scheduler_info: '{{distro.tikv}} 调度器信息' gc_info: GC 信息 task_info: '{{distro.tikv}} 任务信息' snapshot_info: '{{distro.tikv}} 快照信息' coprocessor_info: Coprocessor 信息 raft_info: Raft 信息 tikv_error: '{{distro.tikv}} 错误' tidb_current_config: '{{distro.tidb}} 当前配置' pd_current_config: '{{distro.pd}} 当前配置' tikv_current_config: '{{distro.tikv}} 当前配置' node_load_info: 服务器负载信息 process_cpu_usage: 各实例 CPU 使用率 process_memory_usage: 各实例内存消耗 tidb/pd_goroutines_count: '{{distro.tidb}}/{{distro.pd}} 的 Goroutines 数量' tikv_thread_cpu_usage: '{{distro.tikv}} 的 CPU 使用情况' store_status: '{{distro.tikv}} 节点的存储状态' cluster_status: 集群状态 etcd_status: etcd 状态 cluster_info: 集群拓扑信息 cache_hit: 缓存命中率 cluster_hardware: 集群硬件信息 rocksdb_time_consume: RocksDB 事件耗时 top_10_slow_query: Top 10 慢查询 top_10_slow_query_group_by_digest: 按 SQL 指纹聚合的 Top 10 慢查询 slow_query_with_diff_plan: 不同执行计划的慢查询 comment: compare_diagnose: 通过与参考时间的比较,自动诊断集群问题。 max_diff_item: 两段时间中的最大不同项。 diagnose: 该表显示的是自动诊断的结果,即集群中出现的问题。 total_time_consume: 该表显示的是 {{distro.tidb}}/{{distro.tikv}}/{{distro.pd}} 组件中各事件的耗时。METRIC_NAME 是事件名称;LABEL 是事件标签,如实例、事件类型等;TIME_RATIO 是该事件的总时间除以 TIME_RATIO 为 1 的事件的总时间;TOTAL_TIME 是该事件的总耗时;TOTAL_COUNT 是该事件的总计数;P999 是 0.999 分位数的最大时间;P99 是 0.99 分位数的最大时间;P90 是 0.90 分位数的最大时间;P80 是 0.80 分位数的最大时间。 total_error: 该表显示的是各错误事件的数量。METRIC_NAME 是错误事件名称;LABEL 是事件标签,如实例、事件类型;TOTAL_COUNT 是该错误事件的总数。 tidb_time_consume: 该表显示的是 {{distro.tidb}} 组件中各事件的耗时。METRIC_NAME 是事件名称;LABEL 是事件标签,如实例、事件类型等;TIME_RATIO 是该事件的总时间除以 TIME_RATIO 为 1 的事件的总时间;TOTAL_TIME 是该事件的总耗时;TOTAL_COUNT 是该事件的总计数;P999 是 0.999 分位数的最大时间;P99 是 0.99 分位数的最大时间;P90 是 0.90 分位数的最大时间;P80 是 0.80 分位数的最大时间。 transaction: 该表显示了 {{distro.tidb}} 事务的统计信息。METRIC_NAME 是对象名;LABEL 是对象标签,如实例、事件类型等;TOTAL_VALUE 是该对象的总大小;TOTAL_COUNT 是该对象的总计数;P999 为 0.999 分位数的最大值;P99 是 0.99 分位数的最大值;P90 是 0.90 分位数的最大值;P80 是 0.80 分位数的最大值。 tidb_connection_count: '{{distro.tidb}} 服务器的连接数。' ddl_owner: DDL Owner 的信息。注意:如果没有 DDL 请求被执行,下面的 Owner 信息可能为空,这并不表示 DDL Owner 不存在。 scheduler_initial_config: '{{distro.pd}} 调度器的初始配置值。初始时间是报表的开始时间。' scheduler_change_config: '{{distro.pd}} 调度器的配置更改历史。APPROXIMATE_CHANGE_TIME 为最近的有效更改时间。' tidb_gc_initial_config: '{{distro.tidb}} GC 的初始配置值。初始时间是报表的开始时间。' tidb_gc_change_config: '{{distro.tidb}} GC 的配置更改历史。APPROXIMATE_CHANGE_TIME 为最近的有效更改时间。' tikv_rocksdb_initial_config: '{{distro.tikv}} RocksDB 的初始配置值。初始时间是报表的开始时间。' tikv_rocksdb_change_config: '{{distro.tikv}} RocksDB 的配置更改历史。APPROXIMATE_CHANGE_TIME 为最近的有效更改时间。' tikv_raftstore_initial_config: '{{distro.tikv}} RaftStore 的初始配置值。初始时间是报表的开始时间。' tikv_raftstore_change_config: '{{distro.tikv}} RaftStore 的配置更改历史。APPROXIMATE_CHANGE_TIME 为最近的有效更改时间。' pd_time_consume: 该表显示的是 {{distro.pd}} 组件中各事件的耗时。METRIC_NAME 是事件名称;LABEL 是事件标签,如实例、事件类型等;TIME_RATIO 是该事件的总时间除以 TIME_RATIO 为 1 的事件的总时间;TOTAL_TIME 是该事件的总耗时;TOTAL_COUNT 是该事件的总计数;P999 是 0.999 分位数的最大时间;P99 是 0.99 分位数的最大时间;P90 是 0.90 分位数的最大时间;P80 是 0.80 分位数的最大时间。 tikv_time_consume: 该表显示的是 {{distro.tikv}} 组件中各事件的耗时。METRIC_NAME 是事件名称;LABEL 是事件标签,如实例、事件类型等;TIME_RATIO 是该事件的总时间除以 TIME_RATIO 为 1 的事件的总时间;TOTAL_TIME 是该事件的总耗时;TOTAL_COUNT 是该事件的总计数;P999 是 0.999 分位数的最大时间;P99 是 0.99 分位数的最大时间;P90 是 0.90 分位数的最大时间;P80 是 0.80 分位数的最大时间。 table: name: tidb_transaction: '{{distro.tidb}} 事务' tidb_kv_request: '{{distro.tidb}} KV 请求' tidb_slow_query: 慢查询 tidb_ddl_handle_job: DDL 任务 tidb_ddl_batch_add_index: 批量索引添加 tidb_load_schema: Schema 加载 tidb_meta_operation: '{{distro.tidb}} 元操作' tidb_auto_id_request: '{{distro.tidb}} 自增 ID 请求' tidb_statistics_auto_analyze: '{{distro.tidb}} 自动分析' tidb_gc: 垃圾回收 pd_client_cmd: '{{distro.pd}} 客户端命令' pd_handle_request: '{{distro.pd}} 请求' pd_handle_transactions: etcd 事务 pd_peer_round_trip: 网络延迟 tikv_cop_request: Coprocessor 读请求 tikv_cop_handle: Coprocessor 请求 tikv_handle_snapshot: 快照处理 tikv_send_snapshot: 快照发送 tikv_commit_log: Raft 提交日志 tidb_transaction_retry_num: '{{distro.tidb}} 事务重试数' tidb_txn_region_num: 事务操作的 Region 数量 tidb_txn_kv_write_num: 事务执行的 KV 写入数量 tidb_txn_kv_write_size: 事务执行的 KV 写入大小 tidb_load_safepoint_total_num: 安全点装载总数量 tikv_scheduler_stage_total_num: 调度程序状态的总数量 tikv_worker_handled_tasks_total_num: worker 处理的任务总数量 tikv_worker_pending_tasks_total_num: 工作进程的挂起和运行任务的总数量 tikv_futurepool_handled_tasks_total_num: future_pool 处理的任务总数量 tikv_futurepool_pending_tasks_total_num: future_pool 总挂起和运行任务数量 tikv_snapshot_kv_count: 快照的 KV 数量 tikv_snapshot_size: 快照大小 tikv_cop_scan_keys_num: '{{distro.tikv}} Coprocessor 扫描键总数量' tikv_cop_total_response_total_size: '{{distro.tikv}} Coprocessor 响应总大小' tikv_cop_scan_num: '{{distro.tikv}} Coprocessor 扫描操作总数量' tikv_raft_sent_messages_total_num: 发送的 Raft 消息的总数量 tikv_flush_messages_total_num: 持久化 Raft 消息的总数量 tikv_receive_messages_total_num: 接受 Raft 消息的总数量 tikv_raft_dropped_messages_total: 丢弃 Raft 消息的总数量 tikv_raft_proposals_total_num: Raft proposal 的总数量 tikv_grpc_error_total_count: gRPC 消息失败的总数量 tikv_critical_error_total_count: '{{distro.tikv}} 临界误差的总数量' tikv_coprocessor_request_error_total_count: Coprocessor 错误总数量 node_disk_write_latency: 磁盘写延迟 node_disk_read_latency: 磁盘读取延迟 sched_worker: 调度器工作线程 tikv_memtable_hit: memtable 命中率 tikv_block_all_cache_hit: 所有块缓存命中率 tikv_block_index_cache_hit: 索引块缓存命中率 tikv_block_filter_cache_hit: 过滤块缓存命中率 tikv_block_data_cache_hit: 数据块缓存命中率 tikv_block_bloom_prefix_cache_hit: bloom_prefix 块缓存命中率 comment: tidb_query: SQL 查询耗时,标签是"SQL 类型"。 tidb_get_token(us): 会话获取令牌以执行 SQL 查询的耗时,标签是"实例"。 tidb_parse: 解析 SQL 的耗时,标签是"SQL 类型"。 tidb_compile: 构建查询计划的时间,标签是"SQL 类型"。 tidb_execute: 执行 SQL 的时间,不包括获得查询结果的时间,标签是"SQL 类型"。 tidb_distsql_execution: 执行 distsql 的耗时,标签是"类型"。 tidb_cop: KV storage Coprocessor 处理的耗时,标签是"实例"。 tidb_transaction: 事务执行 durations 的时间成本,包括重试,标签是"SQL 类型"。 tidb_transaction_local_latch_wait: 事务执行时本地锁占用的时间,标签是"实例"。 tidb_kv_backoff: '{{distro.tidb}} 事务锁等待键值存储的时间,标签是"类型"。' tidb_kv_request: KV 请求 durations 的耗时,标签是"类型"。 tidb_slow_query: '{{distro.tidb}} 慢查询的时间开销,标签是"实例"。' tidb_slow_query_cop_process: '{{distro.tidb}} 的慢查询总 cop 处理的耗时,标签是"实例"。' tidb_slow_query_cop_wait: '{{distro.tidb}} 的慢查询总 cop 的等待时间,标签是"实例"。' tidb_ddl_handle_job: 处理 {{distro.tidb}} DDL 任务的耗时,标签是"类型"。 tidb_ddl_worker: DDL worker 处理任务的耗时,标签是"实例"。 tidb_ddl_update_self_version: '{{distro.tidb}} schema 同步器版本更新的耗时,标签是"结果"。' tidb_owner_handle_syncer: 在 etcd 上执行 {{distro.tidb}} DDL 所有者操作的耗时,标签是"类型"。 tidb_ddl_batch_add_index: '{{distro.tidb}} 批量添加索引的耗时,标签是"类型"。' tidb_ddl_deploy_syncer: '{{distro.tidb}} DDL schema 同步器统计的时间成本,包括 init、start、watch、clear,标签是"类型"。' tidb_load_schema: 加载 {{distro.tidb}} schema 的时间成本,标签是"类型"。 tidb_meta_operation: '{{distro.tidb}} 元操作的时间成本,包括 get/set 模式和 DDL 作业,标签是"实例"。' tidb_auto_id_request: '{{distro.tidb}} 自增 ID 处理 ID 请求的耗时,标签是"类型"。' tidb_statistics_auto_analyze: 自动分析 {{distro.tidb}} 的耗时,标签是"类型"。 tidb_gc: KV 存储垃圾回收的时间,标签是"实例"。 tidb_gc_push_task: KV 存储范围内 worker 处理一项任务的耗时,标签是"实例"。 tidb_batch_client_unavailable: KV 存储批量处理不可用的耗时,标签是"类型"。 tidb_batch_client_wait: KV 存储批量处理客户端等待请求的耗时,标签是"实例"。 pd_start_tso_wait: 等待获取开始时间戳 timestamp 的耗时,标签是"实例"。 pd_tso_rpc: 发送 TSO 请求直到收到响应的时间,标签是"实例"。 pd_tso_wait: 客户端开始等待 timestamp 直到收到 timestamp 结果的耗时,标签是"实例"。 pd_client_cmd: '{{distro.pd}} 客户端命令的耗时,标签是"类型"。' pd_handle_request: '{{distro.pd}} 处理请求的耗时,标签是"类型"。' pd_grpc_completed_commands: '{{distro.pd}} 完成各种 gRPC 命令的耗时,标签是"gRPC 方法"。' pd_operator_finish: '{{distro.pd}} 完成各种调度命令的时间,标签是"类型"。' pd_operator_step_finish: '{{distro.pd}} 完成操作步骤的耗时,标签是"类型"。' pd_handle_transactions: '{{distro.pd}} 处理 etcd 事务的耗时,标签是"结果"。' pd_region_heartbeat: 每个 {{distro.tikv}} 实例中心跳的耗时,标签是"服务地址"。 etcd_wal_fsync: etcd 将 WAL 写入持久存储器的耗时,标签是"实例"。 pd_peer_round_trip: 网络的延迟,标签是"实例"。 tikv_grpc_message: gRPC 报文的 {{distro.tikv}} 处理耗时,标签是"类型"。 tikv_cop_request: Coprocessor 处理读请求的时间开销,标签是"请求"。 tikv_cop_handle: 处理 Coprocessor 请求的时间开销,标签是"请求"。 tikv_cop_wait: Coprocessor 请求等待处理的耗时,标签是"请求"。 tikv_scheduler_command: 执行 commit 命令的耗时,标签是"类型"。 tikv_scheduler_latch_wait: 提交命令中 {{distro.tikv}} 锁存器等待的时间开销,标签是"类型"。 tikv_handle_snapshot: 处理快照的时间开销,标签是"类型"。 tikv_send_snapshot: 发送快照的时间开销,标签是"实例"。 tikv_storage_async_request: 处理异步快照请求的时间开销,标签是"类型"。 tikv_raft_append_log: Raft appends log 的时间开销,标签是"实例"。 tikv_raft_apply_log: Raft apply log 的时间开销,标签是"实例"。 tikv_raft_apply_wait: Raft apply wait 的时间开销,标签是"实例"。 tikv_raft_process: Peer processes in Raft 的时间开销,标签是"实例"。 tikv_raft_propose_wait: 每一个 Raft 提议的等待时间,标签是"类型"。 tikv_raft_store_events: RaftStore events 的时间开销,标签是"类型"。 tikv_commit_log: Raft 提交日志的时间开销,标签是"实例"。 tikv_check_split: 运行分割检查的耗时,标签是"实例"。 tikv_ingest_sst: Ingest SST 文件的耗时,标签是"实例"。 tikv_gc_tasks: 执行 GC 任务的耗时,标签是"任务"。 tikv_pd_request: '{{distro.tikv}} 向 {{distro.pd}} 发送请求的耗时,标签是"类型"。' tikv_lock_manager_deadlock_detect: tikv_lock_manager_waiter_lifetime: tikv_backup_range: tikv_backup: tidb_transaction_retry_num: '{{distro.tidb}} 事务重试次数,标签是"实例"。' tidb_transaction_statement_num: 一个事务中 {{distro.tidb}} 语句数的总数量。Internal 是指 {{distro.tidb}} 内部事务,标签是"实例"。' tidb_txn_region_num: 每个事务进行操作的区域数量,标签是"实例"。 tidb_txn_kv_write_num: 每个事务执行的 KV 写入数量,标签是"实例"。 tidb_txn_kv_write_size: 每个事务执行的 KV 写入大小,标签是"实例"。 tidb_load_safepoint_total_num: 安全点装载总数量,标签是"实例"。 tidb_lock_resolver_total_num: lock resolve 的总数量,标签是"实例"。 pseudo_estimation_total_count: 使用伪估计的 {{distro.tidb}} 优化器的总数量,标签是"实例","类型"。 dump_feedback_total_count: '{{distro.tidb}} 转储统计数据回 KV 存储的操作总数量,标签是"实例"。' store_query_feedback_total_count: '{{distro.tidb}} 存储查询反馈的总数量,标签是"实例"。' update_stats_total_count: 使用反馈更新统计数据的 {{distro.tidb}} 总数量,标签是"实例"。 blance-leader-in: Leader 移动到 {{distro.tikv}} 存储的总数量,标签是"实例"。 blance-leader-out: Leader 移出 {{distro.tikv}} 存储的总数量,标签是"实例"。 blance-region-in: 移动到 {{distro.tikv}} 存储的 Region 总数量,标签是"实例"。 blance-region-out: 移出 {{distro.tikv}} 存储的的 Region 总数量,标签是"实例"。 Approximate Region size: 近似 Region 大小,标签是"实例"。 store size: 存储大小,标签是"实例"。 tikv_scheduler_keys_read: 由一条命令读取的键数量,标签是"实例","类型"。 tikv_scheduler_keys_written: 由一条命令写入的键数量,标签是"实例","类型"。 tikv_scheduler_scan_details_total_num: 在执行一条命令时,扫描每个 CF 的详细信息的总数量,标签是"实例"。 tikv_scheduler_stage_total_num: 调度程序状态的总数量,标签是"实例","阶段","类型"。 tikv_gc_keys_total_num: GC 期间 CF 中受影响的键的总数量,标签是"实例"。 tidb_gc_worker_action_total_num: KV 存储垃圾回收总量,标签是"实例","类型"。 tikv_worker_handled_tasks_total_num: worker 处理的任务总数量,标签是"实例"。 tikv_worker_pending_tasks_total_num: 工作进程的挂起和运行任务的总数量,标签是"实例"。 tikv_futurepool_handled_tasks_total_num: future_pool 处理的任务总数量,标签是"实例"。 tikv_futurepool_pending_tasks_total_num: future_pool 的总挂起和运行任务,标签是"实例"。 tikv_snapshot_kv_count: tikv_snapshot_kv_count,标签是"实例"。 tikv_snapshot_size: 快照内 KV 的数量,标签是"实例"。 tikv_snapshot_state_total_count: '{{distro.tikv}} 的快照大小,标签是"实例","类型"。' tikv_cop_scan_keys_num: '{{distro.tikv}} Coprocessor 扫描键总数量,标签是"实例"。' tikv_cop_total_response_total_size: '{{distro.tikv}} Coprocessor 响应总大小,标签是"实例"。' tikv_cop_scan_num: '{{distro.tikv}} Coprocessor 扫描操作总数量,标签是"实例"。' tikv_raft_sent_messages_total_num: 发送的 Raft 消息的总数量,标签是"实例","类型"。 tikv_flush_messages_total_num: Raft 上刷新了的信息总数量,标签是"实例"。 tikv_receive_messages_total_num: Raft 收到的的信息总数量,标签是"实例"。 tikv_raft_dropped_messages_total: Raft 丢掉的的信息总数量,标签是"实例","类型"。 tikv_raft_proposals_total_num: Raft 提议的的总数量,标签是"实例","类型"。 tikv_grpc_error_total_count: gRPC 消息失败的总数量,标签是"实例","类型"。 tikv_critical_error_total_count: '{{distro.tikv}} 临界误差的总数量,标签是"实例","类型"。' tikv_scheduler_is_busy_total_count: 使 {{distro.tikv}} 实例暂时不可用的调度器繁忙事件的总数量,标签是"实例"。 tikv_channel_full_total_count: 通道完全错误的总数量,它将使 {{distro.tikv}} 实例暂时不可用,标签是"实例"。 tikv_coprocessor_request_error_total_count: Coprocessor 错误的总数量,标签是"实例","原因"。 tikv_engine_write_stall: 指示使 {{distro.tikv}} 实例暂时不可用的写失速事件,标签是"实例"。 tikv_server_report_failures_total_count: 报告失败消息的总数量,标签是"实例"。 tikv_storage_async_request_error: 存储请求错误的总数量,标签是"实例","状态","类型"。 tikv_lock_manager_detect_error_total_count: '{{distro.tikv}} 锁管理器检测错误的总数量,标签是"实例","类型"。' tikv_backup_errors_total_count: '{{distro.tikv}} 锁管理的总错误,标签是"实例","错误"。' node_disk_write_latency: 每个节点的磁盘写延迟,标签是"实例","设备"。 node_disk_read_latency: 每个节点的磁盘读取延迟,标签是"实例","设备"。 grpc: 每个 {{distro.tikv}} gRPC 的 CPU 利用率,标签是"实例"。' raftstore: '{{distro.tikv}} RaftStore 线程的 CPU 利用率,标签是"实例"。' Async apply: '{{distro.tikv}} 异步应用线程的 CPU 利用率,标签是"实例"。' sched_worker: '{{distro.tikv}} 调度器工作线程的 CPU 利用率,标签是"实例"。' snapshot: '{{distro.tikv}} 快照的 CPU 利用率,标签是"实例"。' unified read pool: '{{distro.tikv}} 统一读池线程的 CPU 利用率,标签是"实例"。' storage read pool: '{{distro.tikv}} 存储读池线程的 CPU 利用率,标签是"实例"。' storage read pool normal: '{{distro.tikv}} 存储读池普通线程的 CPU 利用率,标签是"实例"。' storage read pool high: '{{distro.tikv}} 存储较高读线程的 CPU 利用率,标签是"实例"。' storage read pool low: '{{distro.tikv}} 存储较低读线程的 CPU 利用率,标签是"实例"。' cop: '{{distro.tikv}} Coprocessor 的 CPU 利用率,标签是"实例"。' cop normal: '{{distro.tikv}} Coprocessor 普通线程的 CPU 利用率,标签是"实例"。' cop high: '{{distro.tikv}} Coprocessor 高线程的 CPU 利用率,标签是"实例"。' cop low: '{{distro.tikv}} Coprocessor 低线程的 CPU 利用率,标签是"实例"。' rocksdb: '{{distro.tikv}} RocksDB 的 CPU 利用率,标签是"实例"。' gc: '{{distro.tikv}} GC 的 CPU 利用率,标签是"实例"。' split_check: '{{distro.tikv}} split_chec 的 CPU 利用率,标签是"实例"。' region_score: store 的 Region 得分,标签是"服务地址"。 leader_score: store 的 Leader 得分,标签是"服务地址"。 region_count: store 的 Region 数量,标签是"服务地址"。 leader_count: store 的 Leader 数量,标签是"服务地址"。 region_size: store 的 Region 大小,标签是"服务地址"。 leader_size: store 的 Leader 大小,标签是"服务地址"。 tikv_memtable_hit: memtable 的命中率,标签是"实例"。 tikv_block_all_cache_hit: 所有块缓存的命中率,标签是"实例"。 tikv_block_index_cache_hit: 索引块缓存的命中率,标签是"实例"。 tikv_block_filter_cache_hit: 过滤块缓存的命中率,标签是"实例"。 tikv_block_data_cache_hit: 数据块缓存的命中率,标签是"实例"。 tikv_block_bloom_prefix_cache_hit: bloom_prefix 块缓存的命中率,标签是"实例"。 get duration: RocksDB 执行 get 操作的耗时,标签是"实例"。 seek duration: RocksDB 执行 seek 操作的耗时,标签是"实例"。 write duration: RocksDB 执行写操作的耗时,标签是"实例"。 WAL sync duration: RocksDB 执行 WAL 同步操作的耗时,标签是"实例"。 compaction duration: RocksDB 执行压缩操作的耗时,标签是"实例"。 SST read duration: RocksDB 读取 SST 文件的耗时,标签是"实例"。 write stall duration: 由写停顿引起的时间,标签是"实例"。 ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/diagnoseReportApp/types.ts ================================================ import { createContext } from 'react' export interface TableRowDef { values: string[] sub_values: string[][] comment: string } export interface TableDef { category: string[] title: string comment: string column: string[] rows: TableRowDef[] } export const ExpandContext = createContext(false) ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/react-app-env.d.ts ================================================ declare module '*.module.css' { const classes: { readonly [key: string]: string } export default classes } declare module '*.module.less' { const classes: { readonly [key: string]: string } export default classes } declare module '*.yaml' { const classes: { readonly [key: string]: string } export default classes } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/styles/override.less ================================================ // reset ::selection pseudo element values ::selection { color: currentColor; background-color: #b3d6ff; } html { font-size: 14px; } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/styles/style.less ================================================ @root-entry-name: default; @import 'antd/lib/style/components.less'; // it is expected to import 'antd/es/style/components.less' but it doesn't exist this file ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/utils/appOptions.ts ================================================ export type AppOptions = { hideNav: boolean skipNgmCheck: boolean lang: string } const defAppOptions: AppOptions = { hideNav: false, skipNgmCheck: false, lang: '' } const optionsKey = 'dashboard_app_options' export function saveAppOptions(options: AppOptions) { localStorage.setItem(optionsKey, JSON.stringify(options)) } export function loadAppOptions(): AppOptions { const s = localStorage.getItem(optionsKey) if (s === null) { return defAppOptions } const opt = JSON.parse(s) if (!!opt && opt.constructor === Object) { return opt } return defAppOptions } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/utils/auth.ts ================================================ import { EventEmitter2 } from 'eventemitter2' const tokenKey = 'dashboard_auth_token' export const authEvents = new EventEmitter2() export const EVENT_TOKEN_CHANGED = 'tokenChanged' export function getAuthToken() { return localStorage.getItem(tokenKey) } export function setAuthToken(token) { const lastToken = getAuthToken() if (lastToken !== token) { localStorage.setItem(tokenKey, token) authEvents.emit(EVENT_TOKEN_CHANGED, token) } } export function clearAuthToken() { const lastToken = getAuthToken() if (lastToken) { localStorage.removeItem(tokenKey) authEvents.emit(EVENT_TOKEN_CHANGED, null) } } export function getAuthTokenAsBearer() { const token = getAuthToken() if (!token) { return null } return `Bearer ${token}` } export enum AuthTypes { SQLUser = 0, SharingCode = 1, SSO = 2 } export default { authEvents, EVENT_TOKEN_CHANGED, getAuthToken, setAuthToken, clearAuthToken, getAuthTokenAsBearer, AuthTypes } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/utils/authSSO.ts ================================================ import { Modal } from 'antd' import { ReqConfig } from '@pingcap/tidb-dashboard-lib' import client from '~/client' import { AuthTypes, setAuthToken } from './auth' function newRandomString(length: number) { let text = '' const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' for (let i = 0; i < length; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)) } return text } function getBaseURL() { return `${window.location.protocol}//${window.location.host}${window.location.pathname}` } function getRedirectURL() { return `${getBaseURL()}?sso_callback=1` } export async function getAuthURL() { const codeVerifier = newRandomString(128) const state = newRandomString(32) sessionStorage.setItem('sso.codeVerifier', codeVerifier) sessionStorage.setItem('sso.state', state) const resp = await client .getInstance() .userSSOGetAuthURL({ codeVerifier, redirectUrl: getRedirectURL(), state }) return resp.data } export function isSSOCallback() { const p = new URLSearchParams(window.location.search) return p.has('sso_callback') } async function handleSSOCallbackInner() { const p = new URLSearchParams(window.location.search) if (p.get('state') !== sessionStorage.getItem('sso.state')) { throw new Error( 'Invalid OIDC state: You may see this error when your SSO sign in is outdated.' ) } const r = await client.getInstance().userLogin( { message: { type: AuthTypes.SSO, extra: JSON.stringify({ code: p.get('code'), code_verifier: sessionStorage.getItem('sso.codeVerifier'), redirect_url: getRedirectURL() }) } }, { handleError: 'custom' } as ReqConfig ) sessionStorage.removeItem('sso.codeVerifier') sessionStorage.removeItem('sso.state') setAuthToken(r.data.token) window.location.replace(getBaseURL()) } export async function handleSSOCallback() { try { await handleSSOCallbackInner() } catch (e) { Modal.error({ title: 'SSO Sign In Failed', content: '' + e, okText: 'Sign In Again', onOk: () => window.location.replace(getBaseURL()) }) } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/utils/distro/assetsRes.ts ================================================ import publicPathPrefix from '../publicPathPrefix' let timestamp = document .querySelector('meta[name="x-distro-assets-res-timestamp"]') ?.getAttribute('content') if (timestamp === '__DISTRO_ASSETS_RES_TIMESTAMP__') { timestamp = new Date().valueOf() + '' } const logoSvg = `${publicPathPrefix}/distro-res/logo.svg?t=${timestamp}` const lightLogoSvg = `${publicPathPrefix}/distro-res/logo-icon-light.svg?t=${timestamp}` const landingSvg = `${publicPathPrefix}/distro-res/landing.png?t=${timestamp}` export { logoSvg, lightLogoSvg, landingSvg } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/utils/distro/stringsRes.ts ================================================ import { updateDistro } from '@pingcap/tidb-dashboard-lib' import defDistroStringsRes from './strings_res.json' let distro = defDistroStringsRes // it is a base64 encoded string let distroStringsRes = document .querySelector('meta[name="x-distro-strings-res"]') ?.getAttribute('content') if (distroStringsRes && distroStringsRes !== '__DISTRO_STRINGS_RES__') { try { const distroObj = JSON.parse(atob(distroStringsRes)) distro = { ...defDistroStringsRes, ...distroObj } } catch (error) { console.log(error) } } updateDistro(distro) ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/utils/distro/strings_res.json ================================================ {"tidb":"TiDB","tikv":"TiKV","pd":"PD","tiflash":"TiFlash","ticdc":"TiCDC","tiproxy":"TiProxy","tso":"TSO","scheduling":"Scheduling"} ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/utils/publicPathPrefix.ts ================================================ const DEF_PUBLIC_PATH_PREFIX = '/dashboard' let prefix = document .querySelector('meta[name="x-public-path-prefix"]') ?.getAttribute('content') || DEF_PUBLIC_PATH_PREFIX if (prefix === '__PUBLIC_PATH_PREFIX__') { prefix = DEF_PUBLIC_PATH_PREFIX } export default prefix ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/utils/registry.ts ================================================ import React from 'react' import ReactDOM from 'react-dom' import singleSpaReact from 'single-spa-react' import * as singleSpa from 'single-spa' import { i18n, routing } from '@pingcap/tidb-dashboard-lib' import { AppOptions } from './appOptions' export default class AppRegistry { public defaultRouter = '' public apps = {} public constructor(public appOptions: AppOptions) {} static newReactSpaApp = function (rootComponentAsyncLoader, targetDomId) { const reactLifecycles = singleSpaReact({ React, ReactDOM, loadRootComponent: async () => { const component = await rootComponentAsyncLoader() if (component.default) { return component.default } return component }, domElementGetter: () => document.getElementById(targetDomId)! }) return { bootstrap: [reactLifecycles.bootstrap], mount: [reactLifecycles.mount], unmount: [reactLifecycles.unmount] } } /** * Register a TiDB Dashboard application. * * This function is a light encapsulation over single-spa's registerApplication * which provides some extra registry capabilities. * * @param {{ * id: string, * reactRoot: Function, * routerPrefix: string, * indexRoute: string, * isDefaultRouter: boolean, * icon: string, * }} app */ register(app) { if (app.translations) { i18n.addTranslations(app.translations) } singleSpa.registerApplication( app.id, AppRegistry.newReactSpaApp(app.reactRoot, '__spa_content__'), () => { return routing.isLocationMatchPrefix(app.routerPrefix) }, { registry: this, app } ) if (!app.indexRoute) { app.indexRoute = app.routerPrefix } if (!this.defaultRouter || app.isDefaultRouter) { this.defaultRouter = app.indexRoute } this.apps[app.id] = app return this } /** * Get the default router for initial routing. */ getDefaultRouter() { return this.defaultRouter || '/' } /** * Get the registry of the current active app. */ getActiveApp() { const mountedApps = singleSpa.getMountedApps() for (let i = 0; i < mountedApps.length; i++) { const app = mountedApps[i] if (this.apps[app] !== undefined) { return this.apps[app] } } } } ================================================ FILE: ui/packages/tidb-dashboard-for-op/src/utils/store.ts ================================================ import { store, ReqConfig } from '@pingcap/tidb-dashboard-lib' import client, { InfoInfoResponse } from '~/client' import { authEvents, EVENT_TOKEN_CHANGED, getAuthToken } from './auth' export async function reloadWhoAmI(): Promise { if (!getAuthToken()) { store.update((s) => { s.whoAmI = undefined }) return false } try { const resp = await client.getInstance().infoWhoami({ handleError: 'custom' } as ReqConfig) store.update((s) => { s.whoAmI = resp.data }) return true } catch (ex) { store.update((s) => { s.whoAmI = undefined }) return false } } export async function mustLoadAppInfo(): Promise { const resp = await client.getInstance().infoGet({ handleError: 'custom' } as ReqConfig) store.update((s) => { s.appInfo = resp.data }) return resp.data } authEvents.on(EVENT_TOKEN_CHANGED, async () => { await reloadWhoAmI() }) ================================================ FILE: ui/packages/tidb-dashboard-for-op/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "noEmit": true, "noImplicitAny": false, "noImplicitThis": false, "baseUrl": ".", "paths": { "~/*": ["src/*"] } }, "include": ["src"] } ================================================ FILE: ui/packages/tidb-dashboard-lib/builder.js ================================================ const chalk = require('chalk') const { watch } = require('chokidar') const esbuild = require('esbuild') const postCssPlugin = require('@baurine/esbuild-plugin-postcss3') const autoprefixer = require('autoprefixer') const { yamlPlugin } = require('esbuild-plugin-yaml') const { lessModifyVars, lessGlobalVars } = require('../../less-vars') // customized plugin: log time const logTime = (_options = {}) => ({ name: 'logTime', setup(build) { let time build.onStart(() => { time = new Date() console.log(`Build started`) }) build.onEnd(() => { console.log(`Build ended: ${chalk.yellow(`${new Date() - time}ms`)}`) }) } }) const { dependencies } = require('./package.json') const esbuildParams = { color: true, entryPoints: ['src/index.ts'], outfile: 'dist/index.js', target: ['esnext'], format: 'esm', bundle: true, sourcemap: true, logLevel: 'error', incremental: true, platform: 'browser', external: Object.keys(dependencies), plugins: [ postCssPlugin.default({ lessOptions: { modifyVars: lessModifyVars, globalVars: lessGlobalVars, javascriptEnabled: true }, enableCache: true, plugins: [autoprefixer] }), yamlPlugin(), logTime() ] } async function main() { const builder = await esbuild.build(esbuildParams) function rebuild() { builder.rebuild().catch((err) => console.log(err)) } const isDev = process.env.NODE_ENV !== 'production' if (isDev) { watch('src/**/*', { ignoreInitial: true }).on('all', () => { rebuild() }) } else { process.exit(0) } } main() ================================================ FILE: ui/packages/tidb-dashboard-lib/gulpfile.js ================================================ const { task, parallel } = require('gulp') const shell = require('gulp-shell') // below way doesn't work // task('tsc:dev', parallel(shell.task('tsc -w'), shell.task('tsc-alias -w'))) // https://stackoverflow.com/a/47305304/2998877 task('tsc:dev', shell.task('tsc-watch --onCompilationComplete "tsc-alias"')) task('tsc:build', shell.task('tsc && tsc-alias')) // https://www.npmjs.com/package/eslint-watch task('lint:watch', shell.task('esw --watch --cache --ext .tsx,.ts src/')) task('lint:check', shell.task('esw --cache --ext tsx,ts src/')) task('esbuild:dev', shell.task('NODE_ENV=development node builder.js')) task('esbuild:build', shell.task('NODE_ENV=production node builder.js')) task('dev', parallel('tsc:dev', 'lint:watch', 'esbuild:dev')) task('build', parallel('tsc:build', 'lint:check', 'esbuild:build')) ================================================ FILE: ui/packages/tidb-dashboard-lib/package.json ================================================ { "name": "@pingcap/tidb-dashboard-lib", "version": "1.0.0", "description": "", "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "dev": "gulp dev", "build": "rimraf dist && gulp build" }, "author": "", "license": "ISC", "dependencies": { "@ahooksjs/use-url-state": "^3.5.0", "@ant-design/colors": "^6.0.0", "@ant-design/icons": "^4.7.0", "@baurine/grafana-value-formats": "^1.0.4", "@baurine/sql-formatter-plus": "^1.5.3", "@elastic/charts": "^46.10.1", "@g07cha/flexbox-react": "^5.0.0", "@tanstack/react-query": "4", "@uifabric/utilities": "^7.33.5", "ace-builds": "^1.6.0", "ahooks": "^3.1.9", "antd": "^4.18.7", "axios": "^1.12.0", "classnames": "^2.3.1", "d3": "^5.16.0", "d3-flextree": "2.1.2", "d3-graphviz": "4.1.0", "dayjs": "^1.10.8", "hsluv": "^0.1.0", "i18next": "^23.7.11", "i18next-browser-languagedetector": "^6.1.3", "lodash": "^4.17.21", "metrics-chart": "^0.32.0", "mixpanel-browser": "^2.45.0", "moize": "^5.4.7", "office-ui-fabric-react": "^7.183.1", "pullstate": "^1.23.0", "rc-picker": "^2.6.4", "rc-trigger": "^5.2.11", "rc-util": "^5.19.3", "react": "^17.0.2", "react-ace": "^10.1.0", "react-copy-to-clipboard": "^5.0.2", "react-csv": "^2.2.2", "react-dom": "^17.0.2", "react-error-boundary": "^3.1.4", "react-highlight-words": "^0.18.0", "react-i18next": "^11.15.4", "react-intersection-observer": "^9.2.2", "react-json-view": "1.21.3", "react-markdown": "^8.0.3", "react-router": "6", "react-router-dom": "6", "react-split": "^2.0.14", "react-spring": "^9.4.4", "react-syntax-highlighter": "^16.0.0", "react-use": "^15.3.3", "sql-formatter": "^4.0.2", "string-template": "^1.0.0", "url": "^0.11.0", "uuid": "^8.3.2", "visual-plan": "^0.0.7" }, "devDependencies": { "@baurine/esbuild-plugin-postcss3": "^0.4.3", "@openapitools/openapi-generator-cli": "^2.4.26", "@types/d3": "^5.7.2", "@types/d3-graphviz": "^2.6.7", "@types/lodash": "^4.14.180", "@types/node": "^16.9.1", "@types/react": "^17.0.20", "@types/react-dom": "^17.0.9", "autoprefixer": "^10.4.2", "chalk": "4.1.2", "chokidar": "^3.5.2", "esbuild": "^0.14.23", "esbuild-plugin-svgr": "^1.0.0", "esbuild-plugin-yaml": "^0.0.1", "eslint-watch": "^8.0.0", "fs-extra": "^10.0.0", "gulp": "^4.0.2", "gulp-shell": "^0.8.0", "rimraf": "^3.0.2", "tsc-alias": "^1.6.9", "tsc-watch": "^5.0.3", "typescript": "^4.7.3" }, "resolutions": { "d3-selection": "1.4.1" } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/components/DiskTable.tsx ================================================ import { Tooltip, Typography } from 'antd' import React, { useContext, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { getValueFormat } from '@baurine/grafana-value-formats' import { WarningOutlined } from '@ant-design/icons' import { HostinfoInfo, HostinfoPartitionInfo } from '@lib/client' import { Bar, CardTable } from '@lib/components' import { useClientRequest } from '@lib/utils/useClientRequest' import { InstanceKind, InstanceKinds, instanceKindName } from '@lib/utils/instanceTable' import { ClusterInfoContext } from '../context' interface IExpandedDiskItem extends HostinfoPartitionInfo { key: string host?: string instancesCount: Record } function expandDisksItems(rows: HostinfoInfo[]): IExpandedDiskItem[] { const expanded: IExpandedDiskItem[] = [] rows.forEach((row) => { const instancesPerPartition: Record< string, Record > = {} let partitions = 0 Object.values(row.instances ?? {}).forEach((i) => { if (!i) { return } if (!instancesPerPartition[i.partition_path_lower!]) { instancesPerPartition[i.partition_path_lower!] = { pd: 0, tidb: 0, tikv: 0, tiflash: 0, ticdc: 0, tiproxy: 0, tso: 0, scheduling: 0 } } instancesPerPartition[i.partition_path_lower!][i.type!]++ }) for (let pathL in row.partitions) { const instancesCount = instancesPerPartition[pathL] if (!instancesCount) { // This partition does not have deployed instances, skip continue } const partition = row.partitions[pathL] expanded.push({ key: `${row.host} ${pathL}`, host: row.host, instancesCount, ...partition }) partitions++ } if (partitions === 0) { // Supply dummy item.. expanded.push({ key: row.host ?? '', host: row.host, instancesCount: { pd: 0, tidb: 0, tikv: 0, tiflash: 0, ticdc: 0, tiproxy: 0, tso: 0, scheduling: 0 } }) } }) return expanded } export default function HostTable() { const { t } = useTranslation() const ctx = useContext(ClusterInfoContext) const { data, isLoading, error } = useClientRequest( ctx!.ds.clusterInfoGetHostsInfo ) const diskData = useMemo(() => expandDisksItems(data?.hosts ?? []), [data]) const columns: IColumn[] = useMemo( () => [ { name: t('cluster_info.list.disk_table.columns.host'), key: 'host', minWidth: 100, maxWidth: 150, onRender: (row: IExpandedDiskItem) => { if (!row.free) { return ( {row.host} ) } return ( {row.host} ) } }, { name: t('cluster_info.list.disk_table.columns.mount_dir'), key: 'mount_dir', minWidth: 150, maxWidth: 200, onRender: (row: IExpandedDiskItem) => { if (!row.path) { return } return ( {row.path} ) } }, { name: t('cluster_info.list.disk_table.columns.fs'), key: 'fs', minWidth: 50, maxWidth: 100, onRender: (row: IExpandedDiskItem) => { return row.fstype?.toUpperCase() ?? '' } }, { name: t('cluster_info.list.disk_table.columns.disk_size'), key: 'disk_size', minWidth: 60, maxWidth: 100, onRender: (row: IExpandedDiskItem) => { if (!row.total) { return } return getValueFormat('bytes')(row.total, 1) } }, { name: t('cluster_info.list.disk_table.columns.disk_usage'), key: 'disk_usage', minWidth: 100, maxWidth: 150, onRender: (row: IExpandedDiskItem) => { if (!row.total || !row.free) { return } const total = row.total const free = row.free const used = total - free const usedPercent = (used / total).toFixed(3) const tooltipContent = ( Used: {getValueFormat('bytes')(used, 1)} ( {getValueFormat('percentunit')(+usedPercent, 1)}) ) return ( ) } }, { name: t('cluster_info.list.disk_table.columns.instances'), key: 'instances', minWidth: 100, maxWidth: 200, onRender: (row: IExpandedDiskItem) => { const item = InstanceKinds.map((ik) => { if (row.instancesCount[ik] > 0) { return `${row.instancesCount[ik]} ${instanceKindName(ik)}` } else { return '' } }) const content = item.filter((v) => v.length > 0).join(', ') return ( {content} ) } } ], [t] ) return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/components/HostTable.tsx ================================================ import { Tooltip, Typography } from 'antd' import React, { useContext, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { red } from '@ant-design/colors' import { getValueFormat } from '@baurine/grafana-value-formats' import { HostinfoInfo } from '@lib/client' import { Bar, CardTable, Pre } from '@lib/components' import { useClientRequest } from '@lib/utils/useClientRequest' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { InstanceKind, InstanceKinds, instanceKindName } from '@lib/utils/instanceTable' import { WarningOutlined } from '@ant-design/icons' import { ClusterInfoContext } from '../context' interface IExpandedHostItem extends HostinfoInfo { key: string instancesCount: Record } function expandHostItems(rows: HostinfoInfo[]): IExpandedHostItem[] { const expanded: IExpandedHostItem[] = [] rows.forEach((row) => { const instancesCount: Record = { pd: 0, tidb: 0, tikv: 0, tiflash: 0, ticdc: 0, tiproxy: 0, tso: 0, scheduling: 0 } Object.values(row.instances ?? {}).forEach((i) => { if (!i) { return } instancesCount[i.type!]++ }) expanded.push({ key: row.host ?? '', instancesCount, ...row }) }) return expanded } export default function HostTable() { const { t } = useTranslation() const ctx = useContext(ClusterInfoContext) const { data, isLoading, error } = useClientRequest( ctx!.ds.clusterInfoGetHostsInfo ) const hostData = useMemo(() => expandHostItems(data?.hosts ?? []), [data]) const columns: IColumn[] = useMemo( () => [ { name: t('cluster_info.list.host_table.columns.host'), key: 'host', minWidth: 100, maxWidth: 150, onRender: (row: IExpandedHostItem) => { if (!row.cpu_info) { // We assume that CPU info must be successfully retrieved. return ( {row.host} ) } return ( {row.host} ) } }, { name: t('cluster_info.list.host_table.columns.cpu'), key: 'cpu', minWidth: 100, maxWidth: 150, onRender: (row: IExpandedHostItem) => { const { cpu_info: c } = row if (!c) { return } const tooltipContent = ` Physical Cores: ${c.physical_cores} Logical Cores: ${c.logical_cores}` return ( {tooltipContent.trim()}}> {`${c.physical_cores!} (${c.logical_cores!} vCore)`} ) } }, { name: t('cluster_info.list.host_table.columns.cpu_arch'), key: 'cpu-arch', minWidth: 60, maxWidth: 100, onRender: (row: IExpandedHostItem) => { const { cpu_info: c } = row if (!c || !c.arch) { return {'Unknow'} } return {`${c.arch}`} } }, { name: t('cluster_info.list.host_table.columns.cpu_usage'), key: 'cpu_usage', minWidth: 100, maxWidth: 150, onRender: (row: IExpandedHostItem) => { if (!row.cpu_usage) { return } const system = row.cpu_usage.system ?? 0 const idle = row.cpu_usage.idle ?? 1 const user = 1 - system - idle const tooltipContent = ` User: ${getValueFormat('percentunit')(user)} System: ${getValueFormat('percentunit')(system)}` return ( {tooltipContent.trim()}}> ) } }, { name: t('cluster_info.list.host_table.columns.memory'), key: 'memory', minWidth: 60, maxWidth: 100, onRender: (row: IExpandedHostItem) => { if (!row.memory_usage) { return } return getValueFormat('bytes')(row.memory_usage.total ?? 0, 1) } }, { name: t('cluster_info.list.host_table.columns.memory_usage'), key: 'memory_usage', minWidth: 100, maxWidth: 150, onRender: (row: IExpandedHostItem) => { if (!row.memory_usage) { return } const { total, used } = row.memory_usage const usedPercent = (used! / total!).toFixed(3) const title = (
Used: {getValueFormat('bytes')(used!, 1)} ( {getValueFormat('percentunit')(+usedPercent, 1)})
) return ( ) } }, { name: t('cluster_info.list.host_table.columns.instances'), key: 'instances', minWidth: 100, maxWidth: 200, onRender: (row: IExpandedHostItem) => { const item = InstanceKinds.map((ik) => { if (row.instancesCount[ik] > 0) { return `${row.instancesCount[ik]} ${instanceKindName(ik)}` } else { return '' } }) const content = item.filter((v) => v.length > 0).join(', ') return ( {content} ) } } ], [t] ) return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/components/InstanceTable.tsx ================================================ import { DeleteOutlined } from '@ant-design/icons' import { useMemoizedFn } from 'ahooks' import { Divider, Popconfirm, Tooltip } from 'antd' import React, { useCallback, useContext, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { CardTable, InstanceStatusBadge } from '@lib/components' import DateTime from '@lib/components/DateTime' import { buildInstanceTable, IInstanceTableItem, InstanceStatus } from '@lib/utils/instanceTable' import { useClientRequest } from '@lib/utils/useClientRequest' import { ClusterInfoContext } from '../context' function StatusColumn({ node, onHideTiDB }: { node: IInstanceTableItem onHideTiDB: (node) => void }) { const { t } = useTranslation() const onConfirm = useMemoizedFn(() => { onHideTiDB && onHideTiDB(node) }) return ( {node.instanceKind === 'tidb' && node.status !== InstanceStatus.Up && ( <> )} ) } export default function ListPage() { const { t } = useTranslation() const ctx = useContext(ClusterInfoContext) const { data: dataTiDB, isLoading: loadingTiDB, error: errTiDB, sendRequest } = useClientRequest(ctx!.ds.getTiDBTopology) const { data: dataStores, isLoading: loadingStores, error: errStores } = useClientRequest(ctx!.ds.getStoreTopology) const { data: dataPD, isLoading: loadingPD, error: errPD } = useClientRequest(ctx!.ds.getPDTopology) const { data: dataTiCDC, isLoading: loadingTiCDC, error: errTiCDC } = useClientRequest(ctx!.ds.getTiCDCTopology) const { data: dataTiProxy, isLoading: loadingTiProxy, error: errTiProxy } = useClientRequest(ctx!.ds.getTiProxyTopology) const { data: dataTSO, isLoading: loadingTSO, error: errTSO } = useClientRequest(ctx!.ds.getTSOTopology) const { data: dataScheduling, isLoading: loadingScheduling, error: errScheduling } = useClientRequest(ctx!.ds.getSchedulingTopology) // query TiCDC and TiProxy components returns 404 under TiDB 7.6.0 // filter out the 404 error const errors = [ errTiDB, errStores, errPD, errTiCDC, errTiProxy, errTSO, errScheduling ].filter((e) => e?.response?.status !== 404) const [tableData, groupData] = useMemo( () => buildInstanceTable({ dataPD, dataTiDB, dataTiKV: dataStores?.tikv, dataTiFlash: dataStores?.tiflash, dataTiCDC, dataTiProxy, dataTSO, dataScheduling, includeTiFlash: true }), [ dataTiDB, dataStores, dataPD, dataTiCDC, dataTiProxy, dataTSO, dataScheduling ] ) const handleHideTiDB = useCallback( async (node) => { await ctx!.ds.topologyTidbAddressDelete(`${node.ip}:${node.port}`) sendRequest() }, [sendRequest, ctx] ) const columns = useMemo( () => [ { name: t('cluster_info.list.instance_table.columns.node'), key: 'node', minWidth: 100, maxWidth: 160, onRender: ({ ip, port }) => { const fullName = `${ip}:${port}` return ( {fullName} ) } }, { name: t('cluster_info.list.instance_table.columns.status'), key: 'status', minWidth: 100, maxWidth: 120, onRender: (node) => ( ) }, { name: t('cluster_info.list.instance_table.columns.up_time'), key: 'start_timestamp', minWidth: 100, maxWidth: 150, onRender: ({ start_timestamp: ts }) => { if (ts !== undefined && ts !== 0) { return } } }, { name: t('cluster_info.list.instance_table.columns.version'), fieldName: 'version', key: 'version', minWidth: 100, maxWidth: 150, onRender: ({ version }) => ( {version} ) }, { name: t('cluster_info.list.instance_table.columns.git_hash'), fieldName: 'git_hash', key: 'git_hash', minWidth: 100, maxWidth: 200, onRender: ({ git_hash }) => ( {git_hash} ) }, { name: t('cluster_info.list.instance_table.columns.deploy_path'), fieldName: 'deploy_path', key: 'deploy_path', minWidth: 150, maxWidth: 300, onRender: ({ deploy_path }) => ( {deploy_path} ) } ], [t, handleHideTiDB] ) return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/components/Statistics.module.less ================================================ @import 'antd/es/style/themes/default.less'; // FIXME: We should not provide padding for CardTab content, so that user // can control whether a padding is needed. For example, to a . .content { margin-left: -@padding-page; margin-right: -@padding-page; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/components/Statistics.tsx ================================================ import React, { useContext } from 'react' import { useClientRequest } from '@lib/utils/useClientRequest' import { ClusterinfoClusterStatisticsPartial } from '@lib/client' import { AnimatedSkeleton, ErrorBar, Descriptions, Card } from '@lib/components' import { useTranslation } from 'react-i18next' import { getValueFormat } from '@baurine/grafana-value-formats' import { Alert } from 'antd' import styles from './Statistics.module.less' import { InstanceKinds, instanceKindName } from '@lib/utils/instanceTable' import { ClusterInfoContext } from '../context' function PartialInfo({ data }: { data?: ClusterinfoClusterStatisticsPartial }) { const { t } = useTranslation() return ( {data?.number_of_instances ?? 'Unknown'} {data?.number_of_hosts ?? 'Unknown'} {getValueFormat('bytes')(data?.total_memory_capacity_bytes ?? 0, 1)} {data?.total_physical_cores ?? 'Unknown'} {data?.total_logical_cores ?? 'Unknown'} ) } export default function Statistics() { const ctx = useContext(ClusterInfoContext) const { data, isLoading, error } = useClientRequest( ctx!.ds.clusterInfoGetStatistics ) const { t } = useTranslation() return ( {error && } {data && (
{(data.probe_failure_hosts ?? 0) > 0 && ( )} {(data.versions ?? []).join(', ')} {InstanceKinds.map((ik) => { const d = data.stats_by_instance_kind?.[ik] const instNum = d?.number_of_instances ?? 0 return instNum > 0 ? ( ) : null })}
)}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/components/StoreLocation.tsx ================================================ import React, { useContext, useMemo } from 'react' import { useClientRequest } from '@lib/utils/useClientRequest' import { AnimatedSkeleton, ErrorBar } from '@lib/components' import StoreLocationTree, { buildTreeData, getShortStrMap } from './StoreLocationTree' import { ClusterInfoContext } from '../context' export default function StoreLocation() { const ctx = useContext(ClusterInfoContext) const { data, isLoading, error, sendRequest } = useClientRequest( ctx!.ds.getStoreLocationTopology ) const treeData = useMemo(() => buildTreeData(data), [data]) const shortStrMap = useMemo(() => getShortStrMap(data), [data]) return (
document.documentElement.clientHeight - 80 - 48 * 2 // 48 = margin of cardInner } onReload={sendRequest} />
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/components/StoreLocationTree/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .tooltip { opacity: 0; position: absolute; top: 0; left: 0; padding: @padding-xs @padding-md; background: white; text-align: center; line-height: @line-height-base; border-radius: @border-radius-base; z-index: 10; transition: all 0.1s ease-out; pointer-events: none; box-shadow: @box-shadow-base; &::before { content: ''; position: absolute; bottom: 0; left: 50%; width: 12px; height: 12px; background: white; border: 1px solid #ddd; border-top-color: transparent; border-left-color: transparent; transform: translate(-50%, 50%) rotate(45deg); transform-origin: center center; z-index: 10; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/components/StoreLocationTree/index.stories.tsx ================================================ import React from 'react' import StoreLocationTree, { buildTreeData, trimDuplicate, getShortStrMap } from '.' export default { title: 'StoreLocationTree' } const dataSource1 = { name: 'Stores', value: '', children: [ { name: 'zone', value: 'sh', children: [ { name: 'rack', value: 'r1', children: [ { name: 'host', value: 'h1', children: [ { name: 'TiKV', value: '127.0.0.1:20160', children: [] } ] }, { name: 'host', value: 'h2', children: [ { name: 'TiKV', value: '127.0.0.1:20162', children: [] } ] } ] } ] }, { name: 'zone', value: 'bj', children: [ { name: 'rack', value: 'r1', children: [ { name: 'host', value: 'h1', children: [ { name: 'TiKV', value: '127.0.0.1:20161', children: [] } ] } ] }, { name: 'TiFlash', value: '127.0.0.1:3930', children: [] } ] } ] } export const Normal = () => const dataSource2 = { name: 'Stores', value: '', children: [ { name: 'failure-domain.beta.kubernetes.io/region', value: 'us-west1', children: [ { name: 'failure-domain.beta.kubernetes.io/zone', value: 'us-west1-a', children: [ { name: 'kubernetes.io/hostname', value: 'shoot--stating--a13df0bd-56f54530-z1-111111-tkq7r.internal', children: [ { name: 'TiFlash', value: 'db-tiflash-0.db-tiflash-peer.tidb1373', children: [] } ] }, { name: 'kubernetes.io/hostname', value: 'shoot--stating--a13df0bd-b8cdec65-z1-22222-fdsaf.internal', children: [ { name: 'TiKV', value: 'db-tikv-0.db-tikv-peer.tidb1373', children: [] } ] } ] }, { name: 'failure-domain.beta.kubernetes.io/zone', value: 'us-west1-b', children: [ { name: 'kubernetes.io/hostname', value: 'shoot--stating--a13df0bd-xxxxxxxxxx-z1-33333-xxxxx.internal', children: [ { name: 'TiKV', value: 'db-tikv-1.db-tikv-peer.tidb1373', children: [] } ] } ] }, { name: 'failure-domain.beta.kubernetes.io/zone', value: 'us-west1-c', children: [ { name: 'kubernetes.io/hostname', value: 'shoot--stating--a13df0bd-yyyyy-z1-33333-mmmm.internal', children: [ { name: 'TiKV', value: 'db-tikv-2.db-tikv-peer.tidb1373', children: [] } ] } ] } ] } ] } export const Kubernetes = () => ///////////////////////////// const arr1 = [ 'aaa-bbbb-111a.abc.123', 'aaa-bbbb-222a.abc.123', 'aaa-bbbb-333a.abc.123' ] const arr2 = ['aaa-111a.abc.123', 'aaa-222a.abc.123', 'aaa-333a.abc.123'] const arr3 = [] const arr4 = ['abc'] const arr5 = ['abcd', 'abce'] console.log(trimDuplicate(arr1)) console.log(trimDuplicate(arr2)) console.log(trimDuplicate(arr3)) console.log(trimDuplicate(arr4)) console.log(trimDuplicate(arr5)) ///////////////////////////// const data1 = { location_labels: [ 'failure-domain.beta.kubernetes.io/region', 'failure-domain.beta.kubernetes.io/zone', 'kubernetes.io/hostname' ], stores: [ { address: 'db-tiflash-0.db-tiflash-peer.tidb1373', labels: { engine: 'tiflash', 'failure-domain.beta.kubernetes.io/region': 'us-west1', 'failure-domain.beta.kubernetes.io/zone': 'us-west1-a', 'kubernetes.io/hostname': 'shoot--stating--a13df0bd-56f54530-z1-111111-tkq7r.internal' } }, { address: 'db-tikv-0.db-tikv-peer.tidb1373', labels: { engine: '', 'failure-domain.beta.kubernetes.io/region': 'us-west1', 'failure-domain.beta.kubernetes.io/zone': 'us-west1-a', 'kubernetes.io/hostname': 'shoot--stating--a13df0bd-b8cdec65-z1-22222-fdsaf.internal' } }, { address: 'db-tikv-1.db-tikv-peer.tidb1373', labels: { engine: '', 'failure-domain.beta.kubernetes.io/region': 'us-west1', 'failure-domain.beta.kubernetes.io/zone': 'us-west1-b', 'kubernetes.io/hostname': 'shoot--stating--a13df0bd-xxxxxxxxxx-z1-33333-xxxxx.internal' } }, { address: 'db-tikv-2.db-tikv-peer.tidb1373', labels: { engine: '', 'failure-domain.beta.kubernetes.io/region': 'us-west1', 'failure-domain.beta.kubernetes.io/zone': 'us-west1-c', 'kubernetes.io/hostname': 'shoot--stating--a13df0bd-yyyyy-z1-33333-mmmm.internal' } } ] } const dataSource = buildTreeData(data1) const shortStrMap = getShortStrMap(data1) console.log(shortStrMap) export const KubernetesByShort = () => ( ) ///////////////////////////// ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/components/StoreLocationTree/index.tsx ================================================ import React, { useRef, useEffect } from 'react' import * as d3 from 'd3' import { ZoomInOutlined, ZoomOutOutlined, ReloadOutlined } from '@ant-design/icons' import { Space } from 'antd' import { cyan, magenta, grey } from '@ant-design/colors' import { useTranslation } from 'react-i18next' import { TopologyStoreLocation } from '@lib/client' import styles from './index.module.less' import { instanceKindName } from '@lib/utils/instanceTable' ////////////////////////////////////// type ShortStrMap = Record export function getShortStrMap( data: TopologyStoreLocation | undefined ): ShortStrMap { let allShortStrMap: ShortStrMap = {} if (data === undefined) { return allShortStrMap } // location labels // failure-domain.beta.kubernetes.io/region => region data.location_labels?.forEach((label) => { if (label.indexOf('/') >= 0) { const shortStr = label.split('/').pop() if (shortStr) { allShortStrMap[label] = shortStr } } }) // location labels value data.location_labels?.forEach((label) => { // get label values const labelValues: string[] = [] data.stores?.forEach((store) => { const val = store.labels?.[label] if (val) { labelValues.push(val) } }) const shortStrMap = trimDuplicate(labelValues) allShortStrMap = Object.assign(allShortStrMap, shortStrMap) }) // tikv & tiflash nodes address const addresses = (data.stores || []).map((s) => s.address!) addresses.forEach((addr) => { if (addr.startsWith('db-')) { const shortStr = addr.split('.').shift() if (shortStr) { allShortStrMap[addr] = shortStr } } }) return allShortStrMap } // input: ['aaa-111a.abc.123', 'aaa-222a.abc.123', 'aaa-333a.abc.123'], items in the array have either the same prefix or suffix, or both. // output: // { // "aaa-111a.abc.123":"111a", // "aaa-222a.abc.123":"222a", // "aaa-333a.abc.123":"333a" // } export function trimDuplicate(strArr: string[]): ShortStrMap { const shortStrMap: ShortStrMap = {} const strSet = new Set(strArr) if (strSet.size < 2) { return shortStrMap } let i = 0 let c const charSet = new Set() // calc the prefix length let headDotOrMinusPos = -1 while (true) { charSet.clear() for (let str of strSet) { c = str[i] if (c === undefined) { break } charSet.add(c) } if (c === undefined) { break } if (charSet.size > 1) { break } if (c === '.' || c === '-') { headDotOrMinusPos = i } i++ } // calc the suffix length i = 0 let tailDotOrMinusPos = -1 while (true) { charSet.clear() for (let str of strSet) { c = str[str.length - 1 - i] if (c === undefined) { break } charSet.add(c) } if (c === undefined) { break } if (charSet.size > 1) { break } if (c === '.' || c === '-') { tailDotOrMinusPos = i } i++ } if (headDotOrMinusPos === -1 && tailDotOrMinusPos === -1) { return shortStrMap } strSet.forEach((s) => { const startIdx = headDotOrMinusPos + 1 const endIdx = tailDotOrMinusPos === -1 ? s.length : s.length - 1 - tailDotOrMinusPos const short = s.slice(startIdx, endIdx) shortStrMap[s] = short }) return shortStrMap } ////////////////////////////////////// const NODE_STORES = 'Stores' const NODE_TIFLASH = () => instanceKindName('tiflash') const NODE_TIKV = () => instanceKindName('tikv') type TreeNode = { name: string value: string children: TreeNode[] } export function buildTreeData( data: TopologyStoreLocation | undefined ): TreeNode { const treeData: TreeNode = { name: NODE_STORES, value: '', children: [] } if ((data?.location_labels?.length || 0) > 0) { const locationLabels: string[] = data?.location_labels || [] for (const store of data?.stores || []) { // reset curNode, point to tree nodes beginning let curNode = treeData for (const curLabel of locationLabels) { const curLabelVal = store.labels![curLabel] if (curLabelVal === undefined) { continue } let subNode: TreeNode | undefined = curNode.children.find( (el) => el.name === curLabel && el.value === curLabelVal ) if (subNode === undefined) { subNode = { name: curLabel, value: curLabelVal, children: [] } curNode.children.push(subNode) } // make curNode point to subNode curNode = subNode } const storeType = store.labels!['engine'] === 'tiflash' ? NODE_TIFLASH() : NODE_TIKV() curNode.children.push({ name: storeType, value: store.address!, children: [] }) } } return treeData } ////////////////////////////////////// interface ITooltipConfig { enable: boolean offsetX: number offsetY: number } export interface IStoreLocationProps { dataSource: any shortStrMap?: ShortStrMap getMinHeight?: () => number onReload?: () => void } const MAX_STR_LENGTH = 16 const margin = { left: 60, right: 40, top: 80, bottom: 100 } const dx = 40 const diagonal = d3 .linkHorizontal() .x((d: any) => d.y) .y((d: any) => d.x) function calcHeight(root) { let x0 = Infinity let x1 = -x0 root.each((d) => { if (d.x > x1) x1 = d.x if (d.x < x0) x0 = d.x }) return x1 - x0 } export default function StoreLocationTree({ dataSource, shortStrMap = {}, getMinHeight, onReload }: IStoreLocationProps) { const divRef = useRef(null) const { t } = useTranslation() const tooltipConfig = useRef() tooltipConfig.current = { enable: true, offsetX: 0, offsetY: 0 } useEffect(() => { let divWidth = divRef.current?.clientWidth || 0 const root = d3.hierarchy(dataSource) as any root.descendants().forEach((d, i) => { d.id = i d._children = d.children // collapse all nodes default // if (d.depth) d.children = null }) const dy = divWidth / (root.height + 2) let tree = d3.tree().nodeSize([dx, dy]) const div = d3.select(divRef.current) div.select('svg#slt').remove() const svg = div .append('svg') .attr('id', 'slt') .attr('width', divWidth) .attr('height', dx + margin.top + margin.bottom) .style('font', '14px sans-serif') .style('user-select', 'none') const bound = svg .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`) const gLink = bound .append('g') .attr('fill', 'none') .attr('stroke', '#ddd') .attr('stroke-width', 2) const gNode = bound .append('g') .attr('cursor', 'pointer') .attr('pointer-events', 'all') // tooltip const tooltip = d3.select('#store-location-tooltip') // zoom const zoom = d3 .zoom() .scaleExtent([0.1, 5]) .filter(function () { // ref: https://godbasin.github.io/2018/02/07/d3-tree-notes-4-zoom-amd-drag/ // only zoom when pressing CTRL const isWheelEvent = d3.event instanceof WheelEvent return !isWheelEvent || (isWheelEvent && d3.event.ctrlKey) }) .on('start', () => { // hide tooltip if it shows tooltip.style('opacity', 0) tooltipConfig.current!.enable = false }) .on('zoom', () => { const t = d3.event.transform bound.attr( 'transform', `translate(${t.x + margin.left}, ${t.y + margin.top}) scale(${t.k})` ) // this will cause unexpected result when dragging // svg.attr('transform', d3.event.transform) }) .on('end', () => { const t = d3.event.transform tooltipConfig.current = { enable: t.k === 1, // disable tooltip if zoom offsetX: t.x, offsetY: t.y } }) svg.call(zoom as any) // zoom actions d3.select('#slt-zoom-in').on('click', function () { zoom.scaleBy(svg.transition().duration(500) as any, 1.2) }) d3.select('#slt-zoom-out').on('click', function () { zoom.scaleBy(svg.transition().duration(500) as any, 0.8) }) d3.select('#slt-zoom-reset').on('click', function () { // https://stackoverflow.com/a/51981636/2998877 svg .transition() .duration(500) .call(zoom.transform as any, d3.zoomIdentity) onReload?.() }) update(root) function update(source) { // use altKey to slow down the animation, interesting! const duration = d3.event && d3.event.altKey ? 2500 : 500 const nodes = root.descendants().reverse() const links = root.links() // compute the new tree layout // it modifies root self tree(root) const boundHeight = calcHeight(root) // node.x represent the y axes position actually // [root.y, root.x] is [0, 0], we need to move it to [0, boundHeight/2] root.descendants().forEach((d, i) => { d.x += boundHeight / 2 }) if (root.x0 === undefined) { // initial root.x0, root.y0, only need to set it once root.x0 = root.x root.y0 = root.y } const contentHeight = boundHeight + margin.top + margin.bottom const transition = svg .transition() .duration(duration) .attr('width', divWidth) .attr('height', Math.max(getMinHeight?.() || 0, contentHeight)) // update the nodes const node = gNode.selectAll('g').data(nodes, (d: any) => d.id) // enter any new nodes at the parent's previous position const nodeEnter = node .enter() .append('g') .attr('transform', (_d) => `translate(${source.y0},${source.x0})`) .attr('fill-opacity', 0) .attr('stroke-opacity', 0) .on('click', (d: any) => { d.children = d.children ? null : d._children update(d) }) .on('mouseenter', onMouseEnter) .on('mouseleave', onMouseLeave) function onMouseEnter(datum) { if (!tooltipConfig.current?.enable) { return } const { name, value } = datum.data if ( shortStrMap[name] === undefined && shortStrMap[value] === undefined ) { return } tooltip.select('#store-location-tooltip-name').text(name) tooltip.select('#store-location-tooltip-value').text(value) const x = datum.y + margin.left + tooltipConfig.current.offsetX const y = datum.x + margin.top - 20 + tooltipConfig.current.offsetY tooltip.style( 'transform', `translate(calc(-50% + ${x}px), calc(-100% + ${y}px))` ) tooltip.style('opacity', 1) } function onMouseLeave() { tooltip.style('opacity', 0) } // circle nodeEnter .append('circle') .attr('r', 8) .attr('fill', '#fff') .attr('stroke', (d: any) => { if (d._children) { return grey[1] } if (d.data.name === NODE_TIFLASH()) { return magenta[4] } return cyan[5] }) .attr('stroke-width', 3) // text for root node nodeEnter .filter(({ data: { name } }: any) => name === NODE_STORES) .append('text') .attr('dy', '0.31em') .attr('x', -15) .attr('text-anchor', 'end') .text(({ data: { name } }: any) => name) // text for non-root and non-leaf nodes const middleNodeText = nodeEnter .filter( ({ data: { name } }: any) => name !== NODE_STORES && name !== NODE_TIFLASH() && name !== NODE_TIKV() ) .append('text') middleNodeText .append('tspan') .text(({ data: { name } }: any) => shortStrMap[name] ?? name) .attr('x', -15) .attr('dy', '-0.2em') .attr('text-anchor', 'end') middleNodeText .append('tspan') .text(({ data: { value } }: any) => { if (value.length <= MAX_STR_LENGTH) { return value } let shortStr = shortStrMap[value] ?? value if (shortStr.length > MAX_STR_LENGTH) { const midIdx = Math.round(MAX_STR_LENGTH / 2) - 1 shortStr = shortStr.slice(0, midIdx) + '..' + shortStr.slice(shortStr.length - midIdx, shortStr.length) } return shortStr }) .attr('x', -15) .attr('dy', '1em') .attr('text-anchor', 'end') // text for leaf nodes const leafNodeText = nodeEnter .filter( ({ data: { name } }: any) => name === NODE_TIFLASH() || name === NODE_TIKV() ) .append('text') leafNodeText .append('tspan') .text(({ data: { name } }: any) => name) .attr('x', 15) .attr('dy', '-0.2em') leafNodeText .append('tspan') .text(({ data: { value } }: any) => shortStrMap[value] ?? value) .attr('x', 15) .attr('dy', '1em') // transition nodes to their new position node .merge(nodeEnter as any) .transition(transition as any) .attr('transform', (d: any) => `translate(${d.y},${d.x})`) .attr('fill-opacity', 1) .attr('stroke-opacity', 1) // transition exiting nodes to the parent's new position node .exit() .transition(transition as any) .remove() .attr('transform', (d) => `translate(${source.y},${source.x})`) .attr('fill-opacity', 0) .attr('stroke-opacity', 0) // update the links const link = gLink.selectAll('path').data(links, (d: any) => d.target.id) // enter any new links at the parent's previous position const linkEnter = link .enter() .append('path') .attr('d', (_d) => { const o = { x: source.x0, y: source.y0 } return diagonal({ source: o, target: o } as any) }) // transition links to their new position link .merge(linkEnter as any) .transition(transition as any) .attr('d', diagonal as any) // transition exiting nodes to the parent's new position link .exit() .transition(transition as any) .remove() .attr('d', (_d) => { const o = { x: source.x, y: source.y } return diagonal({ source: o, target: o } as any) }) // stash the old positions for transition root.eachBefore((d) => { d.x0 = d.x d.y0 = d.y }) } function resizeHandler() { divWidth = divRef.current?.clientWidth || 0 const dy = divWidth / (root.height + 2) tree = d3.tree().nodeSize([dx, dy]) update(root) } window.addEventListener('resize', resizeHandler) return () => { window.removeEventListener('resize', resizeHandler) } }, [dataSource, getMinHeight, onReload, shortStrMap]) return (
*{t('cluster_info.list.store_topology.tooltip')}
) } // refs: // https://observablehq.com/@d3/tidy-tree // https://observablehq.com/@d3/collapsible-tree ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { ClusterinfoGetHostsInfoResponse, TopologyStoreLocation, TopologyTiDBInfo, ClusterinfoStoreTopologyResponse, TopologyPDInfo, ClusterinfoClusterStatistics, TopologyTiCDCInfo, TopologyTiProxyInfo, TopologyTSOInfo, TopologySchedulingInfo } from '@lib/client' import { IContextConfig, ReqConfig } from '@lib/types' export interface IClusterInfoDataSource { clusterInfoGetHostsInfo( options?: ReqConfig ): AxiosPromise getStoreLocationTopology( options?: ReqConfig ): AxiosPromise getTiDBTopology(options?: ReqConfig): AxiosPromise> getStoreTopology( options?: ReqConfig ): AxiosPromise getPDTopology(options?: ReqConfig): AxiosPromise> getTiCDCTopology(options?: ReqConfig): AxiosPromise> getTiProxyTopology( options?: ReqConfig ): AxiosPromise> getTSOTopology(options?: ReqConfig): AxiosPromise> getSchedulingTopology( options?: ReqConfig ): AxiosPromise> topologyTidbAddressDelete( address: string, options?: ReqConfig ): AxiosPromise clusterInfoGetStatistics( options?: ReqConfig ): AxiosPromise } export interface IClusterInfoContext { ds: IClusterInfoDataSource cfg: IContextConfig } export const ClusterInfoContext = createContext( null ) export const ClusterInfoProvider = ClusterInfoContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router, Route, Routes, Navigate } from 'react-router-dom' import { Root } from '@lib/components' import ListPage from './pages/List' import { addTranslations } from '@lib/utils/i18n' import { useLocationChange } from '@lib/hooks/useLocationChange' import { ClusterInfoContext } from './context' import translations from './translations' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> } /> ) } const App = () => { const ctx = useContext(ClusterInfoContext) if (ctx === null) { throw new Error('ClusterInfoContext must not be null') } return ( ) } export default App export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/pages/List.module.less ================================================ @import 'antd/es/style/themes/default.less'; .card_tab_navs { padding-left: @padding-page; // 48px padding-right: @padding-page; // 48px height: @padding-page; // 48px margin-bottom: @padding-md; // 16px border-bottom: 1px solid @gray-4; :global { .ant-tabs-ink-bar { height: @outline-width; // 2px } } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/pages/List.tsx ================================================ import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky' import React from 'react' import { useTranslation } from 'react-i18next' import { useNavigate, useParams } from 'react-router-dom' import { Card } from '@lib/components' import CardTabs from '@lib/components/CardTabs' import InstanceTable from '../components/InstanceTable' import HostTable from '../components/HostTable' import DiskTable from '../components/DiskTable' import StoreLocation from '../components/StoreLocation' import Statistics from '../components/Statistics' import styles from './List.module.less' function renderTabBar(props, DefaultTabBar) { return ( ) } export default function ListPage() { const { tabKey } = useParams() const navigate = useNavigate() const { t } = useTranslation() const tabs = [ { key: 'instance', title: t('cluster_info.list.instance_table.title'), content: () => }, { key: 'host', title: t('cluster_info.list.host_table.title'), content: () => }, { key: 'disk', title: t('cluster_info.list.disk_table.title'), content: () => }, { key: 'store_topology', title: t('cluster_info.list.store_topology.title'), content: () => }, { key: 'statistics', title: t('cluster_info.list.statistics.title'), content: () => } ] return ( { navigate(`/cluster_info/${key}`) }} renderTabBar={renderTabBar} animated={false} tabs={tabs} /> ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/status/status.ts ================================================ export const STATUS_UNREACHABLE = 0 export const STATUS_UP = 1 export const STATUS_TOMBSTONE = 2 export const STATUS_OFFLINE = 3 export const STATUS_DOWN = 4 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/translations/en.yaml ================================================ cluster_info: nav_title: Cluster Info list: instance_table: title: Instances columns: node: Address version: Version status: Status up_time: Up Time deploy_path: Deployment Directory git_hash: Git Hash actions: hide_db: tooltip: Hide confirm: Do you want to hide this {{distro.tidb}} instance? host_table: title: Hosts columns: host: Host Address cpu: CPU cpu_arch: CPU Arch cpu_usage: CPU Usage memory: Memory memory_usage: Memory Usage instances: Instances instanceUnavailable: Failed to get the host information disk_table: title: Disks columns: host: Host Address mount_dir: Mount Directory fs: File System disk_size: Disk Capacity disk_usage: Disk Usage instances: Instances store_topology: title: Store Topology tooltip: You can also zoom in or out by pressing CTRL and scrolling mouse wheel statistics: title: Statistics summary_title: Cluster Summary field: version: Version instances: '# Instances' hosts: '# Hosts that instances deployed' memory_capacity: Σ Memory capacity (of all hosts) physical_cores: Σ CPU physical cores (of all hosts) logical_cores: Σ CPU logical cores (of all hosts) message: instance_down: 'Some instances are down in {{n}} host(s) so that host related information may be inccurate.' sub_statistics: Sub-statistics below are counted by instance kinds. The sum of host metrics in sub-statistics can be larger "Cluster Summary" when different instances are deployed in the same host. error: load: 'Load component {{comp}} error: {{cause}}' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ClusterInfo/translations/zh.yaml ================================================ cluster_info: nav_title: 集群信息 list: instance_table: title: 实例 columns: node: 地址 version: 版本 status: 状态 up_time: 启动时间 deploy_path: 部署路径 git_hash: Git 哈希值 actions: hide_db: tooltip: 隐藏 confirm: 您确认要隐藏该 {{distro.tidb}} 实例吗? host_table: title: 主机 columns: host: 主机地址 cpu: CPU cpu_arch: CPU 架构 cpu_usage: CPU 使用率 memory: 物理内存 memory_usage: 内存使用率 instances: 实例 instanceUnavailable: 获取主机信息失败 disk_table: title: 磁盘 columns: host: 主机地址 mount_dir: 磁盘挂载点 fs: 文件系统 disk_size: 磁盘容量 disk_usage: 磁盘使用率 instances: 实例 store_topology: title: 存储拓扑 tooltip: 按住 Ctrl 键并滑动鼠标滚轮也可以缩放 statistics: title: 统计 summary_title: 集群总计 field: version: 版本 instances: 总实例数量 hosts: 实例部署的总机器数量 memory_capacity: 内存总量总和 (按实例部署的机器计算) physical_cores: CPU 物理核心数总和 (按实例部署的机器计算) logical_cores: CPU 逻辑核心数总和 (按实例部署的机器计算) message: instance_down: '由于有 {{n}} 台机器上的所有实例都未启动或无法访问,因此统计中关于机器的指标可能会不准确。' sub_statistics: 子统计按不同实例类型分别计算。当一个机器上部署了不同类型实例时,以下子统计的机器指标累加起来会超过“集群总计”数量。 error: load: '加载组件 {{comp}} 失败: {{cause}}' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Configuration/InlineEditor.tsx ================================================ import React, { useState, useCallback, useEffect } from 'react' import { EditOutlined } from '@ant-design/icons' import { Input, Popover, Button, Space, Tooltip } from 'antd' import { useMemoizedFn } from 'ahooks' interface IInlineEditorProps { title?: string value: any displayValue: string onSave?: (newValue: any) => Promise } function valueWithSameType(newValue, oldValue) { if (typeof oldValue === 'string') { return newValue } else if (typeof oldValue === 'number') { // Note: `Number()` is more strict than `parseFloat()`. const v = Number(newValue) if (isNaN(v)) { throw new Error(`"${newValue}" is not a number`) } return v } else if (typeof oldValue === 'boolean') { switch (String(newValue).toLowerCase().trim()) { case 'true': case 'yes': case '1': return true case 'false': case 'no': case '0': return false default: throw new Error(`"${newValue}" is not a boolean`) } } else { // Otherwise, return as string return newValue } } function InlineEditor({ value, displayValue, title, onSave }: IInlineEditorProps) { const [isVisible, setIsVisible] = useState(false) const [inputVal, setInputVal] = useState(displayValue) const [isPosting, setIsPosting] = useState(false) const handleCancel = useCallback(() => { setIsVisible(false) setInputVal(displayValue) }, [displayValue]) const handleSave = useMemoizedFn(async () => { if (!onSave) { setIsVisible(false) return } try { setIsPosting(true) // PD only accept modified config in the same value type, // i.e. true => false, but not true => "false" const r = await onSave(valueWithSameType(inputVal, value)) if (r !== false) { // When onSave returns non-false, input value is not reverted and only popup is hidden setIsVisible(false) } else { // When onSave returns false, popup is not hidden and value is reverted setInputVal(displayValue) } } catch (e) { setInputVal(displayValue) setIsVisible(false) } finally { setIsPosting(false) } }) const handleInputValueChange = useCallback((e) => { setInputVal(e.target.value) }, []) useEffect(() => { setInputVal(displayValue) }, [displayValue]) const renderPopover = useMemoizedFn(() => { return (
) }) return ( {' '} {displayValue} ) } export default InlineEditor ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Configuration/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { ConfigurationEditRequest, ConfigurationEditResponse, ConfigurationAllConfigItems } from '@lib/client' import { ReqConfig } from '@lib/types' export interface IConfigurationDataSource { configurationEdit( request: ConfigurationEditRequest, options?: ReqConfig ): AxiosPromise configurationGetAll( options?: ReqConfig ): AxiosPromise } export interface IConfigurationContext { ds: IConfigurationDataSource } export const ConfigurationContext = createContext( null ) export const ConfigurationProvider = ConfigurationContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Configuration/index.tsx ================================================ import React, { useMemo, useCallback, useRef, useState, useEffect, useContext } from 'react' import { Routes, Route, HashRouter as Router } from 'react-router-dom' import { IGroup, IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky' import { Modal, Spin, Tooltip, Input } from 'antd' import { useMemoizedFn, useDebounce } from 'ahooks' import { useTranslation } from 'react-i18next' import { LoadingOutlined } from '@ant-design/icons' import { Root, CardTable, Card, Pre } from '@lib/components' import { useClientRequest } from '@lib/utils/useClientRequest' import { ConfigurationItem } from '@lib/client' import { addTranslations } from '@lib/utils/i18n' import { useLocationChange } from '@lib/hooks/useLocationChange' import InlineEditor from './InlineEditor' import { ConfigurationContext } from './context' import translations from './translations' addTranslations(translations) interface IRow extends ConfigurationItem { kind: string } interface IValueProps { item: IRow onSaved?: () => void } const loadingSpinner = function Value({ item, onSaved }: IValueProps) { const ctx = useContext(ConfigurationContext) const handleSave = useMemoizedFn(async (newValue) => { try { const resp = await ctx!.ds.configurationEdit({ id: item.id, kind: item.kind, new_value: newValue }) if ((resp?.data?.warnings?.length ?? 0) > 0) { Modal.warning({ title: 'Edit configuration is partially done', content: (
{resp.data.warnings?.map((w) => w.message).join('\n\n')}
) }) } } catch (e) { return false } onSaved?.() }) const stringValue = String(item.value) if (item.is_multi_value) { return ( (multiple values){' '} {stringValue} ) } else if (!item.is_editable) { return ( {stringValue} ) } else { // Note: We preserve the original value so that newValue's type can be inferred. return ( ) } } function getKey(item: IRow) { return `${item.kind}.${item.id}` } function Configuration() { const ctx = useContext(ConfigurationContext) if (ctx === null) { throw new Error('ConfigurationContext must not be null') } const { data, isLoading, error, sendRequest } = useClientRequest( ctx!.ds.configurationGetAll ) const { t } = useTranslation() const [filterValueLower, setFilterValueLower] = useState('') const debouncedFilterValue = useDebounce(filterValueLower, { wait: 200 }) const handleSaved = useCallback(() => { sendRequest() }, [sendRequest]) const handleFilterChange = useCallback((e) => { setFilterValueLower(e.target.value.toLowerCase()) }, []) const errors = useMemo(() => { if (error) { return [error] } if (data?.errors) { return data.errors } return [] }, [data, error]) const [rows, setRows] = useState([]) const [groups, setGroups] = useState([]) const lastSavedGroups = useRef([]) // When data is changed, re-calculate rows and groups. useEffect(() => { if (!data) { setRows([]) setGroups([]) lastSavedGroups.current = [] return } const newRows: IRow[] = [] const newGroups: IGroup[] = [] let startIndex = 0 for (const configKind of [ 'tidb_variable', 'pd_config', 'tikv_config', 'tidb_config' ]) { const items = data?.items?.[configKind] ?? [] for (const item of items) { if (debouncedFilterValue.length > 0) { if ( item.id?.toLowerCase().indexOf(debouncedFilterValue) === -1 && String(item.value).toLowerCase().indexOf(debouncedFilterValue) === -1 ) { continue } } newRows.push({ ...item, kind: configKind }) } newGroups.push({ key: configKind, name: t(`configuration.common.kind.${configKind}`), startIndex: startIndex, count: newRows.length - startIndex }) startIndex = newRows.length } setRows(newRows) // DetailsList internally changes the group element and add new fields. When assigning new // fresh groups, group states will be changed, result in UI state not preserved. // Thus, we update to use new groups only when groups are different. if (JSON.stringify(lastSavedGroups.current) === JSON.stringify(newGroups)) { // Update group reference, otherwise DetailsList won't update setGroups((g) => [...g]) } else { setGroups(newGroups) lastSavedGroups.current = JSON.parse(JSON.stringify(newGroups)) } }, [data, debouncedFilterValue, t]) const columns = useMemo(() => { const columns: IColumn[] = [ { key: 'key', name: 'Config', minWidth: 300, maxWidth: 300, onRender: (item) => { return ( {item.id} ) } }, { key: 'value', name: 'Value', onRender: (item) => { return }, minWidth: 300, maxWidth: 300 } ] return columns }, [handleSaved]) return (
) } function AppRoutes() { useLocationChange() return ( } /> ) } export default function () { return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Configuration/translations/en.yaml ================================================ configuration: nav_title: Configurations common: kind: tidb_variable: '{{distro.tidb}} Variables' pd_config: '{{distro.pd}} Configurations' tikv_config: '{{distro.tikv}} Configurations' tidb_config: '{{distro.tidb}} Configurations' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Configuration/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Configuration/translations/zh.yaml ================================================ configuration: nav_title: 实例配置 common: kind: tidb_variable: '{{distro.tidb}} 变量' pd_config: '{{distro.pd}} 配置' tikv_config: '{{distro.tikv}} 配置' tidb_config: '{{distro.tidb}} 配置' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { ConprofComponent, ConprofNgMonitoringConfig, ConprofEstimateSizeRes, ConprofGroupProfileDetail, ConprofGroupProfiles, TopologyTiDBInfo, ClusterinfoStoreTopologyResponse, TopologyPDInfo } from '@lib/client' import { IContextConfig, ReqConfig } from '@lib/types' export interface IConProfilingDataSource { continuousProfilingActionTokenGet( q: string, options?: ReqConfig ): AxiosPromise continuousProfilingComponentsGet( options?: ReqConfig ): AxiosPromise> continuousProfilingConfigGet( options?: ReqConfig ): AxiosPromise continuousProfilingConfigPost( request: ConprofNgMonitoringConfig, options?: ReqConfig ): AxiosPromise continuousProfilingEstimateSizeGet( options?: ReqConfig ): AxiosPromise continuousProfilingGroupProfileDetailGet( ts: number, options?: ReqConfig ): AxiosPromise continuousProfilingGroupProfilesGet( beginTime?: number, endTime?: number, options?: ReqConfig ): AxiosPromise> getTiDBTopology(options?: ReqConfig): AxiosPromise> getStoreTopology( options?: ReqConfig ): AxiosPromise getPDTopology(options?: ReqConfig): AxiosPromise> } export interface IConProfilingConfig extends IContextConfig { publicPathBase: string checkNgm?: boolean // default value is true showSetting?: boolean // default value is true enableDownloadGroup?: boolean // default value is true enableDotGraph?: boolean // default value is true enablePreviewGoroutine?: boolean // default value is true listDuration?: number // unit hour, 1 means 1 hour, 2 means 2 hours, default value is 2 hours maxDays?: number // default value is unlimited clusterId?: string deployType?: string } export interface IConProfilingContext { ds: IConProfilingDataSource cfg: IConProfilingConfig } export const ConProfilingContext = createContext( null ) export const ConProfilingProvider = ConProfilingContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router, Route, Routes } from 'react-router-dom' import { Root, ParamsPageWrapper, NgmNotStartedGuard } from '@lib/components' import { addTranslations } from '@lib/utils/i18n' import { Detail, List } from './pages' import { ConProfilingContext } from './context' import translations from './translations' import { useLocationChange } from '@lib/hooks/useLocationChange' addTranslations(translations) function AppRoutes() { useLocationChange() const ctx = useContext(ConProfilingContext) const checkNgm = ctx?.cfg.checkNgm ?? true return ( ) : ( ) } /> ) : ( ) } /> ) } const App = () => { const ctx = useContext(ConProfilingContext) if (ctx === null) { throw new Error('ConProfilingContext must not be null') } return ( ) } export default App export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/pages/ConProfSettingForm.tsx ================================================ import React, { useState, useCallback, useMemo, useContext } from 'react' import { Form, Skeleton, Switch, Input, Space, Button, Modal, Select } from 'antd' import { ExclamationCircleOutlined } from '@ant-design/icons' import { useTranslation, TFunction } from 'react-i18next' import { getValueFormat } from '@baurine/grafana-value-formats' import { ConprofContinuousProfilingConfig } from '@lib/client' import { useClientRequest } from '@lib/utils/useClientRequest' import { DrawerFooter, ErrorBar, InstanceSelect } from '@lib/components' import { useIsWriteable } from '@lib/utils/store' import { telemetry } from '../utils/telemetry' import { ConProfilingContext } from '../context' const ONE_DAY_SECONDS = 24 * 60 * 60 const RETENTION_SECONDS = [ 3 * ONE_DAY_SECONDS, 5 * ONE_DAY_SECONDS, 10 * ONE_DAY_SECONDS ] function translateSecToDay(seconds: number, t: TFunction) { // in our case, the seconds value must be the multiple of one day seconds if (seconds % ONE_DAY_SECONDS !== 0) { console.warn(`${seconds} is not the mulitple of one day seconds`) } const day = seconds / ONE_DAY_SECONDS return t('conprof.settings.profile_retention_duration_option', { d: day }) } interface Props { onClose: () => void onConfigUpdated: () => any } function ConProfSettingForm({ onClose, onConfigUpdated }: Props) { const ctx = useContext(ConProfilingContext) const [submitting, setSubmitting] = useState(false) const { t } = useTranslation() const isWriteable = useIsWriteable() const { data: initialConfig, isLoading: loading, error } = useClientRequest(() => ctx!.ds.continuousProfilingConfigGet({ handleError: 'custom' }) ) const { data: estimateSize } = useClientRequest(() => ctx!.ds.continuousProfilingEstimateSizeGet({ handleError: 'custom' }) ) const dataRetentionSeconds = useMemo(() => { const curRetentionSec = initialConfig?.continuous_profiling?.data_retention_seconds if ( curRetentionSec && RETENTION_SECONDS.indexOf(curRetentionSec) === -1 && // filter out the duration that is not multiple of ONE_DAY_SECONDS curRetentionSec % ONE_DAY_SECONDS === 0 ) { return RETENTION_SECONDS.concat(curRetentionSec).sort() } return RETENTION_SECONDS }, [initialConfig]) const handleSubmit = useCallback( (values) => { async function updateConfig(values) { const newConfig: ConprofContinuousProfilingConfig = { enable: values.enable, data_retention_seconds: values.data_retention_seconds } try { setSubmitting(true) await ctx!.ds.continuousProfilingConfigPost({ continuous_profiling: newConfig }) telemetry.saveSettings(newConfig) onClose() onConfigUpdated() } finally { setSubmitting(false) } } if (!values.enable) { // confirm Modal.confirm({ title: t('conprof.settings.close_feature'), icon: , content: t('conprof.settings.close_feature_confirm'), okText: t('conprof.settings.actions.close'), cancelText: t('conprof.settings.actions.cancel'), okButtonProps: { danger: true }, onOk: () => updateConfig(values) }) } else { updateConfig(values) } }, [t, onClose, onConfigUpdated, ctx] ) return ( <> {error && } {loading && } {!loading && initialConfig && (
prev.enable !== cur.enable} > {({ getFieldValue }) => getFieldValue('enable') && ( <> ) }
)} ) } export default ConProfSettingForm ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/pages/Detail.tsx ================================================ import { Badge, Button, Modal, Space } from 'antd' import React, { useCallback, useContext, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { ArrowLeftOutlined } from '@ant-design/icons' import { useMemoizedFn } from 'ahooks' import { upperFirst } from 'lodash' import { IGroup } from 'office-ui-fabric-react/lib/DetailsList' import { ConprofProfileDetail } from '@lib/client' import { Card, CardTable, DateTime, Descriptions, Head } from '@lib/components' import { useClientRequest } from '@lib/utils/useClientRequest' import { instanceKindName, InstanceKinds } from '@lib/utils/instanceTable' import useQueryParams from '@lib/utils/useQueryParams' import { telemetry } from '../utils/telemetry' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import { ConProfilingContext } from '../context' enum Action { VIEW_FLAMEGRAPH = 'view_flamegraph', VIEW_GRAPH = 'view_graph', VIEW_TEXT = 'view_text', DOWNLOAD = 'download' } const profileTypeSortOrder: { [key: string]: number } = { profile: 1, heap: 2, goroutine: 3, mutex: 4 } export default function Page() { const ctx = useContext(ConProfilingContext) const enableDownloadGroup = ctx?.cfg.enableDownloadGroup ?? true const enableDotGraph = ctx?.cfg.enableDotGraph ?? true const enablePreviewGoroutine = ctx?.cfg.enablePreviewGoroutine ?? true const { t } = useTranslation() const { ts } = useQueryParams() const { data: groupProfileDetail, isLoading: groupDetailLoading, error: groupDetailError } = useClientRequest((reqConfig) => ctx!.ds.continuousProfilingGroupProfileDetailGet(ts, reqConfig) ) const profileDuration = groupProfileDetail?.profile_duration_secs || 0 const [tableData, groupData] = useMemo(() => { const newRows: ConprofProfileDetail[] = [] const newGroups: IGroup[] = [] let startIndex = 0 const profiles = groupProfileDetail?.target_profiles || [] profiles.sort((a, b) => { if (a.target!.component! > b.target!.component!) { return 1 } else { return ( (profileTypeSortOrder[a.profile_type!] ?? 0) - (profileTypeSortOrder[b.profile_type!] ?? 0) ) } }) for (const kind of InstanceKinds) { profiles.forEach((p) => { if (p.target?.component === kind) { newRows.push(p) } }) if (newRows.length - startIndex > 0) { newGroups.push({ key: instanceKindName(kind), name: instanceKindName(kind), startIndex: startIndex, count: newRows.length - startIndex }) startIndex = newRows.length } } return [newRows, newGroups] }, [groupProfileDetail]) const handleClick = useMemoizedFn( async (action: string, rec: ConprofProfileDetail) => { const { profile_type, target } = rec const { component, address } = target! let dataFormat = '' if (component === 'tikv' && profile_type === 'heap') { switch (action) { case Action.VIEW_FLAMEGRAPH: // tikv heap flamegraph uses Brendan Gregg's collapsed stack format which is text based dataFormat = 'text' break case Action.VIEW_GRAPH: dataFormat = 'svg' break case Action.DOWNLOAD: dataFormat = 'jeprof' break default: } } else if (component === 'tiflash' && profile_type === 'heap') { switch (action) { case Action.VIEW_FLAMEGRAPH: dataFormat = 'text' break case Action.VIEW_GRAPH: dataFormat = 'svg' break case Action.DOWNLOAD: dataFormat = 'jeprof' break default: } } else { switch (action) { case Action.VIEW_GRAPH: dataFormat = 'svg' break case Action.VIEW_TEXT: dataFormat = 'text' break case Action.VIEW_FLAMEGRAPH: case Action.DOWNLOAD: dataFormat = 'protobuf' break default: } } const res = await ctx!.ds.continuousProfilingActionTokenGet( `ts=${ts}&profile_type=${profile_type}&component=${component}&address=${address}&data_format=${dataFormat}` ) const token = res.data if (!token) { return } telemetry.clickAction({ action, profile_type: rec.profile_type!, component: component! }) if (action === Action.VIEW_GRAPH || action === Action.VIEW_TEXT) { const profileURL = `${ ctx!.cfg.apiPathBase }/continuous_profiling/single_profile/view?token=${token}` window.open(profileURL, '_blank') return } if (action === Action.VIEW_FLAMEGRAPH) { // view flamegraph by speedscope const speedscopeTitle = `${rec.target?.component}_${rec.target?.address}_${rec.profile_type}` const profileURL = `${ ctx!.cfg.apiPathBase }/continuous_profiling/single_profile/view?token=${token}` const speedscopeURL = `${ ctx!.cfg.publicPathBase }/speedscope/#profileURL=${encodeURIComponent( profileURL + `&output_type=${dataFormat}` )}&title=${speedscopeTitle}` window.open(speedscopeURL, '_blank') return } if (action === Action.DOWNLOAD) { window.location.href = `${ ctx!.cfg.apiPathBase }/continuous_profiling/download?token=${token}` return } } ) const handleDownloadGroup = useCallback(async () => { const res = await ctx!.ds.continuousProfilingActionTokenGet( `ts=${ts}&data_format=protobuf` ) const token = res.data if (!token) { return } telemetry.downloadProfilingGroupResult() window.location.href = `${ ctx!.cfg.apiPathBase }/continuous_profiling/download?token=${token}` }, [ts, ctx]) const columns = useMemo( () => [ { name: t('conprof.detail.table.columns.instance'), key: 'instance', minWidth: 100, maxWidth: 200, onRender: (record) => record.target.address }, { name: t('conprof.detail.table.columns.content'), key: 'content', minWidth: 100, maxWidth: 100, onRender: (record) => { const profileType = record.profile_type // in the cloud ngm, the `profile` is `cpu` if (profileType === 'profile' || profileType === 'cpu') { return `CPU - ${profileDuration}s` } return upperFirst(profileType) } }, { name: t('conprof.detail.table.columns.status'), key: 'status', minWidth: 100, maxWidth: 150, onRender: (record) => { if (record.state === 'finished' || record.state === 'success') { return ( ) } if (record.state === 'failed') { return ( ) } return } }, { name: t('conprof.detail.table.columns.view_as.title'), key: 'view_as', minWidth: 250, maxWidth: 400, onRender: (record) => { if (record.state === 'failed') { return ( { Modal.error({ title: 'Profile Error', content: record.error }) }} > {t('conprof.detail.table.columns.view_as.error')} ) } if (record.state !== 'finished' && record.state !== 'success') { return <> } const rec = record as ConprofProfileDetail let actionsKey: string[] = [] if (rec.profile_type === 'goroutine') { if (enablePreviewGoroutine) { actionsKey = [Action.VIEW_TEXT] } else { actionsKey = [Action.DOWNLOAD] } } else { if (enableDotGraph) { actionsKey = [ Action.VIEW_FLAMEGRAPH, Action.VIEW_GRAPH, Action.DOWNLOAD ] } else { actionsKey = [Action.VIEW_FLAMEGRAPH, Action.DOWNLOAD] } } return ( {actionsKey.map((action) => { return ( handleClick(action, record)} key={action}> {t(`conprof.detail.table.columns.view_as.${action}`)} ) })} ) } } ], [t, profileDuration, handleClick] ) return (
{t('conprof.detail.head.back')} } titleExtra={ enableDownloadGroup && ( ) } />
{groupProfileDetail && ( )}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/pages/List.module.less ================================================ @import 'antd/es/style/themes/default.less'; .list { &_container { display: flex; flex-direction: column; height: 100vh; } &_toolbar { @media only screen and (max-width: @screen-md) { flex-direction: column; } } } .alert_container { margin-left: @padding-page; margin-right: @padding-page; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/pages/List.tsx ================================================ import { Badge, Tooltip, Space, Drawer, Result, Button, Alert, Form } from 'antd' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import React, { useContext, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { useMemoizedFn } from 'ahooks' import { LoadingOutlined, QuestionCircleOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons' import dayjs, { Dayjs } from 'dayjs' import { Card, CardTable, Toolbar, DatePicker } from '@lib/components' import DateTime from '@lib/components/DateTime' import openLink from '@lib/utils/openLink' import { useClientRequest } from '@lib/utils/useClientRequest' import { instanceKindName } from '@lib/utils/instanceTable' import ConProfSettingForm from './ConProfSettingForm' import styles from './List.module.less' import { telemetry } from '../utils/telemetry' import { isDistro } from '@lib/utils/distro' import { ConProfilingContext } from '../context' import { useURLTimeRange } from '@lib/hooks/useURLTimeRange' export default function Page() { const ctx = useContext(ConProfilingContext) const showSetting = ctx?.cfg.showSetting ?? true const durationHour = ctx?.cfg.listDuration ?? 2 const maxDays = ctx?.cfg.maxDays const { timeRange, setTimeRange } = useURLTimeRange() const endTime = timeRange.type === 'recent' ? '' : `${timeRange.value[1]}` const setEndTime = (v) => { if (!v) { setTimeRange({ type: 'recent', value: durationHour * 60 * 60 }) } else { const endUnix = v.unix() setTimeRange({ type: 'absolute', value: [endUnix - durationHour * 60 * 60, endUnix] }) } } const rangeEndTime: Dayjs | undefined = useMemo(() => { let _rangeEndTime: Dayjs | undefined if (typeof endTime === 'string') { if (endTime === '') { _rangeEndTime = undefined } else { _rangeEndTime = dayjs(parseInt(endTime) * 1000) } } else { _rangeEndTime = endTime } return _rangeEndTime }, [endTime]) const { data: historyTable, isLoading: listLoading, error: historyError, sendRequest: reloadGroupProfiles } = useClientRequest(() => { let _rangeEndTime: Dayjs if (rangeEndTime === undefined) { _rangeEndTime = dayjs() } else { _rangeEndTime = rangeEndTime } const _rangeStartTime = _rangeEndTime.subtract(durationHour, 'h') return ctx!.ds.continuousProfilingGroupProfilesGet( _rangeStartTime.unix(), _rangeEndTime.unix(), { handleError: 'custom' } ) }) const { t } = useTranslation() const navigate = useNavigate() const handleRowClick = useMemoizedFn( (rec, _idx, ev: React.MouseEvent) => { telemetry.clickProfilingListRecord(rec) openLink(`/continuous_profiling/detail?ts=${rec.ts}`, ev, navigate) } ) const historyTableColumns = useMemo( () => [ { name: t('conprof.list.table.columns.targets'), key: 'targets', minWidth: 250, maxWidth: 300, onRender: (rec) => { const { tikv, tidb, pd, tiflash, ticdc, tiproxy } = rec.component_num let s = `${tikv} ${instanceKindName( 'tikv' )}, ${tidb} ${instanceKindName('tidb')}, ${pd} ${instanceKindName( 'pd' )}, ${tiflash} ${instanceKindName('tiflash')}` // to be compatible with old version // this field doesn't not exist in the old version if (ticdc !== undefined) { s = `${s}, ${ticdc} ${instanceKindName('ticdc')}` } if (tiproxy !== undefined) { s = `${s}, ${tiproxy} ${instanceKindName('tiproxy')}` } return s } }, { name: t('conprof.list.table.columns.status'), key: 'status', minWidth: 100, maxWidth: 150, onRender: (rec) => { if (rec.state === 'running') { return ( ) } if (rec.state === 'finished' || rec.state === 'success') { // all success return ( ) } if ( rec.state === 'finished_with_error' || rec.state === 'partial failed' ) { // partial failed return ( ) } if (rec.state === 'failed') { // all failed return ( ) } return } }, { name: t('conprof.list.table.columns.start_at'), key: 'ts', minWidth: 200, maxWidth: 250, onRender: (rec) => { return } }, { name: t('conprof.list.table.columns.duration'), key: 'duration', minWidth: 100, maxWidth: 150, fieldName: 'profile_duration_secs' } ], [t] ) const [showSettings, setShowSettings] = useState(false) const { data: ngMonitoringConfig, sendRequest: reloadConfig } = useClientRequest(ctx!.ds.continuousProfilingConfigGet) const conprofIsDisabled = useMemo( () => ngMonitoringConfig?.continuous_profiling?.enable === false, [ngMonitoringConfig] ) function refresh() { reloadConfig() reloadGroupProfiles() telemetry.clickReloadIcon(rangeEndTime) } function handleFinish(fieldsValues) { setEndTime(fieldsValues['rangeEndTime'] || '') setTimeout(() => { reloadGroupProfiles() telemetry.clickQueryButton(rangeEndTime) }, 0) } return (
{ if (maxDays === undefined) { return false } return ( current < dayjs().subtract(maxDays, 'd').startOf('d') || current > dayjs().endOf('d') ) }} showTime onOpenChange={(open) => open && telemetry.openTimeRangePicker() } onChange={(v) => telemetry.selectTimeRange(v?.toString())} /> -{durationHour}h
{listLoading ? ( ) : ( )} {showSetting && ( { setShowSettings(true) telemetry.clickSettings('settingIcon') }} /> )} {!isDistro() && ( { window.open(t('conprof.settings.help_url'), '_blank') }} /> )}
{conprofIsDisabled && historyTable && historyTable.length > 0 && (
)} {conprofIsDisabled && historyTable?.length === 0 ? ( {!isDistro() && ( )} } /> ) : (
)} setShowSettings(false)} destroyOnClose={true} > setShowSettings(false)} onConfigUpdated={reloadConfig} />
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/pages/index.ts ================================================ import List from './List' import Detail from './Detail' export { List, Detail } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/translations/en.yaml ================================================ conprof: nav_title: Continuous Profiling list: toolbar: refresh: Refresh settings: Settings range_end: Range End Time range_duration: Range Duration query: Query control_form: title: Start Profiling Instances enable_tooltip: This feature is enabled, you can disable it in the settings disable_tooltip: This feature is not enabled, you can enable it in the settings table: title: Profiling History columns: targets: Instances start_at: Start At duration: Duration (sec) status: Status status: running: Running finished: Finished failed: Failed partial_finished: Partial Finished unknown: Unknown actions: detail: Detail detail: head: back: History title: Profiling Detail start_at: Start At download: Download Profiling Result table: columns: instance: Instance kind: Component content: Content status: Status view_as: title: View As error: Error Information view_flamegraph: FlameGraph view_graph: DotGraph download: RawData view_text: RawData status: finished: Finished error: Error settings: title: Settings disabled_result: title: Feature Not Enabled sub_title: Continuous Profiling feature is not enabled. You can modify settings to enable the feature and wait for new data being collected. disabled_with_history: Continuous Profiling feature is not enabled, but you still can view history result. You can modify settings to enable the feature. open_settings: Open Settings switch: Enable Feature switch_tooltip: After being enabled, Continuous Profiling will generate instance profiling results continuously. profile_targets: Profiling Targets profile_targets_tooltip: | After being enabled, Continuous Profiling will profile all instances including newly created instances. There are {{n}} instances, and Continuous Profiling will produce {{size}} results every day. profile_duration: Profiling Duration profile_duration_tooltip: profile_interval: Profiling Interval profile_interval_tooltip: profile_retention_duration: Retention Duration profile_retention_duration_tooltip: | The profiling results are persisted in the disk. Those exceeding the retention duration are deleted. This setting works for all results. profile_retention_duration_option: '{{d}} days' close_feature: Disable Continuous Profiling Feature close_feature_confirm: Are you sure want to disable this feature, it will stop continuous profiling, history result will be kept. actions: close: Disable cancel: Cancel help: Help help_url: https://docs.pingcap.com/tidb/dev/continuous-profiling ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/translations/zh.yaml ================================================ conprof: nav_title: 持续分析 toolbar: refresh: Refresh list: toolbar: refresh: 刷新 settings: 设置 range_end: 区间结束时间 range_duration: 区间长度 query: 查询 control_form: title: 开始性能分析 enable_tooltip: 该功能已开启,你可以在设置中关闭 disable_tooltip: 该功能未启用,你可以在设置中启用 table: title: 性能分析历史 columns: targets: 实例 start_at: 开始时间 duration: 时长(秒) status: 状态 status: running: 分析中 finished: 完成 failed: 失败 partial_finished: 部分完成 unknown: 未知 actions: detail: 详情 detail: head: back: 历史记录 title: 性能分析详情 start_at: 开始时间 download: 下载性能分析结果 table: columns: instance: 实例 kind: 组件 content: 内容 status: 状态 view_as: title: 查看方式 error: 错误信息 view_flamegraph: 火焰图 view_graph: 关系图 download: 原始数据 view_text: 原始数据 status: finished: 完成 error: 错误 settings: title: 设置 disabled_result: title: 该功能未启用 sub_title: 持续性能分析功能未启用。您可以修改设置打开该功能后等待新数据收集。 disabled_with_history: 持续性能分析功能未启用,但仍然可以查看历史数据。您可以修改设置打开该功能。 open_settings: 打开设置 switch: 启用功能 switch_tooltip: 是否启用持续分析功能,启用后,会持续产出实例性能分析结果。 profile_targets: 分析范围 profile_targets_tooltip: | 分析所有实例,在新实例创建后,也会自动加入分析范围。 目前有 {{n}} 个实例,预计每日生成 {{size}} 分析结果文件。 profile_duration: 分析时长 profile_duration_tooltip: profile_interval: 执行周期 profile_interval_tooltip: profile_retention_duration: 保留时间 profile_retention_duration_tooltip: 分析结果会持久化到磁盘中,超过保留时间会被回收。该配置对所有结果生效,包括历史结果。 profile_retention_duration_option: '{{d}} 天' close_feature: 关闭持续分析功能 close_feature_confirm: 确认要关闭该功能吗?关闭后将停止持续分析,历史结果会继续保留。 actions: close: 确认 cancel: 取消 help: 帮助 help_url: https://docs.pingcap.com/zh/tidb/dev/continuous-profiling ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ContinuousProfiling/utils/telemetry.ts ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import { ConprofContinuousProfilingConfig } from '@lib/client' import { mixpanel } from '@lib/utils/telemetry' import { Dayjs } from 'dayjs' export const telemetry = { clickSettings(type: 'firstTimeTips' | 'settingIcon') { mixpanel.track('Conprof: Click Settings', { type }) }, saveSettings(settings: ConprofContinuousProfilingConfig) { mixpanel.track('Conprof: Save Settings', { settings }) }, openTimeRangePicker() { mixpanel.track('Conprof: Open Time Range Picker') }, selectTimeRange(date: string = 'now') { mixpanel.track('Conprof: Select Time Range', { date }) }, clickQueryButton(endTime?: Dayjs) { mixpanel.track('Conprof: Click Query Button', { endTime: endTime?.toString() || 'now' }) }, clickReloadIcon(endTime?: Dayjs) { mixpanel.track('Conprof: Click Reload Icon', { endTime: endTime?.toString() || 'now' }) }, clickProfilingListRecord(record) { mixpanel.track('Conprof: Click Profiling List Record', { record }) }, // conprof detail clickAction(data: { action: string component: string profile_type: string }) { mixpanel.track('Conprof Detail: Click Action', data) }, downloadProfilingGroupResult() { mixpanel.track('Conprof Detail: Download Profiling Group Result') } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Deadlock/components/DeadlockChainGraph.tsx ================================================ import React, { useMemo } from 'react' import { DeadlockModel } from '@lib/client' interface NodeMeta { x: number y: number connectInX: number connectInY: number connectOutX: number connectOutY: number } function calcCircularLayout( center: { x: number; y: number }, circularRadius: number, nodeSize: number, nodeRadius: number ): Array { let result: Array = [] const outAngle = (2 * Math.PI) / nodeSize const halfInnerAngle = (Math.PI * (nodeSize - 2)) / nodeSize / 2 let currentNodeConnectInX = center.x - Math.sin(halfInnerAngle) * nodeRadius let currentNodeConnectInY = center.y + circularRadius - Math.cos(halfInnerAngle) * nodeRadius let currentNodeConnectOutX = center.x + Math.sin(halfInnerAngle) * nodeRadius let currentNodeConnectOutY = center.y + circularRadius - Math.cos(halfInnerAngle) * nodeRadius let angle = 0 for (let i = 0; i < nodeSize; ++i) { angle += outAngle const x = center.x + circularRadius * Math.sin(angle) const y = center.y + circularRadius * Math.cos(angle) result.push({ x: x, y: y, connectInX: currentNodeConnectInX, connectInY: currentNodeConnectInY, connectOutX: currentNodeConnectOutX, connectOutY: currentNodeConnectOutY }) const newNodeConnectInX = (currentNodeConnectInX - center.x) * Math.cos(outAngle) - (currentNodeConnectInY - center.y) * Math.sin(outAngle) + center.x const newNodeConnectInY = (currentNodeConnectInX - center.x) * Math.sin(outAngle) + (currentNodeConnectInY - center.y) * Math.cos(outAngle) + center.y currentNodeConnectInX = newNodeConnectInX currentNodeConnectInY = newNodeConnectInY const newNodeConnectOutX = (currentNodeConnectOutX - center.x) * Math.cos(outAngle) - (currentNodeConnectOutY - center.y) * Math.sin(outAngle) + center.x const newNodeConnectOutY = (currentNodeConnectOutX - center.x) * Math.sin(outAngle) + (currentNodeConnectOutY - center.y) * Math.cos(outAngle) + center.y currentNodeConnectOutX = newNodeConnectOutX currentNodeConnectOutY = newNodeConnectOutY } return result } interface Prop { deadlockChain: DeadlockModel[] } function DeadlockChainGraph(prop: Prop) { const data = useMemo(() => { return { nodes: prop.deadlockChain.map((it) => { return { id: it.try_lock_trx_id } }), links: prop.deadlockChain.map((d, i) => ({ source: i, target: prop.deadlockChain.findIndex( (it) => it.trx_holding_lock === d.try_lock_trx_id ), type: 'blocked', key: d.key })) } }, [prop.deadlockChain]) const nodeRadius = 30 const layout = useMemo( () => calcCircularLayout({ x: 150, y: 150 }, 100, data.nodes.length, 30), [data.nodes.length] ) return ( {data.links.map((link, index) => ( ))} {data.nodes.map((n, i) => ( {n.id?.toString().slice(n.id.toString().length - 6)} ))} ) } export default DeadlockChainGraph ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Deadlock/components/index.ts ================================================ export { default as DeadlockChainGraph } from './DeadlockChainGraph' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Deadlock/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { DeadlockModel } from '@lib/client' import { ReqConfig } from '@lib/types' export interface IDeadlockDataSource { deadlockListGet(options?: ReqConfig): AxiosPromise> } export interface IDeadlockContext { ds: IDeadlockDataSource } export const DeadlockContext = createContext(null) export const DeadlockProvider = DeadlockContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Deadlock/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import { Root } from '@lib/components' import useCache, { CacheContext } from '@lib/utils/useCache' import { useLocationChange } from '@lib/hooks/useLocationChange' import { addTranslations } from '@lib/utils/i18n' import { List, Detail } from './pages' import { DeadlockContext } from './context' import translations from './translations' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> } /> ) } export default function () { const cache = useCache(32) const ctx = useContext(DeadlockContext) if (ctx === null) { throw new Error('DeadlockContext must not be null') } return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Deadlock/pages/Detail.tsx ================================================ import React, { useContext, useState } from 'react' import { DeadlockModel } from '@lib/client' import { useLocation } from 'react-router-dom' import { useEffectOnce } from 'react-use' import { useTranslation } from 'react-i18next' import { CardTable, HighlightSQL } from '@lib/components' import { CacheContext } from '@lib/utils/useCache' import DeadlockChainGraph from '../components/DeadlockChainGraph' import { DeadlockContext } from '../context' function Detail() { const ctx = useContext(DeadlockContext) const { t } = useTranslation() const cache = useContext(CacheContext) const instance = new URLSearchParams(useLocation().search).get('instance') const id = new URLSearchParams(useLocation().search).get('id') let [isLoading, setIsLoading] = useState(true) let [items, setItems] = useState([]) useEffectOnce(() => { setIsLoading(true) if (cache?.get(`deadlock-${instance}-${id}`) !== undefined) { setItems(cache.get(`deadlock-${instance}-${id}`)) setIsLoading(false) } else { ctx!.ds.deadlockListGet().then(({ data }) => { data.forEach((it) => { let items = cache?.get(`deadlock-${it.instance}-${it.id}`) || [] items.push(it) cache?.set(`deadlock-${it.instance}-${it.id}`, items) }) setItems( data.filter( (it) => it.id?.toString() === id && it.instance?.toString() === instance ) ) setIsLoading(false) }) } }) const columns = [ { name: t('deadlock.fields.try_lock_trx_id'), key: 'try_lock_trx_id', minWidth: 100, onRender: (it) => it.try_lock_trx_id }, { name: t('deadlock.fields.current_sql'), key: 'current_sql', minWidth: 350, onRender: (it) => ( ) }, { name: t('deadlock.fields.key'), key: 'key', minWidth: 300, onRender: (it) => it.key }, { name: t('deadlock.fields.trx_holding_lock'), key: 'trx_holding_lock', minWidth: 150, onRender: (it) => it.trx_holding_lock } ] return ( <> ) } export default Detail ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Deadlock/pages/List.tsx ================================================ import { DeadlockModel } from '@lib/client' import { AnimatedSkeleton, AutoRefreshButton, Card, CardTable } from '@lib/components' import openLink from '@lib/utils/openLink' import { useMemoizedFn } from 'ahooks' import { CacheContext } from '@lib/utils/useCache' import React, { useMemo, useState, useContext } from 'react' import { useNavigate } from 'react-router-dom' import { useEffectOnce } from 'react-use' import { useTranslation } from 'react-i18next' import { DeadlockContext } from '../context' function List() { const ctx = useContext(DeadlockContext) const { t } = useTranslation() const cache = useContext(CacheContext) let [isLoading, setIsLoading] = useState(true) let [items, setItems] = useState([]) const navigate = useNavigate() const pullItems = async () => { cache?.clear() setIsLoading(true) const { data } = await ctx!.ds.deadlockListGet() data.forEach((it) => { let items = cache?.get(`deadlock-${it.instance}-${it.id}`) || [] items.push(it) cache?.set(`deadlock-${it.instance}-${it.id}`, items) }) setItems(data) setIsLoading(false) } const handleRowClick = useMemoizedFn( (record, index, ev: React.MouseEvent) => { openLink( `/deadlock/detail?id=${record.id}&instance=${record.instance}`, ev, navigate ) } ) useEffectOnce(() => { setIsLoading(true) cache?.clear() ctx!.ds .deadlockListGet() .then((res) => { setItems(res.data) res.data.forEach((it) => { let items = cache?.get(`deadlock-${it.instance}-${it.id}`) || [] items.push(it) cache?.set(`deadlock-${it.instance}-${it.id}`, items) }) }) .catch((e) => { console.error(e) }) .finally(() => { setIsLoading(false) }) }) const summary = useMemo(() => { let result = new Map() for (const item of items) { let summaryEntry = result.get(`${item.instance}-${item.id}`) || { id: item.id, instance: item.instance, occur_time: item.occur_time, items: [] } summaryEntry.items.push(item) result.set(`${item.instance}-${item.id}`, summaryEntry) } return Array.from(result.values()) }, [items]) const columns = [ { name: t('deadlock.fields.instance'), key: 'instance', minWidth: 100, onRender: (it) => it.instance }, { name: 'ID', key: 'id', minWidth: 100, onRender: (it) => it.id }, { name: 'Transaction Count', key: t('deadlock.fields.count'), minWidth: 300, onRender: (it) => it.items.length }, { name: 'Occur time', key: t('deadlock.fields.occur_time'), minWidth: 300, onRender: (it) => new Date(it.occur_time).toLocaleString() } ] return (
) } export default List ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Deadlock/pages/index.ts ================================================ import List from './List' import Detail from './Detail' export { List, Detail } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Deadlock/translations/en.yaml ================================================ deadlock: nav_title: Deadlock Information fields: instance: Instance count: Transaction count occur_time: Occur time try_lock_trx_id: Transaction acquiring the lock trx_holding_lock: Transaction holding the lock current_sql: SQL for acquiring the lock key: Locked key ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Deadlock/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Deadlock/translations/zh.yaml ================================================ deadlock: nav_title: 死锁信息 fields: instance: 实例 count: 事务数量 occur_time: 发生时间 try_lock_trx_id: 尝试加锁事务号 trx_holding_lock: 持锁事务号 current_sql: 尝试加锁 SQL key: 被锁住的 key ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/ApiForm.tsx ================================================ import React, { useCallback, useContext, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Form, Button, Space, Row, Col } from 'antd' import { isNull, isUndefined } from 'lodash' import { DownloadOutlined, UndoOutlined } from '@ant-design/icons' import { EndpointAPIDefinition, EndpointAPIParamDefinition, TopologyPDInfo, TopologyStoreInfo, TopologyTiDBInfo } from '@lib/client' import { ApiFormWidgetConfig, createFormWidget } from './widgets' import { distro } from '@lib/utils/distro' import { DebugAPIContext } from '../context' import { useIsWriteable } from '@lib/utils' export interface Topology { tidb: TopologyTiDBInfo[] tikv: TopologyStoreInfo[] tiflash: TopologyStoreInfo[] pd: TopologyPDInfo[] tiproxy: TopologyPDInfo[] } export default function ApiForm({ endpoint, topology }: { endpoint: EndpointAPIDefinition topology: Topology }) { const ctx = useContext(DebugAPIContext) const isWriteable = useIsWriteable() const { t } = useTranslation() const { id, path_params, query_params, component } = endpoint const endpointHostParamKey = useMemo( () => `${distro()[component!]?.toLowerCase()}_instance`, [component] ) const pathParams = (path_params ?? []).map((p) => { p.required = true return p }) const params = [...pathParams, ...(query_params ?? [])] const [loading, setLoading] = useState(false) const [form] = Form.useForm() const formPaths = [...params.map((p) => p.name!), endpointHostParamKey] const download = useCallback( async (values: any) => { try { setLoading(true) const { [endpointHostParamKey]: host, ...p } = values const [hostname, port] = host.split(':') const param_values = Object.entries(p).reduce((prev, [k, v]) => { if (!(isUndefined(v) || isNull(v) || v === '')) { prev[k] = v } else { // handle the null value params // fill it with the default value if it has const param = params.find((p) => p.name === k) const defVal = (param?.ui_props as any)?.default_val if (!!defVal) { prev[k] = defVal } } return prev }, {}) const resp = await ctx!.ds.debugAPIRequestEndpoint({ api_id: id, host: hostname, port: Number(port), param_values }) const token = resp.data window.location.href = `${ ctx!.cfg.apiPathBase }/debug_api/download?token=${token}` } catch (e) { console.error(e) } finally { setLoading(false) } }, [id, endpointHostParamKey, ctx] ) const endpointParam = useMemo( () => ({ name: endpointHostParamKey, required: true, ui_kind: 'host' }), [endpointHostParamKey] ) const EndpointHost = () => ( ) return (
{params.map((param) => ( ))}
) } function FormItemCol(props: React.HTMLAttributes) { return ( {props.children} ) } function ApiFormItem(widgetConfig: ApiFormWidgetConfig) { const { param } = widgetConfig return ( {createFormWidget(widgetConfig)} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/ApiList.module.less ================================================ .collapse_panel { &:not(:last-child) { border-bottom: 1px solid #eee; } :global { .ant-collapse-header { padding-left: 40px !important; } .ant-collapse-arrow { position: absolute; top: 17px; left: 16px; } } } .header { user-select: none; h4 { margin-bottom: 0; } p { margin-bottom: 0; } } .schema { color: #999; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/ApiList.tsx ================================================ import React, { useContext, useEffect, useMemo, useState } from 'react' import { Collapse, Space, Input, Empty, Alert } from 'antd' import { useTranslation, TFunction } from 'react-i18next' import { SearchOutlined } from '@ant-design/icons' import { debounce } from 'lodash' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky' import { AnimatedSkeleton, Card, Root } from '@lib/components' import { useClientRequest } from '@lib/utils/useClientRequest' import { EndpointAPIDefinition } from '@lib/client' import style from './ApiList.module.less' import ApiForm, { Topology } from './ApiForm' import { buildQueryString } from './widgets' import { distro } from '@lib/utils/distro' import { DebugAPIContext } from '../context' const getEndpointTranslationKey = (endpoint: EndpointAPIDefinition) => `debug_api.${endpoint.component}.endpoints.${endpoint.id}` const useFilterEndpoints = (endpoints?: EndpointAPIDefinition[]) => { const [keywords, setKeywords] = useState('') const nonNullEndpoints = useMemo(() => endpoints || [], [endpoints]) const [filteredEndpoints, setFilteredEndpoints] = useState(nonNullEndpoints) const { t } = useTranslation() useEffect(() => { const k = keywords.trim() if (!!k) { setFilteredEndpoints( nonNullEndpoints.filter((e) => { return ( e.id?.includes(k) || e.path?.includes(k) || t(getEndpointTranslationKey(e)).includes(k) ) }) ) } else { setFilteredEndpoints(nonNullEndpoints) } }, [nonNullEndpoints, keywords, t]) return { endpoints: filteredEndpoints, filterBy: debounce(setKeywords, 100) } } export default function Page() { const ctx = useContext(DebugAPIContext) const { t, i18n } = useTranslation() const { data: endpointData, isLoading: isEndpointLoading } = useClientRequest( ctx!.ds.debugAPIGetEndpoints ) const { endpoints, filterBy } = useFilterEndpoints(endpointData) // TODO: refine with components/InstanceSelect const { data: tidbTopology = [], isLoading: isTiDBLoading } = useClientRequest(ctx!.ds.getTiDBTopology) const { data: pdTopology = [], isLoading: isPDLoading } = useClientRequest( ctx!.ds.getPDTopology ) const { data: storeTopology, isLoading: isStoreLoading } = useClientRequest( ctx!.ds.getStoreTopology ) const { data: tiproxyTopology = [], isLoading: isTiProxyLoading } = useClientRequest(ctx!.ds.getTiProxyTopology) const topology: Topology = { tidb: tidbTopology!, tikv: storeTopology?.tikv || [], tiflash: storeTopology?.tiflash || [], pd: pdTopology!, tiproxy: tiproxyTopology! } const isTopologyLoading = isTiDBLoading || isPDLoading || isStoreLoading || isTiProxyLoading const groups = useMemo( () => endpoints.reduce((prev, endpoint) => { const groupName = endpoint.component! if (!prev[groupName]) { prev[groupName] = [] } prev[groupName].push(endpoint) return prev }, {} as { [group: string]: EndpointAPIDefinition[] }), [endpoints] ) const sortedGroups = useMemo( () => ['tidb', 'tikv', 'tiflash', 'pd', 'tiproxy'] .filter((sortKey) => groups[sortKey]) .map((sortKey) => groups[sortKey]), [groups] ) function EndpointGroup({ group }: { group: EndpointAPIDefinition[] }) { return ( {group.map((endpoint) => { const descTranslationKey = `debug_api.${endpoint.component}.endpoints.${endpoint.id}_desc` const descExists = i18n.exists(descTranslationKey) return ( } key={endpoint.id!} > {descExists && ( )} ) })} ) } return (
} onChange={(e) => filterBy(e.target.value)} />
{endpoints.length ? ( sortedGroups.map((g) => ( )) ) : ( )}
) } function CustomHeader({ endpoint, translation }: { endpoint: EndpointAPIDefinition translation: { t: TFunction } }) { const { t } = translation return (

{t(getEndpointTranslationKey(endpoint))}

) } // e.g. http://{tidb_ip}/stats/dump/{db}/{table}?queryName={queryName} function Schema({ endpoint }: { endpoint: EndpointAPIDefinition }) { const query = buildQueryString(endpoint.query_params ?? []) return (

{`http://{${distro()[endpoint.component!]?.toLowerCase()}_instance}${ endpoint.path }${query}`}

) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/index.ts ================================================ import ApiList from './ApiList' export { ApiList } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/widgets/Database.tsx ================================================ import React, { useCallback, useContext, useState } from 'react' import { Select } from 'antd' import { useTranslation } from 'react-i18next' import type { ApiFormWidget } from './index' import { useLimitSelection } from './useLimitSelection' import { DebugAPIContext } from '../../context' export const DatabaseWidget: ApiFormWidget = ({ value, onChange }) => { const ctx = useContext(DebugAPIContext) const { t } = useTranslation() const tips = t(`debug_api.widgets.db_dropdown`) const [loading, setLoading] = useState(false) const [options, setOptions] = useState([]) const onFocus = useCallback(async () => { if (options.length) { return } setLoading(true) try { const rst = await ctx!.ds.infoListDatabases() setOptions(rst.data) } finally { setLoading(false) } }, [setLoading, setOptions, options, ctx]) const memoOnChange = useCallback( (tags: string[]) => onChange?.(tags[0]), [onChange] ) const { selectRef, onSelectChange } = useLimitSelection(1, memoOnChange) return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/widgets/Enum.tsx ================================================ import React from 'react' import { Select } from 'antd' import type { ApiFormWidget } from './index' export const EnumWidget: ApiFormWidget = ({ param }) => { return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/widgets/Host.tsx ================================================ import React from 'react' import { Select } from 'antd' import { useTranslation } from 'react-i18next' import type { ApiFormWidget } from './index' import { distro } from '@lib/utils/distro' const portKeys: { [k: string]: string } = { tidb: 'status_port', tikv: 'status_port', tiflash: 'status_port', pd: 'port', tiproxy: 'status_port' } export const HostSelectWidget: ApiFormWidget = ({ endpoint, topology }) => { const { t } = useTranslation() const componentEndpoints = topology[endpoint.component!] const portKey = portKeys[endpoint.component!] return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/widgets/Table.tsx ================================================ import React, { useCallback, useContext, useRef, useState } from 'react' import { Select } from 'antd' import { useTranslation } from 'react-i18next' import { InfoTableSchema } from '@lib/client' import type { ApiFormWidget } from './index' import { useLimitSelection } from './useLimitSelection' import { DebugAPIContext } from '../../context' export const TableWidget: ApiFormWidget = ({ form, value, onChange }) => { const ctx = useContext(DebugAPIContext) const { t } = useTranslation() const tips = t(`debug_api.widgets.table_dropdown`) const [loading, setLoading] = useState(false) const [options, setOptions] = useState([]) const prevDBValue = useRef('') const onFocus = useCallback(async () => { // Hardcode associated with the db field const dbValue = form.getFieldValue('db') if (prevDBValue.current === dbValue) { return } else { prevDBValue.current = dbValue } if (!dbValue) { setOptions([]) return } setLoading(true) try { const rst = await ctx!.ds.infoListTables(dbValue) setOptions(rst.data) } finally { setLoading(false) } }, [setLoading, setOptions, form, ctx]) const memoOnChange = useCallback( (tags: string[]) => onChange?.(tags[0]), [onChange] ) const { selectRef, onSelectChange } = useLimitSelection(1, memoOnChange) return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/widgets/TableID.tsx ================================================ import React, { useCallback, useContext, useState } from 'react' import { Select } from 'antd' import { useTranslation } from 'react-i18next' import { InfoTableSchema } from '@lib/client' import type { ApiFormWidget } from './index' import { useLimitSelection } from './useLimitSelection' import { DebugAPIContext } from '../../context' const filterOptionByNameAndID: any = ( inputValue: string, // children means Select.Option children nodes option: { children: string } ) => { return option.children.includes(inputValue) } export const TableIDWidget: ApiFormWidget = ({ value, onChange }) => { const ctx = useContext(DebugAPIContext) const { t } = useTranslation() const tips = t(`debug_api.widgets.table_id_dropdown`) const [loading, setLoading] = useState(false) const [options, setOptions] = useState([]) const onFocus = useCallback(async () => { if (options.length) { return } setLoading(true) try { const rst = await ctx!.ds.infoListTables() setOptions(rst.data) } finally { setLoading(false) } }, [setLoading, setOptions, options, ctx]) const memoOnChange = useCallback( (tags: string[]) => onChange?.(tags[0]), [onChange] ) const { selectRef, onSelectChange } = useLimitSelection(1, memoOnChange) return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/widgets/Text.tsx ================================================ import React from 'react' import { Input } from 'antd' import type { ApiFormWidget } from './index' export const TextWidget: ApiFormWidget = ({ param }) => { const placeholder = ((param.ui_props as any)?.placeholder as string) ?? '' return } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/widgets/index.tsx ================================================ import React from 'react' import type { FormInstance } from 'antd/es/form/Form' import type { Topology } from '../ApiForm' import { TextWidget } from './Text' import { EnumWidget } from './Enum' import { HostSelectWidget } from './Host' import { DatabaseWidget } from './Database' import { TableWidget } from './Table' import { TableIDWidget } from './TableID' import { EndpointAPIDefinition, EndpointAPIParamDefinition } from '@lib/client' export interface Widgets { [type: string]: ApiFormWidget } export interface ApiFormWidget { (config: ApiFormWidgetConfig): JSX.Element } export interface ApiFormWidgetConfig { form: FormInstance param: EndpointAPIParamDefinition endpoint: EndpointAPIDefinition topology: Topology value?: string onChange?: (v: string) => void } // For customized form controls. https://ant.design/components/form-cn/#components-form-demo-customized-form-controls const createJSXElementWrapper = (WidgetDef: ApiFormWidget) => (config: ApiFormWidgetConfig) => const paramModelWidgets: Widgets = { host: HostSelectWidget, text: TextWidget, dropdown: EnumWidget, db_dropdown: createJSXElementWrapper(DatabaseWidget), table_dropdown: createJSXElementWrapper(TableWidget), table_id_dropdown: createJSXElementWrapper(TableIDWidget) } export const createFormWidget = (config: ApiFormWidgetConfig) => { const { param } = config const widget = paramModelWidgets[param.ui_kind ?? 'text'] return widget(config) } // query string export const buildQueryString = (params: EndpointAPIParamDefinition[]) => { const query = params.reduce((prev, param, i) => { if (i === 0) { prev += '?' } else { prev += '&' } prev += `${param.name}={${param.name}}` return prev }, '') return query } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/apilist/widgets/useLimitSelection.ts ================================================ import { useCallback, useRef } from 'react' export const useLimitSelection = (limit: number, emit: Function) => { const selectRef = useRef(null) const onSelectChange = useCallback( (items: string[]) => { // Limit the available options to one option // There are no official limit props. https://github.com/ant-design/ant-design/issues/6626 if (items.length > limit) { items.shift() } if (items.length === limit) { selectRef.current.blur() } emit?.(items) }, [emit, limit, selectRef] ) return { selectRef, onSelectChange } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { EndpointAPIDefinition, EndpointRequestPayload, InfoTableSchema, TopologyTiDBInfo, ClusterinfoStoreTopologyResponse, TopologyPDInfo, TopologyTiProxyInfo } from '@lib/client' import { IContextConfig, ReqConfig } from '@lib/types' export interface IDebugAPIDataSource { debugAPIGetEndpoints( options?: ReqConfig ): AxiosPromise> debugAPIRequestEndpoint( req: EndpointRequestPayload, options?: ReqConfig ): AxiosPromise infoListDatabases(options?: ReqConfig): AxiosPromise> infoListTables( databaseName?: string, options?: ReqConfig ): AxiosPromise> getTiDBTopology(options?: ReqConfig): AxiosPromise> getStoreTopology( options?: ReqConfig ): AxiosPromise getPDTopology(options?: ReqConfig): AxiosPromise> getTiProxyTopology( options?: ReqConfig ): AxiosPromise> } export interface IDebugAPIContext { ds: IDebugAPIDataSource cfg: IContextConfig } export const DebugAPIContext = createContext(null) export const DebugAPIProvider = DebugAPIContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/index.tsx ================================================ import React, { useContext } from 'react' import { Routes, Route, HashRouter as Router } from 'react-router-dom' import { Root } from '@lib/components' import { addTranslations } from '@lib/utils/i18n' import { useLocationChange } from '@lib/hooks/useLocationChange' import { DebugAPIContext } from './context' import { ApiList } from './apilist' import translations from './translations' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> ) } export default function () { const ctx = useContext(DebugAPIContext) if (ctx === null) { throw new Error('DebugAPIContext must not be null') } return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/translations/en.yaml ================================================ debug_api: nav_title: Debug Data keyword_search: Filter by keyword endpoints_not_found: Endpoints not found warning_header: title: Warning body: These debug endpoints and data are largely internal and intended for use by {{ distro.tidb }} developers. Please use this feature under the guidance of {{ distro.tidb }} technical support. form: download: Download reset: Reset widgets: host_select_placeholder: Select an instance db_dropdown: Select or input a database name table_dropdown: Select or input a table name table_id_dropdown: Select or input a table ID tidb: name: '{{distro.tidb}}' endpoints: tidb_stats_by_table: Statistics Data - of a Table tidb_stats_by_table_timestamp: Statistics Data - of a Table and Timestamp tidb_stats_by_table_timestamp_desc: The timestamp needs to be set within the GC safe point tidb_settings: Current Config tidb_schema: Schema Information - All / by TableID tidb_schema_by_db: Schema Information - by Database tidb_schema_by_table: Schema Information - by Database + Table tidb_schema_by_table_id: Schema and Table Information - by TableID tidb_ddl_history: DDL History tidb_server_info: Server Information - Current tidb_all_servers_info: Server Information - All Servers tidb_all_regions_meta: Region - All tidb_region_meta_by_id: Region - by RegionID tidb_table_regions: Region - by Database + Table tidb_hot_regions: Region - Hot tidb_pprof: pprof tidb_pprof_desc: The `seconds` parameter is only effective to `kind=profile` and `kind=trace`. # for old tidb-dashboard backend api before 5.4.0 # get back from https://github.com/pingcap/tidb-dashboard/pull/1103/files#diff-fc455ec052d6f881db40d33377d3a6cd757100b476f10eb68ca5e72696e8a463L23 tidb_stats_dump: Statistics Data - of a Table tidb_stats_dump_timestamp: Statistics Data - of a Table and Timestamp tidb_stats_dump_timestamp_desc: The timestamp needs to be set within the GC safe point tidb_config: Current Config # tidb_schema: Schema Information - All / by TableID tidb_schema_db: Schema Information - by Database tidb_schema_db_table: Schema Information - by Database + Table tidb_dbtable_tableid: Schema and Table Information - by TableID tidb_info: Server Information - Current tidb_info_all: Server Information - All Servers tidb_regions_meta: Region - All tidb_region_id: Region - by RegionID # tidb_table_regions: Region - by Database + Table # tidb_hot_regions: Region - Hot # tidb_pprof: pprof # tidb_pprof_desc: The `seconds` parameter is only effective to `kind=profile` and `kind=trace`. tikv: name: '{{distro.tikv}}' endpoints: tikv_config: Current Config tikv_pprof_profile: CPU Profiling # for old tidb-dashboard backend api before 5.4.0 # tikv_config: Current Config tikv_profile: CPU Profiling tiflash: name: '{{distro.tiflash}}' endpoints: tiflash_config: Current Config tiflash_pprof_profile: CPU Profiling # for old tidb-dashboard backend api before 5.4.0 # tiflash_config: Current Config tiflash_profile: CPU Profiling pd: name: '{{distro.pd}}' endpoints: pd_cluster_info: Cluster Information (pd-ctl cluster) pd_cluster_status: Cluster Status pd_configs_all: Current Config pd_health: Cluster Health Information (pd-ctl health) pd_hot_read: Hot - Read (pd-ctl hot read) pd_hot_write: Hot - Write (pd-ctl hot write) pd_hot_stores: Hot - Stores (pd-ctl hot store) pd_labels_all: All Labels (pd-ctl label) pd_members_all: All Members Information (pd-ctl member) pd_leader: Leader Information (pd-ctl member leader show) pd_operators: All Operators (pd-ctl operator show) pd_regions_all: Regions - All (pd-ctl region) pd_region_by_id: Region - by RegionID (pd-ctl region [id]) pd_region_by_key: Region - by Key Reside in (pd-ctl region key [key]) pd_regions_sibling_by_id: Regions - Sibling Regions by RegionID (pd-ctl region sibling [id]) pd_regions_store: Regions - All Regions of a Store (pd-ctl region store [store-id]) pd_regions_by_top_read: Regions - Top Read Flow (pd-ctl region topread) pd_regions_by_top_write: Regions - Top Write Flow (pd-ctl region topread) pd_regions_by_top_conf_ver: Regions - Top Conf Version (pd-ctl region topconfver) pd_regions_by_top_version: Regions - Top Version (pd-ctl region topversion) pd_regions_by_top_size: Regions - Top Size (pd-ctl region topsize) pd_regions_by_state: Regions - by State (region check [state]) pd_schedulers_all: All Schedulers (pd-ctl scheduler show) pd_stores_all: Stores - All (pd-ctl store) pd_stores_by_label: Stores - by Label (pd-ctl label store [name] [value]) pd_store_by_id: Store - by StoreID (pd-ctl store [id]) pd_pprof: pprof pd_pprof_desc: The `seconds` parameter is only effective to `kind=profile` and `kind=trace`. # for old tidb-dashboard backend api before 5.4.0 pd_cluster: Cluster Information (pd-ctl cluster) # pd_cluster_status: Cluster Status pd_config_show_all: Current Config # pd_health: Cluster Health Information (pd-ctl health) # pd_hot_read: Hot - Read (pd-ctl hot read) # pd_hot_write: Hot - Write (pd-ctl hot write) # pd_hot_stores: Hot - Stores (pd-ctl hot store) pd_labels: All Labels (pd-ctl label) pd_members_show: All Members Information (pd-ctl member) pd_leader_show: Leader Information (pd-ctl member leader show) pd_operator_show: All Operators (pd-ctl operator show) pd_regions: Regions - All (pd-ctl region) pd_region_id: Region - by RegionID (pd-ctl region [id]) pd_region_key: Region - by Key Reside in (pd-ctl region key [key]) pd_region_scan: Regions - Scan All (pd-ctl region scan) pd_region_sibling: Regions - Sibling Regions by RegionID (pd-ctl region sibling [id]) # pd_regions_store: Regions - All Regions of a Store (pd-ctl region store [store-id]) pd_region_start_key: Regions - All Regions Starting from a Key (pd-ctl region startkey [key]) pd_region_top_read: Regions - Top Read Flow (pd-ctl region topread) pd_region_top_write: Regions - Top Write Flow (pd-ctl region topread) pd_region_top_conf_ver: Regions - Top Conf Version (pd-ctl region topconfver) pd_region_top_version: Regions - Top Version (pd-ctl region topversion) pd_region_top_size: Regions - Top Size (pd-ctl region topsize) pd_region_check: Regions - by State (region check [state]) pd_scheduler_show: All Schedulers (pd-ctl scheduler show) pd_stores: Stores - All (pd-ctl store) pd_label_stores: Stores - by Label (pd-ctl label store [name] [value]) pd_store_id: Store - by StoreID (pd-ctl store [id]) # pd_pprof: pprof # pd_pprof_desc: The `seconds` parameter is only effective to `kind=profile` and `kind=trace`. tiproxy: name: '{{distro.tiproxy}}' endpoints: tiproxy_config: Current Config tiproxy_pprof: pprof tiproxy_pprof_desc: The `seconds` parameter is only effective to `kind=profile` and `kind=trace`. ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/DebugAPI/translations/zh.yaml ================================================ debug_api: nav_title: 内部调试数据 keyword_search: 按关键字过滤接口 endpoints_not_found: 找不到对应接口 warning_header: title: 警告 body: 本页面提供的调试接口主要面向 {{ distro.tidb }} 开发者、提供数据库内部运行数据。请在 {{ distro.tidb }} 技术支持的指导下使用本功能。 form: download: 下载 reset: 重置 widgets: host_select_placeholder: 选择实例 db_dropdown: 选择或输入一个数据库名称 table_dropdown: 选择或输入一个数据表名称 table_id_dropdown: 选择或输入一个数据表 ID tidb: endpoints: tidb_stats_by_table_timestamp_desc: 时间戳应当在 GC Safe Point 以后 tidb_pprof_desc: seconds 参数仅对 kind=profile 和 kind=trace 生效 # for old tidb-dashboard backend api before 5.4.0 tidb_stats_dump_timestamp_desc: 时间戳应当在 GC Safe Point 以后 pd: endpoints: pd_pprof_desc: seconds 参数仅对 kind=profile 和 kind=trace 生效 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Diagnose/components/DiagnosisTable.tsx ================================================ import { Button } from 'antd' import React, { useEffect, useMemo, useRef, useState, useCallback, useContext } from 'react' import { useTranslation } from 'react-i18next' import { LoadingOutlined } from '@ant-design/icons' import { DiagnoseTableDef } from '@lib/client' import { CardTable, DateTime } from '@lib/components' import { useClientRequest, RequestFactory } from '@lib/utils/useClientRequest' import { diagnosisColumns } from '../utils/tableColumns' import { DiagnoseContext } from '../context' import { useIsWriteable } from '@lib/utils' // FIXME: use better naming // stableTimeRange: used to start diagnosing when triggering by clicking "Start" outside this component // unstableTimeRange: used to start diagnosing when triggering by clicking "Start" inside this component export interface IDiagnosisTableProps { stableTimeRange: [number, number] unstableTimeRange: [number, number] kind: string } type ReqFnType = RequestFactory // Modified from SearchResult.tsx function Row({ renderer, props }) { const [expanded, setExpanded] = useState(false) const handleClick = useCallback(() => { setExpanded((v) => !v) }, []) // https://stackoverflow.com/questions/53623294/how-to-conditionally-change-a-color-of-a-row-in-detailslist const backgroundColor = props.item.is_sub ? 'lightcyan' : 'inhert' return (
{renderer({ ...props, styles: { root: { backgroundColor } }, item: { ...props.item, expanded } })}
) } export default function DiagnosisTable({ stableTimeRange, unstableTimeRange, kind }: IDiagnosisTableProps) { const ctx = useContext(DiagnoseContext) const isWriteable = useIsWriteable() const { t } = useTranslation() const [internalTimeRange, setInternalTimeRange] = useState<[number, number]>([ 0, 0 ]) useEffect(() => setInternalTimeRange(stableTimeRange), [stableTimeRange]) function handleStart() { setInternalTimeRange(unstableTimeRange) } const timeChanged = useMemo( () => internalTimeRange[0] !== unstableTimeRange[0] || internalTimeRange[1] !== unstableTimeRange[1], [internalTimeRange, unstableTimeRange] ) const reqFn = useRef(null) useEffect(() => { reqFn.current = (reqConfig) => ctx!.ds.diagnoseDiagnosisPost( { start_time: internalTimeRange[0], end_time: internalTimeRange[1], kind }, reqConfig ) }, [internalTimeRange, kind, ctx]) const { data, isLoading, error, sendRequest } = useClientRequest( reqFn.current!, { immediate: false } ) useEffect(() => { if (internalTimeRange[0] !== 0) { sendRequest() } }, [internalTimeRange, sendRequest]) //////////////// const allRows = useMemo(() => { const _columnHeaders = data?.column?.map((col) => col.toLocaleLowerCase()) || [] let _rows: any[] = [] data?.rows?.forEach((row, rowIdx) => { // values (array) let _newRow = { row_idx: rowIdx, is_sub: false, show_sub: false } row.values?.forEach((v, v_idx) => { const key = _columnHeaders[v_idx] _newRow[key] = v }) // subvalues (2 demensional array) let _subRows: any[] = [] row.sub_values?.forEach((sub_v) => { let _subRow = { row_idx: rowIdx, is_sub: true } sub_v.forEach((v, idx) => { const key = _columnHeaders[idx] _subRow[key] = v }) _subRows.push(_subRow) }) _newRow['sub_rows'] = _subRows _rows.push(_newRow) }) return _rows }, [data]) const [items, setItems] = useState(allRows) useEffect(() => { setItems(allRows) }, [allRows]) const toggleShowSub = useCallback( (rowIdx, showSub) => { let newRows = [...items] let curRowPos = newRows.findIndex( (el) => el.row_idx === rowIdx && el.is_sub === false ) if (curRowPos === -1) { return } let curRow = newRows[curRowPos] // update status curRow.show_sub = showSub if (showSub) { // insert sub rows newRows.splice(curRowPos + 1, 0, ...curRow.sub_rows) } else { // remove sub rows newRows.splice(curRowPos + 1, curRow.sub_rows.length) } setItems(newRows) }, [items] ) const columns = useMemo( () => diagnosisColumns(items, toggleShowSub), [items, toggleShowSub] ) //////////////// const renderRow = useCallback((props, defaultRender) => { if (!props) { return null } return }, []) //////////////// function cardExtra() { if (isLoading) { return } if (timeChanged || error) { return ( ) } return null } function subTitle() { if (internalTimeRange[0] > 0) { return ( ~{' '} ) } return null } return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Diagnose/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { DiagnoseGenDiagnosisReportRequest, DiagnoseTableDef } from '@lib/client' import { ReqConfig } from '@lib/types' export interface IDiagnoseDataSource { diagnoseDiagnosisPost( request: DiagnoseGenDiagnosisReportRequest, options?: ReqConfig ): AxiosPromise } export interface IDiagnoseContext { ds: IDiagnoseDataSource } export const DiagnoseContext = createContext(null) export const DiagnoseProvider = DiagnoseContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Diagnose/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router, Route, Routes } from 'react-router-dom' import { Root } from '@lib/components' import { useLocationChange } from '@lib/hooks/useLocationChange' import { addTranslations } from '@lib/utils/i18n' import { DiagnoseContext } from './context' import { DiagnoseGenerator } from './pages' import translations from './translations' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> ) } const App = () => { const ctx = useContext(DiagnoseContext) if (ctx === null) { throw new Error('DiagnoseContext must not be null') } return ( ) } export default App export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Diagnose/pages/DiagnoseGenerator.tsx ================================================ import { Button, Form, Input, InputNumber, Select } from 'antd' import dayjs, { Dayjs } from 'dayjs' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import React, { useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { getValueFormat } from '@baurine/grafana-value-formats' import { Card } from '@lib/components' import { DatePicker } from '@lib/components' import DiagnosisTable from '../components/DiagnosisTable' import { useIsWriteable } from '@lib/utils' const DURATION_MINS = [5, 10, 30, 60, 24 * 60] const DEF_DURATION_MINS = 10 function minsAgo(mins: number): Dayjs { return dayjs().subtract(mins, 'm') } export default function DiagnoseGenerator() { const { t } = useTranslation() const isWriteable = useIsWriteable() const [duration, setDuration] = useState(DEF_DURATION_MINS) const [startTime, setStartTime] = useState(() => minsAgo(duration)) const timeRange: [number, number] = useMemo(() => { const _startTime = dayjs(startTime).unix() return [_startTime, _startTime + duration * 60] }, [startTime, duration]) const [stableTimeRange, setStableTimeRange] = useState<[number, number]>([ 0, 0 ]) function handleFinish() { setStableTimeRange(timeRange) } const timeChanged = useMemo( () => timeRange[0] !== stableTimeRange[0] || timeRange[1] !== stableTimeRange[1], [timeRange, stableTimeRange] ) return (
setStartTime(val || minsAgo(duration))} /> prev.rangeDuration !== cur.rangeDuration } > {({ getFieldValue }) => { return ( getFieldValue('rangeDuration') === 0 && ( `${value} min`} parser={(value) => parseInt(value?.replace(/[^\d]/g, '') || '') } style={{ width: 120 }} onChange={(val) => setDuration(val as number)} /> ) ) }} {timeChanged && ( )}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Diagnose/pages/index.ts ================================================ import DiagnoseGenerator from './DiagnoseGenerator' export { DiagnoseGenerator } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Diagnose/translations/en.yaml ================================================ diagnose: nav_title: Cluster Diagnostics generate: title: Cluster Diagnostics range_begin: Range Start Time range_duration: Range Duration submit: Start time_duration: custom: Custom table_title: config_diagnosis: Config Diagnosis error_diagnosis: Error Diagnosis performance_diagnosis: Performance Diagnosis fields: rule: Rule item: Item type: Type instance: Instance status_address: Status Address value: Value reference: Reference severity: Severity details: Details ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Diagnose/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Diagnose/translations/zh.yaml ================================================ diagnose: nav_title: 集群诊断 generate: title: 集群诊断 range_begin: 区间起始时间 range_duration: 区间长度 submit: 开始 time_duration: custom: 自定义 table_title: config_diagnosis: 配置诊断 error_diagnosis: 故障诊断 performance_diagnosis: 性能诊断 fields: rule: Rule item: Item type: Type instance: Instance status_address: Status Address value: Value reference: Reference severity: Severity details: Details ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Diagnose/utils/tableColumns.tsx ================================================ import { Tooltip } from 'antd' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import React from 'react' import { PlusOutlined, MinusOutlined } from '@ant-design/icons' import { TextWithInfo, TextWrap } from '@lib/components' type ToggleShowSubFn = (rowIdx: number, showSub: boolean) => void function commonColumnName(fieldName: string): any { return } function commonColumn(fieldName: string, minWidth: number, maxWidth?: number) { return { name: commonColumnName(fieldName), key: fieldName, fieldName, minWidth, maxWidth, onRender: (rec) => { if (rec.expanded) { return {rec[fieldName]} } else { return ( {rec[fieldName]} ) } } } } function ruleColumn(toggleShowSub: ToggleShowSubFn): IColumn { const handleClick = (ev: React.MouseEvent, rec) => { ev.stopPropagation() toggleShowSub(rec.row_idx, !rec.show_sub) } return { ...commonColumn('rule', 150, 200), onRender: (rec) => ( {rec.is_sub && '|--'} {!rec.is_sub && rec.sub_rows.length > 0 && (rec.show_sub ? ( handleClick(ev, rec)} /> ) : ( handleClick(ev, rec)} /> ))}{' '} {rec.expanded ? ( rec.rule ) : ( {rec.rule} )} ) } } function itemColumn(): IColumn { return commonColumn('item', 100, 150) } function typeColumn(): IColumn { return commonColumn('type', 60, 80) } function instanceColumn(): IColumn { return commonColumn('instance', 100, 200) } function statusAddressColumn(): IColumn { return commonColumn('status_address', 100, 200) } function valueColumn(): IColumn { return commonColumn('value', 100, 150) } function referenceColumn(): IColumn { return commonColumn('reference', 100, 150) } function severityColumn(): IColumn { return commonColumn('severity', 100, 120) } function detailsColumn(): IColumn { return commonColumn('details', 200) } function categoryColumn(): IColumn { return commonColumn('category', 100, 200) } function tableColumn(): IColumn { return commonColumn('table', 100, 200) } function errorColumn(): IColumn { return commonColumn('error', 200) } ////////////////////////////////////////// export function diagnosisColumns( rows: any[], toggleShowSub: ToggleShowSubFn ): IColumn[] { if (rows.length > 0 && rows[0].error) { return [categoryColumn(), tableColumn(), errorColumn()] } return [ ruleColumn(toggleShowSub), itemColumn(), typeColumn(), instanceColumn(), statusAddressColumn(), valueColumn(), referenceColumn(), severityColumn(), detailsColumn() ] } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { ProfilingGroupDetailResponse, ProfilingTaskGroupModel, ProfilingStartRequest, ConprofNgMonitoringConfig, TopologyTiDBInfo, ClusterinfoStoreTopologyResponse, TopologyPDInfo, TopologyTiCDCInfo, TopologyTiProxyInfo, TopologyTSOInfo, TopologySchedulingInfo } from '@lib/client' import { IContextConfig, ReqConfig } from '@lib/types' export interface IInstanceProfilingDataSource { getActionToken( id?: string, action?: string, options?: ReqConfig ): AxiosPromise getProfilingGroupDetail( groupId: string, options?: ReqConfig ): AxiosPromise getProfilingGroups( options?: ReqConfig ): AxiosPromise> startProfiling( req: ProfilingStartRequest, options?: ReqConfig ): AxiosPromise continuousProfilingConfigGet( options?: ReqConfig ): AxiosPromise getTiDBTopology(options?: ReqConfig): AxiosPromise> getStoreTopology( options?: ReqConfig ): AxiosPromise getPDTopology(options?: ReqConfig): AxiosPromise> getTiCDCTopology(options?: ReqConfig): AxiosPromise> getTiProxyTopology( options?: ReqConfig ): AxiosPromise> getTSOTopology(options?: ReqConfig): AxiosPromise> getSchedulingTopology( options?: ReqConfig ): AxiosPromise> } export interface IInstanceProfilingContext { ds: IInstanceProfilingDataSource cfg: IContextConfig & { publicPathBase: string } } export const InstanceProfilingContext = createContext(null) export const InstanceProfilingProvider = InstanceProfilingContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router, Route, Routes } from 'react-router-dom' import { useLocationChange } from '@lib/hooks/useLocationChange' import { Root, ParamsPageWrapper } from '@lib/components' import { addTranslations } from '@lib/utils/i18n' import { Detail, List } from './pages' import translations from './translations' import { InstanceProfilingContext } from './context' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> } /> ) } const App = () => { const ctx = useContext(InstanceProfilingContext) if (ctx === null) { throw new Error('InstanceProfilingContext must not be null') } return ( ) } export default App export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/pages/Detail.tsx ================================================ import { Badge, Button, Modal, Progress, Space, Tooltip } from 'antd' import React, { useCallback, useContext, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useMemoizedFn } from 'ahooks' import { Link } from 'react-router-dom' import { ArrowLeftOutlined, QuestionCircleOutlined } from '@ant-design/icons' import { upperFirst } from 'lodash' import { ProfilingTaskModel } from '@lib/client' import { CardTable, DateTime, Head, Descriptions, Card } from '@lib/components' import { useClientRequestWithPolling } from '@lib/utils/useClientRequest' import { instanceKindName, InstanceKinds } from '@lib/utils/instanceTable' import useQueryParams from '@lib/utils/useQueryParams' import { IGroup } from 'office-ui-fabric-react/lib/DetailsList' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import { IInstanceProfilingDataSource, InstanceProfilingContext } from '../context' enum ViewOptions { FlameGraph = 'flamegraph', Graph = 'graph', Download = 'download', Text = 'text' } enum taskState { Error, Running, Success, Skipped = 4 } enum RawDataType { Protobuf = 'protobuf', Jeprof = 'jeprof', Text = 'text' } interface IRow { kind: string } function mapData(data) { if (!data) { return data } data.tasks_status.forEach((task) => { if (task.state === 1) { let task_elapsed_secs = data.server_time - task.started_at let progress = task_elapsed_secs / data.task_group_status.profile_duration_secs if (progress > 0.99) { progress = 0.99 } if (progress < 0) { progress = 0 } task.progress = progress } // set profiling output options for previous generated SVG files and protobuf files. if (task.raw_data_type === RawDataType.Protobuf) { task.view_options = [ ViewOptions.FlameGraph, ViewOptions.Graph, ViewOptions.Download ] } else if (task.raw_data_type === RawDataType.Jeprof) { task.view_options = [ ViewOptions.FlameGraph, ViewOptions.Graph, ViewOptions.Download ] } else if (task.raw_data_type === RawDataType.Text) { task.view_options = [ViewOptions.Text] } else if (!task.raw_data_type) { switch (task.target.kind) { case 'tidb': case 'pd': task.view_options = [ViewOptions.Graph] break case 'tikv': case 'tiflash': task.view_options = [ViewOptions.FlameGraph] break } } }) return data } function isFinished(data) { const groupState = data?.task_group_status?.state return groupState === 2 || groupState === 3 } async function getActionToken( id: string, apiType: string, fetcher: IInstanceProfilingDataSource['getActionToken'] ): Promise { const res = await fetcher(id, apiType) const token = res.data if (!token) { return } return token } interface IRecord extends ProfilingTaskModel { view_options: ViewOptions[] } export default function Page() { const ctx = useContext(InstanceProfilingContext) const { t } = useTranslation() const { id } = useQueryParams() const { data: respData, isLoading, error } = useClientRequestWithPolling( (reqConfig) => ctx!.ds.getProfilingGroupDetail(id, reqConfig), { shouldPoll: (data) => !isFinished(data) } ) const data = useMemo(() => mapData(respData), [respData]) const profileDuration = respData?.task_group_status?.profile_duration_secs || 0 const [tableData, groupData] = useMemo(() => { const newRows: IRow[] = [] const newGroups: IGroup[] = [] let startIndex = 0 const tasks = data?.tasks_status ?? [] for (const kind of InstanceKinds) { tasks.forEach((task) => { if (task.target.kind === kind) { newRows.push({ ...task, kind: instanceKindName(kind) }) } }) if (newRows.length - startIndex > 0) { newGroups.push({ key: instanceKindName(kind), name: instanceKindName(kind), startIndex: startIndex, count: newRows.length - startIndex }) startIndex = newRows.length } } return [newRows, newGroups] }, [data]) const openResult = useMemoizedFn(async (openAs: string, rec: IRecord) => { let token: string | undefined let profileURL: string switch (openAs) { case ViewOptions.Download: token = await getActionToken( rec.id + '', 'single_download', ctx!.ds.getActionToken ) if (!token) { return } window.location.href = `${ ctx!.cfg.apiPathBase }/profiling/single/download?token=${token}` break case ViewOptions.FlameGraph: token = await getActionToken( rec.id + '', 'single_view', ctx!.ds.getActionToken ) if (!token) { return } profileURL = `${ ctx!.cfg.apiPathBase }/profiling/single/view?token=${token}` const titleOnTab = rec.target?.kind + '_' + rec.target?.display_name const type = rec.raw_data_type === RawDataType.Protobuf ? 'protobuf' : 'text' profileURL = `${ ctx!.cfg.publicPathBase }/speedscope/#profileURL=${encodeURIComponent( // protobuf can be rendered to flamegraph by speedscope profileURL + `&output_type=${type}` )}&title=${titleOnTab}` window.open(`${profileURL}`, '_blank') break case ViewOptions.Graph: case ViewOptions.Text: token = await getActionToken( rec.id + '', 'single_view', ctx!.ds.getActionToken ) if (!token) { return } profileURL = `${ ctx!.cfg.apiPathBase }/profiling/single/view?token=${token}&output_type=${openAs}` window.open(`${profileURL}`, '_blank') break } }) const columns = useMemo( () => [ { name: t('instance_profiling.detail.table.columns.instance'), key: 'instance', minWidth: 100, maxWidth: 200, onRender: (record) => record.target.display_name }, { name: t('instance_profiling.detail.table.columns.content'), key: 'content', minWidth: 100, maxWidth: 100, onRender: (record) => { if (record.profiling_type === 'cpu') { return `CPU - ${profileDuration}s` } else { return upperFirst(record.profiling_type) } } }, { name: t('instance_profiling.detail.table.columns.status'), key: 'status', minWidth: 100, maxWidth: 150, onRender: (record) => { if (record.state === taskState.Running) { return ( ) } else if (record.state === taskState.Error) { return ( ) } else if (record.state === taskState.Skipped) { return ( ) } else { return ( ) } } }, { name: t('instance_profiling.detail.table.columns.view_as.title'), key: 'view_as', minWidth: 250, maxWidth: 400, onRender: (record) => { if (record.state === taskState.Error) { return ( { Modal.error({ title: 'Profile Error', content: record.error }) }} > {t('instance_profiling.detail.table.columns.view_as.error')} ) } if (record.state === taskState.Running) { return (
) } if (record.state !== taskState.Success) { return <> } const rec = record as IRecord return ( {rec.view_options.map((action) => { return ( openResult(action, record)} key={action}> {t( `instance_profiling.detail.table.columns.view_as.${action}` )} ) })} ) } } ], [t, profileDuration, openResult] ) const handleDownloadGroup = useCallback(async () => { const token = await getActionToken( id, 'group_download', ctx!.ds.getActionToken ) if (!token) { return } window.location.href = `${ ctx!.cfg.apiPathBase }/profiling/group/download?token=${token}` }, [id, ctx]) return (
{t('instance_profiling.detail.head.back')} } titleExtra={ } />
{respData && ( )}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/pages/List.module.less ================================================ @import 'antd/es/style/themes/default.less'; .list { &_container { display: flex; flex-direction: column; height: 100vh; } } .alert_container { margin-left: @padding-page; margin-right: @padding-page; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/pages/List.tsx ================================================ import { Badge, Button, Form, Select, Modal, Alert, Space, Tooltip } from 'antd' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import React, { useMemo, useState, useCallback, useRef, useContext } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { useMemoizedFn } from 'ahooks' import { ProfilingStartRequest, ModelRequestTargetNode } from '@lib/client' import { Card, CardTable, InstanceSelect, IInstanceSelectRefProps, MultiSelect, Toolbar } from '@lib/components' import DateTime from '@lib/components/DateTime' import openLink from '@lib/utils/openLink' import { useClientRequest } from '@lib/utils/useClientRequest' import { combineTargetStats } from '../utils' import styles from './List.module.less' import { upperFirst } from 'lodash' import { QuestionCircleOutlined } from '@ant-design/icons' import { isDistro } from '@lib/utils/distro' import { InstanceProfilingContext } from '../context' import { useIsWriteable } from '@lib/utils' const profilingDurationsSec = [10, 30, 60, 120] const defaultProfilingDuration = 30 const profilingTypeOptions = ['CPU', 'Heap', 'Goroutine', 'Mutex'] export default function Page() { const ctx = useContext(InstanceProfilingContext) const isWriteable = useIsWriteable() const { data: historyTable, isLoading: listLoading, error: historyError } = useClientRequest(ctx!.ds.getProfilingGroups) const { data: ngMonitoringConfig } = useClientRequest( ctx!.ds.continuousProfilingConfigGet ) const conprofEnable = ngMonitoringConfig?.continuous_profiling?.enable ?? false const { t } = useTranslation() const navigate = useNavigate() const instanceSelect = useRef(null) const [submitting, setSubmitting] = useState(false) const handleFinish = useCallback( async (fieldsValue) => { if (!fieldsValue.instances || fieldsValue.instances.length === 0) { Modal.error({ content: 'Some required fields are not filled' }) return } if (!instanceSelect.current) { Modal.error({ content: 'Internal error: Instance select is not ready' }) return } const targets: ModelRequestTargetNode[] = instanceSelect .current!.getInstanceByKeys(fieldsValue.instances) .map((instance) => { let port switch (instance.instanceKind) { case 'pd': case 'tso': case 'scheduling': port = instance.port break case 'tidb': case 'tikv': case 'tiflash': case 'ticdc': case 'tiproxy': port = instance.status_port break } return { kind: instance.instanceKind, display_name: instance.key, ip: instance.ip, port } }) .filter((i) => i.port != null) // Default to all types if non is selected const types = !fieldsValue.type?.length ? [...profilingTypeOptions] : fieldsValue.type const req: ProfilingStartRequest = { targets, duration_secs: fieldsValue.duration, requsted_profiling_types: types.map((type) => type.toLowerCase()) } try { setSubmitting(true) const res = await ctx!.ds.startProfiling(req) navigate(`/instance_profiling/detail?id=${res.data.id}`) } finally { setSubmitting(false) } }, [navigate, ctx] ) const handleRowClick = useMemoizedFn( (rec, _idx, ev: React.MouseEvent) => { openLink(`/instance_profiling/detail?id=${rec.id}`, ev, navigate) } ) const historyTableColumns = useMemo( () => [ { name: t('instance_profiling.list.table.columns.targets'), key: 'targets', minWidth: 300, maxWidth: 480, onRender: (rec) => { return combineTargetStats(rec.target_stats) } }, { name: t( 'instance_profiling.list.table.columns.requsted_profiling_types' ), key: 'types', minWidth: 150, maxWidth: 250, onRender: (rec) => { return (rec.requsted_profiling_types ?? ['cpu']) .map((p) => (p === 'cpu' ? 'CPU' : upperFirst(p))) .join(',') } }, { name: t('instance_profiling.list.table.columns.status'), key: 'status', minWidth: 100, maxWidth: 150, onRender: (rec) => { if (rec.state === 0) { // all failed return ( ) } else if (rec.state === 1) { // running return ( ) } else if (rec.state === 2) { // all success return ( ) } else { // partial success return ( ) } } }, { name: t('instance_profiling.list.table.columns.start_at'), key: 'started_at', minWidth: 160, maxWidth: 220, onRender: (rec) => { return } }, { name: t('instance_profiling.list.table.columns.duration'), key: 'duration', minWidth: 100, maxWidth: 150, fieldName: 'profile_duration_secs' } ], [t] ) return (
{!isDistro() && ( { window.open( t('instance_profiling.settings.help_url'), '_blank' ) }} /> )}
{conprofEnable && (
{t('instance_profiling.list.disable_warning')}{' '} {!isDistro() && ( {t('instance_profiling.settings.help')} )} } showIcon />
)}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/pages/index.ts ================================================ import List from './List' import Detail from './Detail' export { List, Detail } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/translations/en.yaml ================================================ profiling: nav_title: Profiling Instances instance_profiling: nav_title: Manual Profiling settings: help: Help help_url: https://docs.pingcap.com/tidb/dev/dashboard-profiling list: control_form: title: Start Profiling Instances profiling_type: placeholder: All Profiling Types columnTitle: Profiling Type duration: label: Duration submit: Start Profiling disable_warning: You cannot start a profile now since continuous profiling is enabled. You can see latest profiling results in the continuous profiling page. table: title: Profiling History columns: targets: Instances requsted_profiling_types: Profiling Types start_at: Start At duration: Duration (sec) status: Status status: running: Running finished: Finished failed: Failed partial_finished: Partial Finished unknown: Unknown actions: detail: Detail detail: head: back: History title: Profiling Detail start_at: Start At download: Download Profiling Result table: columns: instance: Instance kind: Component content: Content status: Status view_as: title: View As error: Error Information flamegraph: FlameGraph graph: DotGraph download: RawData text: RawData status: finished: Finished skipped: Not Applicable skipped_tooltip: This profiling kind is currently not supported running: Running error: Error ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/translations/zh.yaml ================================================ profiling: nav_title: 实例性能分析 instance_profiling: nav_title: 手动分析 settings: help: 帮助 help_url: https://docs.pingcap.com/zh/tidb/dev/dashboard-profiling list: control_form: title: 开始性能分析 profiling_type: placeholder: 所有性能数据 columnTitle: 性能数据类型 duration: label: 分析时长 submit: 开始分析 disable_warning: 当前已启用持续性能分析,因此不能再发起一个新的性能分析。可在持续性能分析页面查看当前及过往的分析结果。 table: title: 性能分析历史 columns: targets: 实例 requsted_profiling_types: 分析类型 start_at: 开始时间 duration: 时长(秒) status: 状态 status: running: 分析中 finished: 完成 failed: 失败 partial_finished: 部分完成 unknown: 未知 actions: detail: 详情 detail: head: back: 历史记录 title: 性能分析详情 start_at: 开始时间 download: 下载性能分析结果 table: columns: instance: 实例 kind: 组件 content: 内容 status: 状态 view_as: title: 查看方式 error: 错误信息 flamegraph: 火焰图 graph: 关系图 download: 原始数据 text: 原始数据 status: finished: 完成 skipped: 不适用 skipped_tooltip: 该分析当前暂不支持 running: 分析中 error: 错误 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/utils/combineTargetStats.ts ================================================ import { ModelRequestTargetStatistics } from '@lib/client' import { instanceKindName } from '@lib/utils/instanceTable' const targetNameMap = { num_tidb_nodes: () => instanceKindName('tidb'), num_tikv_nodes: () => instanceKindName('tikv'), num_pd_nodes: () => instanceKindName('pd'), num_tiflash_nodes: () => instanceKindName('tiflash'), num_ticdc_nodes: () => instanceKindName('ticdc'), num_tiproxy_nodes: () => instanceKindName('tiproxy'), num_tso_nodes: () => instanceKindName('tso'), num_scheduling_nodes: () => instanceKindName('scheduling') } export const combineTargetStats = (stats: ModelRequestTargetStatistics) => Object.entries(stats) .reduce((prev, [key, stat]) => { if (targetNameMap[key]) { const targetName = targetNameMap[key]() targetName && prev.push(`${stat} ${targetName}`) } return prev }, [] as string[]) .join(', ') ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/InstanceProfiling/utils/index.ts ================================================ export * from './combineTargetStats' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/components/KeyViz.less ================================================ @import 'antd/es/style/themes/default.less'; .PD-Cluster-Legend { position: relative; .unit { font-size: 12px; color: #333; position: absolute; right: 0; top: 5px; } } .PD-KeyVis { height: 100vh; display: flex; flex-direction: column; .ui.dropdown .menu { z-index: 9999; } .PD-KeyVis-Toolbar { .group-icons-btn { border: 0; margin-top: -7px; .button { border-left: 0 !important; } .icon { color: #1b1c1d; } } .ant-select .anticon { margin-right: 5px; } } svg, button { user-select: none; } g.tick text { font-family: 'Poppins'; font-size: 12px; text-anchor: start; } .tooltip { padding: 10px; color: #eee; background-color: #333; box-shadow: 5px 5px 10px rgba(black, 0.5); border-radius: 3px; min-width: 200px; div.value { display: flex; align-items: center; div.value { margin: 0; padding: 7px; font-weight: bold; border-radius: 3px; } div.unit { color: #999; font-size: 0.9rem; margin-left: 10px; } } button { line-height: 1; background-color: transparent; border: transparent solid 1px; border-radius: 3px; outline: none; padding: 3px; text-align: left; color: #fff; transition: background-color ease-in 100ms; &:hover { border: #888 solid 1px; cursor: pointer; } &:active { background-color: #888; transition: none; } } .time { color: #aaa; line-height: 1.2; margin-top: 10px; font-size: 0.9rem; } .overviewLabel { margin: 20px 0; .subLabel { padding: 1px 3px; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 250px; } } .keyContainer { margin-top: 10px; padding: 0 3px; .desc { text-transform: uppercase; font-weight: bold; font-size: 0.9rem; color: #ccc; } .label, .key { display: block; color: #888; font-size: 0.8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 250px; } } } .heatmap { flex-grow: 1; margin: @padding-page; margin-top: -32px; } } #PD-KeyVis-Brightness-Overlay { background-color: @select-dropdown-bg; padding: @padding-md; border-radius: @border-radius-base; outline: none; box-shadow: @box-shadow-base; box-sizing: border-box; } .PD-KeyVis-Select-Option .anticon { display: none; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/components/KeyViz.tsx ================================================ import React, { useContext, useState } from 'react' import { Button, Drawer, Result, Space } from 'antd' import { useTranslation } from 'react-i18next' import { useGetSet, useMount } from 'react-use' import { useBoolean, useMemoizedFn } from 'ahooks' import { ConfigKeyVisualConfig } from '@lib/client' import { Heatmap } from '../heatmap' import { HeatmapData, HeatmapRange, DataTag } from '../heatmap/types' import { fetchHeatmap } from '../utils' import KeyVizSettingForm from './KeyVizSettingForm' import KeyVizToolbar from './KeyVizToolbar' import './KeyViz.less' import { useChange } from '@lib/utils/useChange' import { isDistro } from '@lib/utils/distro' import { IKeyVizDataSource, KeyVizContext } from '../context' // const CACHE_EXPRIE_SECS = 10 class HeatmapCache { // cache: CacheEntry[] = [] // latestFetchIdx = 0 async fetch( fetcher: IKeyVizDataSource['keyvisualHeatmapsGet'], range: number | HeatmapRange, metricType: DataTag ): Promise { // return fetchDummyHeatmap() let selection if (typeof range === 'number') { const endTime = Math.ceil(new Date().getTime() / 1000) // this.cache = this.cache.filter((entry) => entry.expireTime > endTime) // const entry = this.cache.find( // (entry) => entry.dateRange === range && entry.metricType === metricType // ) // if (entry) { // return entry.data // } else { selection = { starttime: endTime - range, endtime: endTime } // } } else { selection = range } // this.latestFetchIdx += 1 // const fetchIdx = this.latestFetchIdx const data = await fetchHeatmap(fetcher, selection, metricType) // if (fetchIdx === this.latestFetchIdx) { // if (typeof range === 'number') { // this.cache.push({ // dateRange: range, // metricType: metricType, // expireTime: new Date().getTime() / 1000 + CACHE_EXPRIE_SECS, // data: data, // }) // } return data // } // return undefined } } // Todo: define heatmap state, with auto check control, date range select, reset to zoom // fetchData , changeType, add loading state, change zoom level to reset autofetch, type ChartState = { heatmapData: HeatmapData metricType: DataTag } // TODO: using global state is not a good idea let _chart let cache = new HeatmapCache() const KeyViz = () => { const ctx = useContext(KeyVizContext) const [chartState, setChartState] = useState() const [getSelection, setSelection] = useGetSet(null) const [isLoading, setLoading] = useState(true) const [autoRefreshSeconds, setAutoRefreshSeconds] = useState(0) const [getOnBrush, setOnBrush] = useGetSet(false) const [getDateRange, setDateRange] = useGetSet(3600 * 6) const [getBrightLevel, setBrightLevel] = useGetSet(1) const [getMetricType, setMetricType] = useGetSet('written_bytes') const [config, setConfig] = useState(null) const [ shouldShowSettings, { setTrue: openSettings, setFalse: closeSettings } ] = useBoolean(false) const { t } = useTranslation() const enabled = config?.auto_collection_disabled !== true const updateServiceStatus = useMemoizedFn(async function () { if (ctx?.cfg?.showSetting === false) { return } try { setLoading(true) const resp = await ctx!.ds.keyvisualConfigGet() const config = resp.data setConfig(config) } finally { setLoading(false) } }) useMount(updateServiceStatus) const updateHeatmap = useMemoizedFn(async () => { try { setLoading(true) setOnBrush(false) const metricType = getMetricType() const data = await cache.fetch( ctx!.ds.keyvisualHeatmapsGet, getSelection() || getDateRange(), metricType ) setChartState({ heatmapData: data!, metricType }) } finally { setLoading(false) } }) const onChangeBrightLevel = useMemoizedFn((val) => { if (!_chart) return setBrightLevel(val) _chart.brightness(val) }) const onChangeDateRange = useMemoizedFn((v: number) => { setDateRange(v) setSelection(null) }) const onResetZoom = useMemoizedFn(() => { setSelection(null) }) const onToggleBrush = useMemoizedFn(() => { const newOnBrush = !getOnBrush() setAutoRefreshSeconds(0) setOnBrush(newOnBrush) _chart.brush(newOnBrush) }) const onBrush = useMemoizedFn((selection: HeatmapRange) => { setOnBrush(false) setAutoRefreshSeconds(0) setSelection(selection) }) const onZoom = useMemoizedFn(() => { setAutoRefreshSeconds(0) }) const onChartInit = useMemoizedFn((chart) => { _chart = chart setLoading(false) _chart.brightness(getBrightLevel()) }) useChange(() => { if (autoRefreshSeconds > 0) { onResetZoom() setOnBrush(false) } }, [autoRefreshSeconds]) useChange(() => { if (enabled) { updateHeatmap() } }, [config, getSelection(), getDateRange(), getMetricType()]) const disabledPage = isLoading ? null : ( {!isDistro() && ( )} } /> ) const mainPart = !enabled ? disabledPage : chartState && ( ) return (
{mainPart}
) } export default KeyViz ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/components/KeyVizSettingForm.tsx ================================================ import React, { useState, useMemo, useCallback, useContext } from 'react' import { Form, Skeleton, Switch, Space, Button, Modal, Radio, Input } from 'antd' import { ExclamationCircleOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { useClientRequest } from '@lib/utils/useClientRequest' import { DrawerFooter, ErrorBar } from '@lib/components' import { useIsWriteable } from '@lib/utils/store' import { KeyVizContext } from '../context' const policyConfigurable = process.env.NODE_ENV === 'development' interface Props { onClose: () => void onConfigUpdated: () => any } type SeparatorStatus = { validateStatus: 'warning' | 'success' hasFeedback: boolean help: string } const negateSwitchProps = { getValueProps: (value) => ({ checked: value !== true }), getValueFromEvent: (checked) => !checked } function getSeparatorValidator(t) { const separatorEmptyStatus: SeparatorStatus = { validateStatus: 'warning', hasFeedback: true, help: t('keyviz.settings.separator_empty_warning') } const separatorNotEmptyStatus: SeparatorStatus = { validateStatus: 'success', hasFeedback: true, help: '' } return (value: string | undefined) => value === undefined || value === '' ? separatorEmptyStatus : separatorNotEmptyStatus } function getPolicyOptions(t) { return ['db', 'kv'].map((policy) => { let label = t(`keyviz.settings.policy_${policy}`) return ( {label} ) }) } function KeyVizSettingForm({ onClose, onConfigUpdated }: Props) { const ctx = useContext(KeyVizContext) const [submitting, setSubmitting] = useState(false) const { t } = useTranslation() const isWriteable = useIsWriteable() const { data: config, isLoading: loading, error } = useClientRequest(ctx!.ds.keyvisualConfigGet) const onUpdateServiceStatus = async (values) => { try { setSubmitting(true) await ctx!.ds.keyvisualConfigPut(values) onClose() onConfigUpdated() } finally { setSubmitting(false) } } const onSubmit = (values) => { if ( config?.auto_collection_disabled !== true && values.auto_collection_disabled === true ) { Modal.confirm({ title: t('keyviz.settings.close_keyviz'), icon: , content: t('keyviz.settings.close_keyviz_warning'), okText: t('keyviz.settings.actions.close'), cancelText: t('keyviz.settings.actions.cancel'), okButtonProps: { danger: true }, onOk: () => onUpdateServiceStatus(values) }) } else { onUpdateServiceStatus(values) } } const [form] = Form.useForm() const onValuesChange = useCallback( (changedValues, values) => { if (changedValues?.auto_collection_disabled !== true && !values?.policy) { form.setFieldsValue({ policy: 'db' }) } if ( config?.policy !== 'kv' && changedValues?.policy === 'kv' && !values?.policy_kv_separator ) { form.setFieldsValue({ policy_kv_separator: '/' }) } }, [form, config] ) const policyOptions = useMemo(() => getPolicyOptions(t), [t]) const validateSeparator = useMemo(() => getSeparatorValidator(t), [t]) return ( <> {error && } {loading && } {!loading && config && (
{({ getFieldValue }) => { const enabled = getFieldValue('auto_collection_disabled') !== true const policy = getFieldValue('policy') const separator = getFieldValue('policy_kv_separator') return ( <> {policyOptions} ) }}
)} ) } export default KeyVizSettingForm ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/components/KeyVizToolbar.tsx ================================================ import React, { Component } from 'react' import { AreaChartOutlined, ArrowsAltOutlined, BulbOutlined, ClockCircleOutlined, DownOutlined, LoadingOutlined, QuestionCircleOutlined, SettingOutlined } from '@ant-design/icons' import { Slider, Spin, Select, Dropdown, Button, Tooltip, Space } from 'antd' import { withTranslation, WithTranslation } from 'react-i18next' import Flexbox from '@g07cha/flexbox-react' import { AutoRefreshButton, Card, Toolbar } from '@lib/components' import { getValueFormat } from '@baurine/grafana-value-formats' import { isDistro } from '@lib/utils/distro' import { telemetry as keyVizTelemetry } from '../utils/telemetry' export interface IKeyVizToolbarProps { enabled: boolean isLoading: boolean autoRefreshSeconds: number isOnBrush: boolean metricType: string brightLevel: number dateRange: number showHelp: boolean showSetting: boolean onResetZoom: () => void onToggleBrush: () => void onChangeMetric: (string) => void onChangeDateRange: (number) => void onChangeBrightLevel: (number) => void onChangeAutoRefresh: (number) => void onRefresh: () => void onShowSettings: () => any } class KeyVizToolbar extends Component { state = { exp: 0 } handleRefreshClick = () => { this.props.onRefresh() keyVizTelemetry.clickManualRefresh() } handleAutoRefreshMenuClick = (key) => { this.props.onChangeAutoRefresh(key) keyVizTelemetry.clickAutoRefresh() } handleDateRange = (value) => { this.props.onChangeDateRange(value) keyVizTelemetry.changeTimeDuration(value) } handleMetricChange = (value) => { this.props.onChangeMetric(value) keyVizTelemetry.changeMetric(value) } handleBrightLevel = (exp: number) => { this.props.onChangeBrightLevel(Math.pow(2, exp)) this.setState({ exp }) keyVizTelemetry.changeBright(exp) } handleBrightnessDropdown = () => { setTimeout(() => { this.handleBrightLevel(this.state.exp) }, 0) } handleToggleBrush = () => { this.props.onToggleBrush() keyVizTelemetry.toggleBrush() } handleShowSetting = () => { this.props.onShowSettings() keyVizTelemetry.openSetting() } handleResetZoom = () => { this.props.onResetZoom() keyVizTelemetry.resetZoom() } render() { const { t, enabled, isLoading, dateRange, isOnBrush, metricType, autoRefreshSeconds, showHelp, showSetting } = this.props // in hours const dateRangeOptions = [1, 6, 12, 24, 24 * 7] const MetricOptions = [ { text: t('keyviz.toolbar.view_type.read_bytes'), value: 'read_bytes' }, { text: t('keyviz.toolbar.view_type.write_bytes'), value: 'written_bytes' }, { text: t('keyviz.toolbar.view_type.read_keys'), value: 'read_keys' }, { text: t('keyviz.toolbar.view_type.write_keys'), value: 'written_keys' }, { text: t('keyviz.toolbar.view_type.all'), value: 'integration' } ] return (
{ e.stopPropagation() }} >
this.handleBrightLevel(value as number) } />
} trigger={['click']} onVisibleChange={this.handleBrightnessDropdown} >
{this.props.isLoading && ( } /> )}
{showSetting && ( )} {!isDistro() && showHelp && ( { window.open(t('keyviz.settings.help_url'), '_blank') keyVizTelemetry.openHelp() }} /> )}
) } } export default withTranslation()(KeyVizToolbar) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { ConfigKeyVisualConfig, MatrixMatrix } from '@lib/client' import { ReqConfig } from '@lib/types' export interface IKeyVizDataSource { keyvisualConfigGet(options?: ReqConfig): AxiosPromise keyvisualConfigPut( request: ConfigKeyVisualConfig, options?: ReqConfig ): AxiosPromise keyvisualHeatmapsGet( startkey?: string, endkey?: string, starttime?: number, endtime?: number, type?: | 'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' | 'integration', options?: ReqConfig ): AxiosPromise } export interface IKeyVizConfig { showHelp?: boolean showSetting?: boolean } export interface IKeyVizContext { ds: IKeyVizDataSource cfg?: IKeyVizConfig } export const KeyVizContext = createContext(null) export const KeyVizProvider = KeyVizContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/heatmap/axis/histogram.ts ================================================ import * as d3 from 'd3' import { Section, scaleSections } from '.' const fill = '#333' const fillFocus = '#ccc' const stroke = '#fff' export function histogram(data: number[][]) { let xRange: [number, number] = [0, 0] let yRange: [number, number] = [0, 0] histogram.xRange = function (val: [number, number]) { xRange = val return this } histogram.yRange = function (val: [number, number]) { yRange = val return this } function histogram( xCtx: CanvasRenderingContext2D, yCtx: CanvasRenderingContext2D, xFocusDomain: [number, number] | null, yFocusDomain: [number, number] | null, xScale, yScale ) { const xHeight = xCtx.canvas.height const yWidth = yCtx.canvas.width const xLen = data.length const yLen = data[0].length const xStartIdx = Math.max(0, Math.floor(xScale.invert(xRange[0]))) const xEndIdx = Math.min(xLen - 1, Math.ceil(xScale.invert(xRange[1]))) const yStartIdx = Math.max(0, Math.floor(yScale.invert(yRange[0]))) const yEndIdx = Math.min(yLen - 1, Math.ceil(yScale.invert(yRange[1]))) const xSum: Section[] = [] const ySum: Section[] = [] for (let x = xStartIdx; x < xEndIdx; x++) { let sumVal = 0 for (let y = yStartIdx; y < yEndIdx; y++) { sumVal += data[x][y] } xSum.push({ val: sumVal, startIdx: x, endIdx: x + 1 }) } for (let y = yStartIdx; y < yEndIdx; y++) { let sumVal = 0 for (let x = xStartIdx; x < xEndIdx; x++) { sumVal += data[x][y] } ySum.push({ val: sumVal, startIdx: y, endIdx: y + 1 }) } const xBins = scaleSections( xSum, xFocusDomain, xRange, xScale, (origin, val) => origin + val ) const yBins = scaleSections( ySum, yFocusDomain, yRange, yScale, (origin, val) => origin + val ) const xBinsMax = d3.max(xBins, (section) => section.val)! const yBinsMax = d3.max(yBins, (section) => section.val)! xCtx.clearRect(xRange[0], 0, xRange[1], xHeight) xCtx.strokeStyle = stroke xCtx.lineWidth = 1 for (const bin of xBins) { const width = bin.endPos - bin.startPos const height = (bin.val / xBinsMax) * xHeight if (height < 1) continue xCtx.fillStyle = bin.focus ? fillFocus : fill xCtx.beginPath() xCtx.rect(bin.startPos, xHeight - height, width, height) xCtx.fill() xCtx.stroke() xCtx.closePath() } yCtx.clearRect(0, yRange[0], yWidth, yRange[1]) yCtx.strokeStyle = stroke yCtx.lineWidth = 1 for (const bin of yBins) { const width = (bin.val / yBinsMax) * yWidth const height = bin.endPos - bin.startPos if (width < 1) continue yCtx.fillStyle = bin.focus ? fillFocus : fill yCtx.beginPath() yCtx.rect(yWidth - width, bin.startPos, width, height) yCtx.fill() yCtx.stroke() yCtx.closePath() } } return histogram } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/heatmap/axis/index.ts ================================================ import _ from 'lodash' export type Section = { val: T startIdx: number endIdx: number } export type DisplaySection = { val: T startIdx: number endIdx: number startPos: number endPos: number focus: boolean } const mergeWidth = 3 export function scaleSections( sections: Section[], focusDomain: [number, number] | null, range: [number, number], scale: (idx: number) => number, merge: (origin: T, val: T) => T ): DisplaySection[] { let result: DisplaySection[] = [] let mergedSmallSection: DisplaySection | null = null let oneSectionRendered = false for (const section of sections) { const canvasStart = range[0] const canvasEnd = range[1] const startPos = scale(section.startIdx) const endPos = scale(section.endIdx) const commonStart = Math.max(startPos, canvasStart) const commonEnd = Math.min(endPos, canvasEnd) const focus = focusDomain ? Math.min(scale(focusDomain[1]), endPos) - Math.max(scale(focusDomain[0]), startPos) > 0 : false if (mergedSmallSection) { if ( mergedSmallSection.endPos - mergedSmallSection.startPos >= mergeWidth || commonStart - mergedSmallSection.startPos > mergeWidth || (!oneSectionRendered && section.startIdx % 2 === 0) ) { result.push(mergedSmallSection) oneSectionRendered = true mergedSmallSection = null } } if (commonEnd - commonStart > 0) { if (commonEnd - commonStart > mergeWidth) { result.push( _.assign( { startPos: commonStart, endPos: commonEnd, focus: focus }, section ) ) oneSectionRendered = true mergedSmallSection = null } else { if (mergedSmallSection === null) { mergedSmallSection = _.assign( { startPos: commonStart, endPos: commonEnd, focus: focus }, section ) } else { mergedSmallSection.val = merge(mergedSmallSection.val, section.val) mergedSmallSection.endPos = commonEnd mergedSmallSection.focus = mergedSmallSection.focus || focus } } } } return result } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/heatmap/axis/label-axis.ts ================================================ import _ from 'lodash' import { Section, DisplaySection, scaleSections } from '.' import { KeyAxisEntry } from '../types' import { truncateString } from '../utils' const labelAxisMargin = 4 const labelAxisWidth = 28 const labelTextPadding = 4 const minTextHeight = 17 const fill = '#333' const fillFocus = '#ccc' const stroke = '#fff' const textFill = 'white' const textFillFocus = '#333' const font = '500 12px Poppins' const focusFont = '700 12px Poppins' type Label = Section type DisplayLabel = DisplaySection export function labelAxisGroup(keyAxis: KeyAxisEntry[]) { // Remove the endkey of the last region, so that the row where the region is located is aligned with the startkey. if (keyAxis.length > 1) { keyAxis = keyAxis.slice(1) } let range: [number, number] = [0, 0] const groups = aggrKeyAxisLabel(keyAxis) labelAxisGroup.range = function (val) { range = val return this } function labelAxisGroup( ctx: CanvasRenderingContext2D, focusDomain: [number, number] | null, scale: (idx: number) => number ) { const width = ctx.canvas.width const height = ctx.canvas.height let scaledGroups = groups.map((group) => scaleSections(group, focusDomain, range, scale, () => '') ) ctx.clearRect(0, 0, width, height) ctx.strokeStyle = stroke ctx.lineWidth = 1 ctx.textBaseline = 'middle' for (const [groupIdx, group] of scaledGroups.entries()) { const marginLeft = groupIdx * (labelAxisWidth + labelAxisMargin) for (const label of group) { const width = labelAxisWidth const height = label.endPos - label.startPos ctx.fillStyle = label.focus ? fillFocus : fill ctx.beginPath() ctx.rect(marginLeft, label.startPos, width, height) ctx.fill() ctx.stroke() ctx.closePath() if (shouldShowLabelText(label)) { ctx.font = label.focus ? focusFont : font ctx.fillStyle = label.focus ? textFillFocus : textFill ctx.translate( marginLeft + labelAxisWidth / 2 + 2, label.endPos - labelTextPadding ) ctx.rotate(-Math.PI / 2) ctx.fillText(fitLabelText(label), 0, 0) ctx.resetTransform() ctx.scale(window.devicePixelRatio, window.devicePixelRatio) } } } } return labelAxisGroup } function shouldShowLabelText(label: DisplayLabel): boolean { return ( label.endPos - label.startPos >= minTextHeight && label.val?.length !== 0 ) } function fitLabelText(label: DisplayLabel): string { const rectWidth = label.endPos - label.startPos const textLen = Math.floor(rectWidth / 7.5) return truncateString(label.val, textLen) } function aggrKeyAxisLabel(keyAxis: KeyAxisEntry[]): Label[][] { let result: Label[][] = _.times(4, () => []) let notEqual: boolean[] = _.times(keyAxis.length, () => false) for (let groupIdx = 0; groupIdx < result.length; groupIdx++) { let lastLabel: string | null = null let startKeyIdx: number | null = null for (let keyIdx = 0; keyIdx < keyAxis.length; keyIdx++) { const label = keyAxis[keyIdx].labels[groupIdx] // When the prefixes are equal and this column is null, it is considered equal to the previous row of labels. notEqual[keyIdx] = notEqual[keyIdx] || (label != null && label !== lastLabel) if (notEqual[keyIdx]) { if (startKeyIdx != null && lastLabel != null) { result[groupIdx].push({ val: lastLabel, startIdx: startKeyIdx, endIdx: keyIdx }) startKeyIdx = null } if (label != null) { startKeyIdx = keyIdx } lastLabel = label } } if (startKeyIdx != null && lastLabel != null) { result[groupIdx].push({ val: lastLabel, startIdx: startKeyIdx, endIdx: keyAxis.length }) } } return result } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/heatmap/buffer.ts ================================================ import * as d3 from 'd3' export function createBuffer( normalizedValues: Uint8Array, width: number, height: number, rasterizedColors: Uint32Array ): HTMLCanvasElement { const canvas = d3 .create('canvas') .attr('width', width) .attr('height', height) .node() as HTMLCanvasElement console.time('createBuffer') const ctx = canvas.getContext('2d') as CanvasRenderingContext2D const imageDataBuffer = new ArrayBuffer(width * height * 4) const imageDataPixels = new Uint32Array(imageDataBuffer) const len = normalizedValues.length for (let i = 0; i < len; i++) { imageDataPixels[i] = rasterizedColors[normalizedValues[i]] } const imageData = ctx.createImageData(width, height) imageData.data.set(new Uint8ClampedArray(imageDataBuffer)) ctx.putImageData(imageData, 0, 0) console.timeEnd('createBuffer') return canvas } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/heatmap/chart.ts ================================================ import * as d3 from 'd3' import _ from 'lodash' import dayjs from 'dayjs' import { HeatmapRange, HeatmapData, DataTag } from './types' import { createBuffer } from './buffer' import { labelAxisGroup } from './axis/label-axis' import { histogram } from './axis/histogram' import { getColorScheme, ColorScheme, rasterizeLevel } from './color' import { tagUnit, withUnit, clickToCopyBehavior } from './utils' import legend from './legend' import { tz } from '@lib/utils' const margin = { top: 25, right: 40, bottom: 70, left: 100 } const tooltipOffset = { horizontal: 20, vertical: 20 } type TooltipStatus = { pinned: boolean hidden: boolean x: number y: number } type FocusStatus = { xDomain: [number, number] yDomain: [number, number] } const defaultTooltipStatus = { pinned: false, hidden: true, x: 0, y: 0 } const heatmapCanvasPixelRatio = Math.max(2, window.devicePixelRatio) function normalizeData(d: number[][], maxValue: number) { const height = d.length > 0 ? d[0].length : 0 const width = d.length const len = width * height const normalized = new Uint8Array(len) const logMaxValue = Math.log(maxValue) for (let cIdx = 0; cIdx < width; cIdx++) { for (let rIdx = 0; rIdx < height; rIdx++) { const addr = rIdx * width + cIdx normalized[addr] = (Math.log(d[cIdx][rIdx]) / logMaxValue) * rasterizeLevel } } return normalized } export async function heatmapChart( container, data: HeatmapData, dataTag: DataTag, onBrush: (range: HeatmapRange) => void, onZoom: () => void ) { const maxValue = d3.max(data.data[dataTag].map((array) => d3.max(array)!)) || 0 const normalizedData = normalizeData(data.data[dataTag], maxValue) let colorScheme: ColorScheme let brightness = 1 let bufferCanvas: HTMLCanvasElement let zoomTransform = d3.zoomIdentity let tooltipStatus: TooltipStatus = _.clone(defaultTooltipStatus) let focusStatus: FocusStatus | null = null let isBrushing = false let width = 0 let height = 0 let canvasWidth = 0 let canvasHeight = 0 heatmapChart.brightness = function (val: number) { brightness = val updateBuffer() heatmapChart() } heatmapChart.brush = function (enabled: boolean) { isBrushing = enabled heatmapChart() } heatmapChart.resetZoom = function () { zoomTransform = d3.zoomIdentity heatmapChart() } heatmapChart.size = function (newWidth, newHeight) { const newCanvasWidth = newWidth - margin.left - margin.right const newCanvasHeight = newHeight - margin.top - margin.bottom // Sync transform on resize if (canvasWidth !== 0 && canvasHeight !== 0) { zoomTransform = d3.zoomIdentity .translate( (zoomTransform.x * newCanvasWidth) / canvasWidth, (zoomTransform.y * newCanvasHeight) / canvasHeight ) .scale(zoomTransform.k) } width = newWidth height = newHeight canvasWidth = newCanvasWidth canvasHeight = newCanvasHeight heatmapChart() } function updateBuffer() { const d = data.data[dataTag] const height = d.length > 0 ? d[0].length : 0 const width = d.length const newColorScheme = getColorScheme(maxValue, brightness) bufferCanvas = createBuffer( normalizedData, width, height, newColorScheme.rasterizedColors ) colorScheme = newColorScheme } updateBuffer() heatmapChart() function heatmapChart() { let xHistogramCanvas = container .selectAll('canvas.x-histogram') .data([null]) xHistogramCanvas = xHistogramCanvas .enter() .append('canvas') .classed('x-histogram', true) .style('position', 'absolute') .style('z-index', '100') .merge(xHistogramCanvas) .attr('width', canvasWidth * window.devicePixelRatio) .attr('height', canvasHeight * window.devicePixelRatio) .style('width', canvasWidth + 'px') .style('height', 30 + 'px') .style('margin-top', height - 60 + 'px') .style('margin-left', margin.left + 'px') xHistogramCanvas .node() .getContext('2d') .scale(window.devicePixelRatio, window.devicePixelRatio) let yHistogramCanvas = container .selectAll('canvas.y-histogram') .data([null]) yHistogramCanvas = yHistogramCanvas .enter() .append('canvas') .classed('y-histogram', true) .style('position', 'absolute') .style('z-index', '101') .merge(yHistogramCanvas) .attr('width', 30 * window.devicePixelRatio) .attr('height', canvasHeight * window.devicePixelRatio) .style('width', 30 + 'px') .style('height', canvasHeight + 'px') .style('margin-top', margin.top + 'px') .style('margin-left', width - 30 + 'px') yHistogramCanvas .node() .getContext('2d') .scale(window.devicePixelRatio, window.devicePixelRatio) let labelCanvas = container.selectAll('canvas.label').data([null]) labelCanvas = labelCanvas .enter() .append('canvas') .classed('label', true) .style('position', 'absolute') .style('z-index', '102') .merge(labelCanvas) .style('width', 90 + 'px') .style('height', canvasHeight + 'px') .attr('width', 90 * window.devicePixelRatio) .attr('height', canvasHeight * window.devicePixelRatio) .style('margin-top', margin.top + 'px') labelCanvas .node() .getContext('2d') .scale(window.devicePixelRatio, window.devicePixelRatio) let canvas = container.selectAll('canvas.heatmap').data([null]) canvas = canvas .enter() .append('canvas') .classed('heatmap', true) .style('position', 'absolute') .style('z-index', '103') .merge(canvas) .attr('width', canvasWidth * heatmapCanvasPixelRatio) .attr('height', canvasHeight * heatmapCanvasPixelRatio) .style('width', canvasWidth + 'px') .style('height', canvasHeight + 'px') .style('margin-top', margin.top + 'px') .style('margin-right', margin.right + 'px') .style('margin-bottom', margin.bottom + 'px') .style('margin-left', margin.left + 'px') const ctx: CanvasRenderingContext2D = canvas.node().getContext('2d') ctx.imageSmoothingEnabled = false ctx.scale(heatmapCanvasPixelRatio, heatmapCanvasPixelRatio) let axis = container.selectAll('svg').data([null]) axis = axis .enter() .append('svg') .style('position', 'absolute') .style('z-index', '200') .merge(axis) .style('width', width + 'px') .style('height', height + 'px') let tooltipLayer = container.selectAll('div').data([null]) tooltipLayer = tooltipLayer .enter() .append('div') .style('position', 'absolute') .style('z-index', '300') .style('pointer-events', 'none') .merge(tooltipLayer) .style('width', width + 'px') .style('height', height + 'px') const xScale = d3 .scaleLinear() .domain([0, data.timeAxis.length - 1]) .range([0, canvasWidth]) const yScale = d3 .scaleLinear() .domain([0, data.keyAxis.length - 1]) .range([0, canvasHeight]) const xAxis = d3 .axisBottom(xScale) .tickFormat((idx) => data.timeAxis[idx as number] !== undefined ? // d3.timeFormat('%Y-%m-%d %H:%M:%S')( // new Date(data.timeAxis[idx as number] * 1000) // ) dayjs(data.timeAxis[idx as number] * 1000) .utcOffset(tz.getTimeZone()) .format('YYYY-MM-DD HH:mm:ss') : '' ) .ticks(width / 270) const labelAxis = labelAxisGroup(data.keyAxis).range([0, canvasHeight]) const histogramAxis = histogram(data.data[dataTag]) .xRange([0, canvasWidth]) .yRange([0, canvasHeight]) let xAxisG = axis.selectAll('g.x-axis').data([null]) xAxisG = xAxisG .enter() .append('g') .classed('x-axis', true) .merge(xAxisG) .attr('transform', 'translate(' + margin.left + ',' + (height - 20) + ')') d3.zoom().transform(axis, zoomTransform) const zoomBehavior = d3 .zoom() .scaleExtent([1, 128]) .on('zoom', zooming) .on('end', zoomEnd) function constrainBoucing(transform) { const bounceRatio = 0.8 const dragLeft = Math.max(0, transform.applyX(0)) const dragRight = Math.max(0, canvasWidth - transform.applyX(canvasWidth)) const dragTop = Math.max(0, transform.applyY(0)) const dragBottom = Math.max( 0, canvasHeight - transform.applyY(canvasHeight) ) return d3.zoomIdentity .translate( Math.floor(transform.x - (dragLeft - dragRight) * bounceRatio), Math.floor(transform.y - (dragTop - dragBottom) * bounceRatio) ) .scale(transform.k) } function constrainHard(transform) { let dx0 = transform.invertX(0), dx1 = transform.invertX(canvasWidth) - canvasWidth, dy0 = transform.invertY(0), dy1 = transform.invertY(canvasHeight) - canvasHeight return transform.translate( dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1), dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1) ) } function zooming() { onZoom() if (d3.event.sourceEvent && d3.event.sourceEvent.type === 'mousemove') { zoomTransform = constrainBoucing(d3.event.transform) hideTooltips() } else { zoomTransform = constrainHard(d3.event.transform) showTooltips() } render() } function zoomEnd() { zoomTransform = constrainHard(zoomTransform) axis.call(d3.zoom().transform, zoomTransform) if (tooltipStatus.pinned) { showTooltips() } render() } function focusPoint(x: number, y: number) { focusStatus = { xDomain: [x, x + 0.001], yDomain: [y, y + 0.001] } } function hoverBehavior(axis) { axis.on('mousemove', () => { showTooltips() render() }) axis.on('mouseout', () => { if (!tooltipStatus.pinned && !isBrushing) { focusStatus = null render() } }) } function showTooltips() { tooltipStatus.hidden = false if (!tooltipStatus.pinned) { const mouseCanvasOffset = d3.mouse(canvas.node()) if (isNaN(mouseCanvasOffset[0])) return const xRescale = zoomTransform.rescaleX(xScale) const yRescale = zoomTransform.rescaleY(yScale) const x = xRescale.invert(mouseCanvasOffset[0]) const y = yRescale.invert(mouseCanvasOffset[1]) if (!isBrushing) focusPoint(x, y) if ( mouseCanvasOffset[0] < 0 || mouseCanvasOffset[0] > canvasWidth || mouseCanvasOffset[1] < 0 || mouseCanvasOffset[1] > canvasHeight ) { hideTooltips() } else { tooltipStatus.x = x tooltipStatus.y = y } } } function hideTooltips() { tooltipStatus.hidden = true } function hideAxisTicksWithoutLabel() { axis.selectAll('.tick text').each(function () { if (this.innerHTML === '') { this.parentNode.style.display = 'none' } }) } axis.on('click', clicked) function clicked() { if (d3.event.defaultPrevented) return // zoom const mouseCanvasOffset = d3.mouse(canvas.node()) if ( mouseCanvasOffset[0] < 0 || mouseCanvasOffset[0] > canvasWidth || mouseCanvasOffset[1] < 0 || mouseCanvasOffset[1] > canvasHeight ) { return } tooltipStatus.pinned = !tooltipStatus.pinned showTooltips() render() } axis.call(zoomBehavior) axis.call(hoverBehavior) function render() { renderHeatmap() // renderHighlight() renderAxis() renderBrush() renderTooltip() renderCross() legend(colorScheme, dataTag) } function renderHeatmap() { ctx.clearRect(0, 0, canvasWidth, canvasHeight) ctx.drawImage( bufferCanvas, xScale.invert(zoomTransform.invertX(0)), yScale.invert(zoomTransform.invertY(0)), xScale.invert(canvasWidth * (1 / zoomTransform.k)), yScale.invert(canvasHeight * (1 / zoomTransform.k)), 0, 0, canvasWidth, canvasHeight ) } // function renderHighlight() { // const selectedData = data.data[dataTag] // const xLen = selectedData.length // const yLen = selectedData[0].length // const xRescale = zoomTransform.rescaleX(xScale) // const yRescale = zoomTransform.rescaleY(yScale) // const xStartIdx = Math.max(0, Math.floor(xScale.invert(0))) // const xEndIdx = Math.min(xLen - 1, Math.ceil(xScale.invert(canvasWidth))) // const yStartIdx = Math.max(0, Math.floor(yScale.invert(0))) // const yEndIdx = Math.min(yLen - 1, Math.ceil(yScale.invert(canvasHeight))) // ctx.shadowColor = '#fff' // ctx.shadowBlur = 9 + zoomTransform.k // 10 + 1 * (zoomTransform.k - 1) // ctx.fillStyle = 'blue' // for (let x = xStartIdx; x < xEndIdx; x++) { // for (let y = yStartIdx; y < yEndIdx; y++) { // if (selectedData[x][y] > maxValue / 2) { // const left = xRescale(x) // const top = yRescale(y) // const right = xRescale(x + 1) // const bottom = yRescale(y + 1) // const width = right - left // const height = bottom - top // const xPadding = ((0.8 + 0.5 * (1 - 1 / zoomTransform.k)) * width) / height // const yPadding = ((0.8 + 0.5 * (1 - 1 / zoomTransform.k)) * height) / width // ctx.beginPath() // ctx.shadowOffsetX = (left + 1000) * heatmapCanvasPixelRatio // ctx.shadowOffsetY = (top + 1000) * heatmapCanvasPixelRatio // ctx.fillRect(-1000 - xPadding, -1000 - yPadding, right - left + xPadding * 2, bottom - top + yPadding * 2) // ctx.closePath() // } // } // } // } function renderAxis() { const xRescale = zoomTransform.rescaleX(xScale) const yRescale = zoomTransform.rescaleY(yScale) histogramAxis( xHistogramCanvas.node().getContext('2d'), yHistogramCanvas.node().getContext('2d'), focusStatus?.xDomain, focusStatus?.yDomain, xRescale, yRescale ) labelAxis( labelCanvas.node().getContext('2d'), focusStatus?.yDomain, yRescale ) xAxisG.call(xAxis.scale(xRescale)) hideAxisTicksWithoutLabel() } function renderBrush() { if (isBrushing) { const brush = d3 .brush() .extent([ [0, 0], [canvasWidth, canvasHeight] ]) .on('start', brushStart) .on('brush', brushing) .on('end', brushEnd) let brushSvg = axis.selectAll('g.brush').data([null]) brushSvg = brushSvg .enter() .append('g') .classed('brush', true) .merge(brushSvg) .attr( 'transform', 'translate(' + margin.left + ',' + margin.top + ')' ) .call(brush) function brushStart() { hideTooltips() render() } function brushing() { const selection = d3.event.selection if (selection) { const xRescale = zoomTransform.rescaleX(xScale) const yRescale = zoomTransform.rescaleY(yScale) focusStatus = { xDomain: [ xRescale.invert(selection[0][0]), xRescale.invert(selection[1][0]) ], yDomain: [ yRescale.invert(selection[0][1]), yRescale.invert(selection[1][1]) ] } render() } } function brushEnd() { brushSvg.remove() isBrushing = false const selection = d3.event.selection if (selection) { brush.move(brushSvg, null) const xRescale = zoomTransform.rescaleX(xScale) const yRescale = zoomTransform.rescaleY(yScale) const startTime = data.timeAxis[Math.floor(xRescale.invert(selection[0][0]))] const endTime = data.timeAxis[Math.ceil(xRescale.invert(selection[1][0]))] const startKey = data.keyAxis[Math.ceil(yRescale.invert(selection[1][1]))].key const endKey = data.keyAxis[Math.floor(yRescale.invert(selection[0][1]))].key onBrush({ starttime: startTime, endtime: endTime, startkey: startKey, endkey: endKey }) } showTooltips() render() } } else { axis.selectAll('g.brush').remove() } } function getTooltipOverviewLabel(keyIdx) { const startLabel = data.keyAxis[keyIdx]!.labels const endLabel = data.keyAxis[keyIdx - 1]!.labels if (!startLabel && !endLabel) { return [] } if (!startLabel) { return endLabel } if (!endLabel || _.isEqual(startLabel, endLabel)) { return startLabel } const startLen = startLabel.length const endLen = endLabel.length // Cross start boundary, only use end label if ( startLen >= 1 && startLen + 1 === endLen && _.isEqual(startLabel, endLabel.slice(0, startLen)) ) { return endLabel } // range if ( startLen >= 3 && startLen === endLen && _.isEqual( startLabel.slice(0, startLen - 1), endLabel.slice(0, startLen - 1) ) ) { return [ ...startLabel.slice(0, startLen - 1), `${startLabel[startLen - 1]} ~ ${endLabel[startLen - 1]}` ] } // Cross end boundary, only use start label return startLabel } function renderTooltip() { if (tooltipStatus.hidden) { tooltipLayer.selectAll('div').remove() return } const timeIdx = Math.floor(tooltipStatus.x) const keyIdx = Math.floor(tooltipStatus.y) if (data.keyAxis[keyIdx] == null || data.keyAxis[keyIdx + 1] == null) { return } if ( data.timeAxis[timeIdx] == null || data.timeAxis[timeIdx + 1] == null ) { return } const xRescale = zoomTransform.rescaleX(xScale) const yRescale = zoomTransform.rescaleY(yScale) const canvasOffset = [ xRescale(tooltipStatus.x)!, yRescale(tooltipStatus.y)! ] let tooltipDiv = tooltipLayer.selectAll('div').data([null]) tooltipDiv = tooltipDiv .enter() .append('div') .style('position', 'absolute') // .style('width', tooltipSize.width + 'px') // .style('height', tooltipSize.height + 'px') .classed('tooltip', true) .merge(tooltipDiv) .style('pointer-events', tooltipStatus.pinned ? 'all' : 'none') if (canvasOffset[0] < canvasWidth / 2) { // Left half const v = canvasOffset[0] + tooltipOffset.horizontal + margin.left tooltipDiv.style('left', `${v}px`).style('right', 'auto') } else { // Right half const v = canvasWidth - canvasOffset[0] + tooltipOffset.horizontal + margin.right tooltipDiv.style('right', `${v}px`).style('left', 'auto') } if (canvasOffset[1] < canvasHeight / 2) { // Top half const v = canvasOffset[1] + tooltipOffset.vertical + margin.top tooltipDiv.style('top', `${v}px`).style('bottom', 'auto') } else { // Bottom half const v = canvasHeight - canvasOffset[1] + tooltipOffset.vertical + margin.bottom tooltipDiv.style('bottom', `${v}px`).style('top', 'auto') } const value = data.data[dataTag]?.[timeIdx]?.[keyIdx] let valueDiv = tooltipDiv.selectAll('div.value').data([null]) valueDiv = valueDiv .enter() .append('div') .classed('value', true) .merge(valueDiv) let valueText = valueDiv.selectAll('div.value').data([null]) valueText .enter() .append('div') .classed('value', true) .merge(valueText) .text(withUnit(value)) .style('color', colorScheme.label(value)) .style('background-color', colorScheme.background(value)) let unitText = valueDiv.selectAll('div.unit').data([null]) unitText .enter() .append('div') .classed('unit', true) .merge(unitText) .text(tagUnit(dataTag)) const timeText = [timeIdx, timeIdx + 1] .map((idx) => // d3.timeFormat('%Y-%m-%d\n%H:%M:%S')( // new Date(data.timeAxis[idx] * 1000) // ) dayjs(data.timeAxis[idx as number] * 1000) .utcOffset(tz.getTimeZone()) .format('YYYY-MM-DD HH:mm:ss') ) .join(' ~ ') let timeDiv = tooltipDiv.selectAll('button.time').data([timeText]) timeDiv .enter() .append('button') .classed('time', true) .merge(timeDiv) .call(clickToCopyBehavior, (d) => d) .text((d) => d) let overviewLabelDiv = tooltipDiv .selectAll('div.overviewLabel') .data([keyIdx + 1]) overviewLabelDiv = overviewLabelDiv .enter() .append('div') .classed('overviewLabel', true) .merge(overviewLabelDiv) let overviewSubLabel = overviewLabelDiv .selectAll('.subLabel') .style('display', 'none') .data((keyIdx) => getTooltipOverviewLabel(keyIdx)) overviewSubLabel .enter() .append('button') .classed('subLabel', true) .merge(overviewSubLabel) .call(clickToCopyBehavior, (d) => d) .text((d, idx) => { // Prefix with spaces return '\u00A0'.repeat(idx * 2) + d }) .style('display', 'block') let keyContainer = tooltipDiv.selectAll('div.keyContainer').data([ { desc: 'Start Key (Incl.):', idx: keyIdx + 1 }, { desc: 'End key (Excl.):', idx: keyIdx } ]) keyContainer = keyContainer .enter() .append('div') .classed('keyContainer', true) .merge(keyContainer) let descText = keyContainer.selectAll('.desc').data((d) => [d]) descText .enter() .append('div') .classed('desc', true) .merge(descText) .text(({ desc }) => desc) let keyText = keyContainer.selectAll('button.key').data((d) => [d]) keyText .enter() .append('button') .classed('key', true) .merge(keyText) .call(clickToCopyBehavior, ({ idx }) => data.keyAxis[idx]!.key) .text(({ idx }) => data.keyAxis[idx]!.key) } function renderCross() { if (tooltipStatus.pinned) { const xRescale = zoomTransform.rescaleX(xScale) const yRescale = zoomTransform.rescaleY(yScale) const canvasOffset = [ xRescale(tooltipStatus.x)!, yRescale(tooltipStatus.y)! ] const crossCenterPadding = 3 const crossBorder = 1 const crossSize = 8 const crossWidth = 2 ctx.lineWidth = crossWidth + 2 * crossBorder ctx.strokeStyle = '#111' ctx.beginPath() ctx.moveTo(canvasOffset[0], canvasOffset[1] - crossSize - crossBorder) ctx.lineTo( canvasOffset[0], canvasOffset[1] - crossCenterPadding + crossBorder ) ctx.moveTo( canvasOffset[0], canvasOffset[1] + crossCenterPadding - crossBorder ) ctx.lineTo(canvasOffset[0], canvasOffset[1] + crossSize + crossBorder) ctx.moveTo(canvasOffset[0] - crossSize - crossBorder, canvasOffset[1]) ctx.lineTo( canvasOffset[0] - crossCenterPadding + crossBorder, canvasOffset[1] ) ctx.moveTo( canvasOffset[0] + crossCenterPadding - crossBorder, canvasOffset[1] ) ctx.lineTo(canvasOffset[0] + crossSize + crossBorder, canvasOffset[1]) ctx.stroke() ctx.lineWidth = crossWidth ctx.strokeStyle = '#eee' ctx.beginPath() ctx.moveTo(canvasOffset[0], canvasOffset[1] - crossSize) ctx.lineTo(canvasOffset[0], canvasOffset[1] - crossCenterPadding) ctx.moveTo(canvasOffset[0], canvasOffset[1] + crossCenterPadding) ctx.lineTo(canvasOffset[0], canvasOffset[1] + crossSize) ctx.moveTo(canvasOffset[0] - crossSize, canvasOffset[1]) ctx.lineTo(canvasOffset[0] - crossCenterPadding, canvasOffset[1]) ctx.moveTo(canvasOffset[0] + crossCenterPadding, canvasOffset[1]) ctx.lineTo(canvasOffset[0] + crossSize, canvasOffset[1]) ctx.stroke() } } render() } return heatmapChart } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/heatmap/color.ts ================================================ import * as d3 from 'd3' const heatmapColor = d3.interpolateRgbBasis([ '#000000', '#080808', '#090909', '#101010', '#111111', '#121212', '#131313', '#141414', '#151515', '#171717', '#181818', '#191919', '#410c74', '#72067b', '#b00f53', '#fcc734', '#fbfc43', '#ffffb0' ]) export const rasterizeLevel = 100 export type ColorScale = (val: number) => d3.RGBColor export type ColorScheme = { background: ColorScale label: ColorScale maxValue: number rasterizedColors: Uint32Array } export function getColorScheme( maxValue: number, brightness: number ): ColorScheme { const logScale = (d3 as any).scaleSymlog().domain([0, maxValue / brightness]) const backgroundColorScale = (d: number) => d3.color(heatmapColor(logScale(d)))! as d3.RGBColor const labelColorScale = (d: number) => d3.hsl(backgroundColorScale(d)).l > 0.5 ? (d3.color('black')! as d3.RGBColor) : (d3.color('white')! as d3.RGBColor) const rasterizedColors = new Uint32Array(rasterizeLevel + 1) for (let i = 0; i <= rasterizeLevel; i++) { const color = d3.color( backgroundColorScale(Math.pow(maxValue, i / rasterizeLevel)) ) const colorRgb = color.rgb() rasterizedColors[i] = colorRgb.r | (colorRgb.g << 8) | (colorRgb.b << 16) | 0xff000000 } return { background: backgroundColorScale, label: labelColorScale, maxValue: maxValue, rasterizedColors } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/heatmap/index.tsx ================================================ import React, { useRef, useEffect } from 'react' import * as d3 from 'd3' import { useEventListener } from 'ahooks' import { heatmapChart } from './chart' function _Heatmap(props) { const divRef: React.RefObject = useRef(null) const chart = useRef(null) function updateChartSize() { if (divRef.current == null) { return } if (!chart.current) { return } const container = divRef.current const width = container.offsetWidth const height = container.offsetHeight chart.current.size(width, height) } useEffect(() => { const init = async () => { if (divRef.current != null) { const container = divRef.current chart.current = await heatmapChart( d3.select(container), props.data, props.dataTag, props.onBrush, props.onZoom ) props.onChartInit(chart.current) updateChartSize() } } init() }, [props, props.data, props.dataTag]) useEventListener('resize', () => { updateChartSize() }) return
} export const Heatmap = React.memo(_Heatmap) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/heatmap/legend.ts ================================================ import * as d3 from 'd3' import _ from 'lodash' import { ColorScheme } from './color' import { DataTag } from './types' import { tagUnit, withUnit } from './utils' export default function (colorScheme: ColorScheme, dataTag: DataTag) { let marginRight = 120 let width = 500 let height = 50 let innerWidth = width - marginRight let innerHeight = 26 let tickCount = 5 if (document.querySelector('.PD-Cluster-Legend') === null) { return } let container = (d3 as any) .select('.PD-Cluster-Legend') .style('width', `${width}px`) .style('height', `${height}px`) let xScale = (d3 as any) .scaleSymlog() .domain([colorScheme.maxValue / 1000, colorScheme.maxValue]) .range([0, innerWidth]) let canvas = container.selectAll('canvas').data([null]) canvas = canvas .enter() .append('canvas') .style('position', 'absolute') .style('left', '0px') .style('top', '0px') .merge(canvas) .attr('width', width) .attr('height', height) const ctx: CanvasRenderingContext2D = canvas.node().getContext('2d') for (let x = 0; x < innerWidth; x++) { ctx.fillStyle = colorScheme.background(xScale.invert(x)).toString() ctx.fillRect(x, 0, 1, innerHeight) } let xAxis = d3 .axisBottom(xScale) .tickValues( _.range(0, tickCount + 1).map((d) => xScale.invert((innerWidth * d) / tickCount) ) ) .tickSize(innerHeight) .tickFormat((d) => withUnit(d as number)) let svg = container.selectAll('svg').data([null]) svg = svg .enter() .append('svg') .style('position', 'absolute') .style('left', '0px') .style('top', '0px') .merge(svg) .attr('width', width) .attr('height', height) let xAxisG = svg.selectAll('g').data([null]) xAxisG .enter() .append('g') .merge(xAxisG) .call(xAxis) .call((g) => { g.selectAll('.tick text').attr('y', innerHeight + 6) g.selectAll('.domain').remove() }) let unitLabel = container.selectAll('div').data([null]) unitLabel .enter() .append('div') .classed('unit', true) .merge(unitLabel) .text(tagUnit(dataTag)) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/heatmap/types.ts ================================================ import { DecoratorLabelKey, MatrixMatrix } from '@lib/client' export type KeyAxisEntry = DecoratorLabelKey export type HeatmapData = MatrixMatrix export type DataTag = | 'integration' | 'written_bytes' | 'read_bytes' | 'written_keys' | 'read_keys' export type HeatmapRange = { starttime?: number endtime?: number startkey?: string endkey?: string } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/heatmap/utils.ts ================================================ import * as d3 from 'd3' import { DataTag } from './types' export function tagUnit(tag: DataTag): string { switch (tag) { case 'integration': return 'bytes/min' case 'read_bytes': return 'bytes/min' case 'written_bytes': return 'bytes/min' case 'read_keys': return 'keys/min' case 'written_keys': return 'keys/min' } } export function withUnit(val: number): string { val = val || 0 if (val > 1024 * 1024 * 1024) { return (val / 1024 / 1024 / 1024).toFixed(2) + ' G' } else if (val > 1024 * 1024) { return (val / 1024 / 1024).toFixed(2) + ' M' } else if (val > 1024) { return (val / 1024).toFixed(2) + ' K' } else { return val.toFixed(2) } } export function truncateString(str: string, len: number): string { if (str.length > len) { return ( str.substr(0, len / 2 - 1) + '....' + str.substr(str.length - len / 2 + 1, str.length) ) } else { return str } } export function clickToCopyBehavior(selection, map) { selection.each(function (d) { d3.select(this).on('click', () => { copyToClipboard(map(d)) }) }) } function copyToClipboard(text: string) { const input = d3.select('body').append('input').attr('value', text) input.node()!.select() document.execCommand('copy') input.remove() } export function doEventsOnYield(generator): Promise { return new Promise((resolve, reject) => { let g = generator() let advance = () => { try { let r = g.next() if (r.done) resolve(undefined) } catch (e) { reject(e) } setTimeout(advance, 0) } advance() }) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/index.tsx ================================================ import React, { useContext } from 'react' import { Routes, Route, HashRouter as Router } from 'react-router-dom' import { Root } from '@lib/components' import { addTranslations } from '@lib/utils/i18n' import { useLocationChange } from '@lib/hooks/useLocationChange' import KeyViz from './components/KeyViz' import translations from './translations' import { KeyVizContext } from './context' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> ) } export default () => { const ctx = useContext(KeyVizContext) if (ctx === null) { throw new Error('KeyVizContext must not be null') } return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/translations/en.yaml ================================================ keyviz: nav_title: Key Visualizer toolbar: brightness: Brightness zoom: select: Select & Zoom reset: Reset view_type: read_bytes: Read (bytes) write_bytes: Write (bytes) read_keys: Read (keys) write_keys: Write (keys) all: All settings: title: Settings disabled_result: title: Feature Not Enabled sub_title: | Key Visualizer feature is not enabled so that visual reports cannot be viewed. You can modify settings to enable the feature and wait for new data being collected. open_setting: Open Settings close_keyviz: Disable Key Visualizer Feature close_keyviz_warning: Are you sure want to disable this feature? Current visual reports will be cleared. switch: Enable Feature switch_tooltip: Whether Key Visualizer feature is enabled. When enabled, there will be small overhead. policy: Policy policy_db: '{{distro.tidb}}' policy_kv: Raw KV separator: Separator separator_placeholder: The separator used to split Key separator_empty_warning: If left blank, Key will not be split actions: save: Save close: Disable cancel: Cancel help: Help help_url: https://docs.pingcap.com/tidb/dev/dashboard-key-visualizer ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/translations/zh.yaml ================================================ keyviz: nav_title: 流量可视化 toolbar: brightness: 调整亮度 zoom: select: 框选 reset: 重置 view_type: read_bytes: 读取字节量 write_bytes: 写入字节量 read_keys: 读取次数 write_keys: 写入次数 all: 所有 settings: title: 设置 disabled_result: title: 该功能未启用 sub_title: | 流量可视化功能未启用,因此无法查看可视化报告。 您可以修改设置打开该功能后等待新数据收集。 open_setting: 打开设置 close_keyviz: 关闭流量可视化功能 close_keyviz_warning: 确认要关闭该功能吗?关闭后现有历史记录也将被清空! switch: 启用功能 switch_tooltip: 是否启用流量可视化功能,关闭后将不能使用流量可视化功能,但能减少一些 {{distro.pd}} 的 CPU 资源开销。 policy: 模式 policy_db: '{{distro.tidb}}' policy_kv: 原生 KV separator: 分隔符 separator_placeholder: 用于切分 Key 的分隔符 separator_empty_warning: 分隔符为空串时,Key 将不会被切分 actions: save: 保存 close: 确认 cancel: 取消 help: 帮助 help_url: https://docs.pingcap.com/zh/tidb/dev/dashboard-key-visualizer ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/utils/api.ts ================================================ import { HeatmapData, HeatmapRange, DataTag } from '../heatmap/types' import { IKeyVizDataSource } from '../context' export async function fetchHeatmap( fetcher: IKeyVizDataSource['keyvisualHeatmapsGet'], selection?: HeatmapRange, type: DataTag = 'written_bytes' ): Promise { const resp = await fetcher( selection?.startkey, selection?.endkey, selection?.starttime, selection?.endtime, type ) reverse(resp.data) return resp.data } // Reverse the columns (key axis) of the matrix // so that the direction of the axis matches the first quadrant function reverse(data: HeatmapData) { data.keyAxis.reverse() for (const tag in data.data) { const d = data.data[tag] for (let col of d) { col.reverse() } } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/utils/index.ts ================================================ export * from './api' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/KeyViz/utils/telemetry.ts ================================================ import { mixpanel } from '@lib/utils/telemetry' export const telemetry = { changeLight() { mixpanel.track('KeyViz: Change Light') }, clickManualRefresh() { mixpanel.track('KeyViz: Click Manual Refresh') }, clickAutoRefresh() { mixpanel.track('KeyViz: Clikc Auto Refresh') }, changeTimeDuration(duration: number) { mixpanel.track('KeyViz: Change Time Duration', { duration }) }, changeMetric(metric: string) { mixpanel.track('KeyViz: Change Metric', { metric }) }, changeBright(bright: number) { mixpanel.track('KeyViz: Change Bright', { bright }) }, toggleBrush() { mixpanel.track('KeyViz: Toggle Brush') }, resetZoom() { mixpanel.track('KeyViz: Reset Zoom') }, openSetting() { mixpanel.track('KeyViz: Open Setting') }, openHelp() { mixpanel.track('KeyViz: Open Help') } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Monitoring/components/Monitoring.tsx ================================================ import { Space, Typography, Row, Col, Collapse, Tooltip } from 'antd' import React, { useCallback, useContext, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Stack } from 'office-ui-fabric-react' import { LoadingOutlined, FileTextOutlined } from '@ant-design/icons' import { useMemoizedFn } from 'ahooks' import { MetricsChart, SyncChartPointer, TimeRangeValue, QueryConfig, TransformNullValue } from 'metrics-chart' import { Link } from 'react-router-dom' import { debounce } from 'lodash' import { AutoRefreshButton, Card, DEFAULT_TIME_RANGE, TimeRange, Toolbar, ErrorBar, LimitTimeRange } from '@lib/components' import { useTimeRangeValue } from '@lib/components/TimeRangeSelector/hook' import { store } from '@lib/utils/store' import { tz } from '@lib/utils' import { telemetry } from '../utils/telemetry' import { MonitoringContext } from '../context' export default function Monitoring() { const ctx = useContext(MonitoringContext) const info = store.useState((s) => s.appInfo) const pdVersion = info?.version?.pd_version const [isSomeLoading, setIsSomeLoading] = useState(false) const { t } = useTranslation() const [timeRange, setTimeRange] = useState(DEFAULT_TIME_RANGE) const handleManualRefreshClick = () => { telemetry.clickManualRefresh() return setTimeRange((r) => ({ ...r })) } return ( <> { setTimeRange(v) telemetry.selectTimeRange(v) }} onZoomOutClick={(start, end) => telemetry.clickZoomOut([start, end]) } /> {ctx?.cfg.metricsReferenceLink && ( telemetry.clickDocumentationIcon()} /> )} {isSomeLoading && } {ctx?.cfg.getMetricsQueries(pdVersion).map((item) => ( <> {item.category ? ( ) : ( )} ))} ) } interface MetricsChartWrapperProps { metrics: { title: string queries: QueryConfig[] unit: string nullValue?: TransformNullValue }[] timeRange: TimeRange setTimeRange: (timeRange: TimeRange) => void setIsSomeLoading: (isLoading: boolean) => void } const MetricsChartWrapper: React.FC = ({ metrics, timeRange, setTimeRange, setIsSomeLoading }) => { const ctx = useContext(MonitoringContext) const loadingCounter = useRef(0) const [chartRange, setChartRange] = useTimeRangeValue(timeRange, setTimeRange) const { t } = useTranslation() const promAddrConfigurable = ctx?.cfg.promAddrConfigurable || false // eslint-disable-next-line const setIsSomeLoadingDebounce = useCallback( debounce(setIsSomeLoading, 100, { leading: true }), [] ) const handleOnBrush = (range: TimeRangeValue) => { setChartRange(range) } const onLoadingStateChange = useMemoizedFn((loading: boolean) => { loading ? (loadingCounter.current += 1) : loadingCounter.current > 0 && (loadingCounter.current -= 1) setIsSomeLoadingDebounce(loadingCounter.current > 0) }) const ErrorComponent = (error: Error) => ( {promAddrConfigurable && ( {t('monitoring.change_prom_button')} )} ) return ( {metrics.map((item) => ( {item.title} telemetry.clickSeriesLabel(item.title, seriesName) } /> ))} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Monitoring/context/index.ts ================================================ import { createContext } from 'react' import { MetricsQueryResponse } from '@lib/client' import { QueryConfig, TransformNullValue } from 'metrics-chart' export interface MetricsQueryType { category?: string metrics: { title: string queries: QueryConfig[] unit: string nullValue?: TransformNullValue }[] } interface IMetricConfig { getMetricsQueries: (pdVersion: string | undefined) => MetricsQueryType[] promAddrConfigurable?: boolean timeRangeSelector?: { recent_seconds: number[] customAbsoluteRangePicker: boolean } metricsReferenceLink?: string } export interface IMonitoringDataSource { metricsQueryGet(params: { endTimeSec?: number query?: string startTimeSec?: number stepSec?: number }): Promise } export interface IMonitoringContext { ds: IMonitoringDataSource cfg: IMetricConfig } export const MonitoringContext = createContext(null) export const MonitoringProvider = MonitoringContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Monitoring/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router } from 'react-router-dom' import { Root } from '@lib/components' import { addTranslations } from '@lib/utils/i18n' import translations from './translations' import { MonitoringContext } from './context' import { useLocationChange } from '@lib/hooks/useLocationChange' import Monitoring from './components/Monitoring' addTranslations(translations) function AppRoutes() { useLocationChange() return } export default function () { const ctx = useContext(MonitoringContext) if (ctx === null) { throw new Error('MonitoringContext must not be null') } return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Monitoring/translations/en.yaml ================================================ monitoring: nav_title: Monitoring panel_no_data_tips: Documentation change_prom_button: Change Prometheus Address category: application_connection: Application Connection database_time: Database Time sql_count: SQL Count core_feature_usage: Core Feature Usage latency_break_down: Latency Break Down transaction: Transaction core_path_duration: Core Path Duration server: Server ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Monitoring/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Monitoring/translations/zh.yaml ================================================ monitoring: nav_title: 监控指标 panel_no_data_tips: 文档 change_prom_button: 更改 prometheus 地址 category: application_connection: 应用连接 database_time: 数据库时间 sql_count: SQL 数量 core_feature_usage: 核心功能使用情况 latency_break_down: 延迟及拆解 transaction: 事务 core_path_duration: 核心流程耗时 server: 服务 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Monitoring/utils/telemetry.ts ================================================ import { TimeRange } from '@lib/components' import { mixpanel } from '@lib/utils/telemetry' export const telemetry = { // time range picker clickZoomOut(timestamps: [number, number]) { mixpanel.track('Monitoring: Click Zoom Out Button', { timestamps }) }, selectTimeRange(v: TimeRange) { mixpanel.track('Monitoring: Select Time Range', v) }, clickManualRefresh() { mixpanel.track('Monitoring: Click Manual Refresh') }, selectAutoRefreshOption(seconds: number) { mixpanel.track('Monitoring: Select Auto Refresh Option', { seconds }) }, clickDocumentationIcon() { mixpanel.track('Monitoring: Click Documentation Icon') }, clickSeriesLabel(chartTitle: string, seriesName: string) { mixpanel.track('Monitoring: Click to Hide Series', { chartTitle, seriesName }) } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/components/LogicalOperatorTree.tsx ================================================ import React, { useEffect, useRef } from 'react' import { graphviz } from 'd3-graphviz' import styles from './OperatorTree.module.less' interface LogicalOperatorTreeProps { data: LogicalOperatorNode[] labels?: any className?: string nodeName?: string onSelect?: (name: string) => void } export interface LogicalOperatorNode { id: number children: number[] // children id type: string cost: number selected: boolean property: string info: string } export default function LogicalOperatorTree({ data, labels = {}, className, nodeName, onSelect }: LogicalOperatorTreeProps) { const containerRef = useRef(null) useEffect(() => { const containerEl = containerRef.current if (!containerEl) { return } const define = data .map( (n) => `${n.id} ${createLabels({ label: `${n.type}_${n.id}`, color: labels.color || '', fillcolor: `${n.type}_${n.id}` === nodeName ? '#bfdbfe' : 'white', tooltip: `info: ${n.info}` })};\n` ) .join('') const link = data .map((n) => (n.children || []) .map((c) => `${n.id} -> ${c} ${createLabels(labels)};\n`) .join('') ) .join('') graphviz(containerEl).renderDot( `digraph { node [shape=ellipse fontsize=8 fontname="Verdana" style="filled"]; ${define}\n${link}\n}` ) }, [containerRef, data, labels, nodeName]) // find clicked node function handleClick(e) { const trigger = e.target const parent = e.target.parentNode if ( (trigger?.tagName === 'text' || trigger?.tagName === 'ellipse') && parent?.tagName === 'a' ) { for (const el of parent.children) { if (el.tagName === 'text') { onSelect?.(el.innerHTML) break } } } } return (
) } export function createLabels(labels: { [props: string]: string } = {}): string { const ls = Object.entries(labels).filter(([k, v]) => !!v) if (!ls.length) { return '' } return `[${ls.map(([k, v]) => `${k}="${v}"`).join(' ')}]` } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/components/OperatorTree.module.less ================================================ .operator_tree { height: 300px; width: 200px; svg { height: 100%; width: 100%; } } .cost_tree { width: 100%; height: 600px; } .tree_container { position: relative; } .tree_container:hover .fullscreen_icon_box { opacity: 1; } .fullscreen_icon_box { position: absolute; padding: 4px; top: 0; right: 0; opacity: 0; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/components/PhysicalCostTree.tsx ================================================ import React, { useEffect, useRef, useMemo, useState } from 'react' import { graphviz } from 'd3-graphviz' import { createLabels } from './LogicalOperatorTree' import styles from './OperatorTree.module.less' export interface PhyscialCostParam { id: number // for generate graphviz name: string desc: string cost: number // null means this node can be replaced by a root node // undefined means this node is a leaf node, it is converted from a number leaf node params: null | undefined | { [x: string]: number | PhyscialCostParam } } export interface PhysicalCostRoot { id: number type: string cost: number desc: string params: { [x: string]: number | PhyscialCostParam } } export interface PhysicalCostMap { [x: string]: PhysicalCostRoot } interface PhysicalCostTreeProps { costs: PhysicalCostMap name: string className?: string } let globalId = 1 function buildCostParam(costs: PhysicalCostMap, param: PhyscialCostParam) { param.id = globalId++ if (param.params === null) { // if null, means this cost is a PhysicalCostRoot const root = costs[param.name] if (!root) { throw new Error(`cost for ${param.name} not exist`) } param.params = root.params // nested operator desc may be not correct, let's fix it by root.desc param.desc = root.desc } if (param.params === undefined) { // reach leaf node return } // traverse buildCostParams(costs, param.params) } function buildCostParams( costs: PhysicalCostMap, params: { [x: string]: number | PhyscialCostParam } ) { Object.keys(params).forEach((k) => { const v = params[k] if (typeof v === 'number') { // convert the leaf node // its orignal type is number // convert to leaf PhysicalCostParam with `params: undefined` params[k] = { id: globalId++, name: k, desc: '', params: undefined, cost: v } } else { buildCostParam(costs, v) } }) } function buildCostTree(costs: PhysicalCostMap, root: PhysicalCostRoot) { globalId = root.id + 1 buildCostParams(costs, root.params) } ///////////// interface BoolMap { [x: string]: boolean } type Expands = BoolMap ///////////// function genGraphvizNodeParam( param: PhyscialCostParam, strArr: string[], expands: Expands ) { let str = '' if (param.params === undefined) { // leaf node str = `${param.id} ${createLabels({ label: `${param.name}\n${param.cost.toFixed(4)}`, fillcolor: '#cffafe', tooltip: `${param.id}` })};\n` } else { str = `${param.id} ${createLabels({ label: `${param.name}\ncost: ${param.cost.toFixed(4)}\ndesc: ${ param.desc }`, fillcolor: 'white', tooltip: `${param.id}` })};\n` } strArr.push(str) if (param.params === null || param.params === undefined) { return } if (expands[param.id] !== true) { // not expand return } genGraphvizNodeParams(param.params, strArr, expands) } function genGraphvizNodeParams( params: { [x: string]: number | PhyscialCostParam }, strArr: string[], expands: Expands ) { Object.values(params).forEach((p) => { // number has already converted to PhyscialCostParam // it doesn't exist alreay in fact if (typeof p !== 'number') { genGraphvizNodeParam(p, strArr, expands) } }) } function genGraphvizNodes(root: PhysicalCostRoot, expands: Expands) { const strArr: string[] = [] strArr.push( `${root.id} ${createLabels({ label: `${root.type}_${root.id}\ncost: ${root.cost.toFixed(4)}\ndesc: ${ root.desc }`, fillcolor: 'white', tooltip: `${root.id}` })};\n` ) if (expands[root.id] === true) { genGraphvizNodeParams(root.params, strArr, expands) } return strArr } ////////////////////// function genGraphvizLineParam( parentId: number, param: PhyscialCostParam, strArr: string[], expands: Expands ) { if (expands[parentId] !== true) { return } strArr.push(`${parentId} -> ${param.id};\n`) if (param.params === null || param.params === undefined) { return } genGraphvizLineParams(param.id, param.params, strArr, expands) } function genGraphvizLineParams( parentId: number, params: { [x: string]: number | PhyscialCostParam }, strArr: string[], expands: Expands ) { Object.values(params).forEach((p) => { // number has already converted to PhyscialCostParam // it doesn't exist alreay in fact if (typeof p !== 'number') { genGraphvizLineParam(parentId, p, strArr, expands) } }) } function genGraphvizLines(root: PhysicalCostRoot, expands: Expands) { const strArr: string[] = [] genGraphvizLineParams(root.id, root.params, strArr, expands) return strArr } ////////////////////// export default function PhysicalCostTree({ costs, name, className }: PhysicalCostTreeProps) { const costRoot = useMemo(() => { const root = costs[name] if (root) { buildCostTree(costs, root) } return root }, [costs, name]) const [nodeExpands, setNodeExpands] = useState({}) const containerRef = useRef(null) useEffect(() => { setNodeExpands({}) }, [name]) useEffect(() => { if (!costRoot) { return } const containerEl = containerRef.current if (!containerEl) { return } const define = genGraphvizNodes(costRoot, nodeExpands).join('') const link = genGraphvizLines(costRoot, nodeExpands).join('') graphviz(containerEl).renderDot( `digraph { node [shape=ellipse fontsize=8 fontname="Verdana" style="filled"]; ${define}\n${link}\n}` ) }, [containerRef, costRoot, nodeExpands]) function handleClick(e) { const trigger = e.target const parent = e.target.parentNode // find clicked node if ( (trigger?.tagName === 'text' || trigger?.tagName === 'ellipse') && parent?.tagName === 'a' ) { console.log('title:', parent.getAttribute('title')) const id = parent.getAttribute('title') // toggle setNodeExpands({ ...nodeExpands, [id]: !(nodeExpands[id] ?? false) }) } } return (
{costRoot ? (
) : (

Not exist

)}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/components/PhysicalOperatorTree.tsx ================================================ import React, { useEffect, useRef } from 'react' import { graphviz } from 'd3-graphviz' import { FullscreenOutlined } from '@ant-design/icons' import { LogicalOperatorNode, createLabels } from './LogicalOperatorTree' import styles from './OperatorTree.module.less' export interface PhysicalOperatorNode extends LogicalOperatorNode { parentNode: null | PhysicalOperatorNode childrenNodes: PhysicalOperatorNode[] mapping: string } interface PhysicalOperatorTreeProps { data: PhysicalOperatorNode className?: string onSelect?: (name: string) => void nodeName: string } function convertTreeToArry( node: PhysicalOperatorNode, arr: PhysicalOperatorNode[] ) { arr.push(node) if (node.childrenNodes) { node.childrenNodes.forEach((n) => convertTreeToArry(n, arr)) } } export default function PhysicalOperatorTree({ data, className, onSelect, nodeName }: PhysicalOperatorTreeProps) { const containerRef = useRef(null) useEffect(() => { const containerEl = containerRef.current if (!containerEl) { return } let allDatas: PhysicalOperatorNode[] = [] convertTreeToArry(data, allDatas) const define = allDatas .map( (n) => `${n.id} ${createLabels({ label: `${n.type}_${n.id}\ncost: ${n.cost.toFixed(4)}`, color: n.selected ? '#4169E1' : '', fillcolor: `${n.type}_${n.id}` === nodeName ? '#7dd3fc' : 'white', tooltip: `info: ${n.info}` })};\n` ) .join('') const link = allDatas .map((n) => (n.children || []) .map( (c) => `${n.id} -> ${c} ${createLabels({ color: n.selected ? '#4169E1' : '' })};\n` ) .join('') ) .join('') graphviz(containerEl).renderDot( `digraph { node [shape=ellipse fontsize=8 fontname="Verdana" style="filled"]; ${define}\n${link}\n}` ) }, [containerRef, data, nodeName]) // find clicked node function handleClick(e) { const trigger = e.target const parent = e.target.parentNode if ( (trigger?.tagName === 'text' || trigger?.tagName === 'ellipse') && parent?.tagName === 'a' ) { for (const el of parent.children) { if (el.tagName === 'text') { onSelect?.(el.innerHTML) break } } } } return (
) } export function PhysicalOperatorTreeWithFullScreen({ onFullScreen, ...rest }: PhysicalOperatorTreeProps & { onFullScreen: () => void }) { return (
onFullScreen()} />
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/context/index.ts ================================================ import { createContext } from 'react' // import { AxiosPromise } from 'axios' // import {} from '@lib/client' // import { ReqConfig } from '@lib/types' export interface IOptimizerTraceDataSource {} export interface IOptimizerTraceContext { ds: IOptimizerTraceDataSource } export const OptimizerTraceContext = createContext(null) export const OptimizerTraceProvider = OptimizerTraceContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/examples/new-format.json ================================================ { "logical": { "final": [ { "type": "DataSource", "property": "", "info": "table:customer", "children": [], "id": 1, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:orders", "children": [], "id": 2, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join, equal:[eq(test.customer.c_custkey, test.orders.o_custkey)]", "children": [1, 2], "id": 12, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:lineitem", "children": [], "id": 4, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join, equal:[eq(test.orders.o_orderkey, test.lineitem.l_orderkey)]", "children": [12, 4], "id": 13, "cost": 0, "selected": false }, { "type": "Aggregation", "property": "", "info": "group by:test.lineitem.l_orderkey, test.orders.o_orderdate, test.orders.o_shippriority, funcs:sum(mul(test.lineitem.l_extendedprice, minus(1, test.lineitem.l_discount))), firstrow(test.orders.o_orderdate), firstrow(test.orders.o_shippriority), firstrow(test.lineitem.l_orderkey)", "children": [13], "id": 7, "cost": 0, "selected": false }, { "type": "TopN", "property": "", "info": "Column#35:desc, test.orders.o_orderdate, offset:0, count:10", "children": [7], "id": 11, "cost": 0, "selected": false }, { "type": "Projection", "property": "", "info": "test.lineitem.l_orderkey, Column#35, test.orders.o_orderdate, test.orders.o_shippriority", "children": [11], "id": 8, "cost": 0, "selected": false } ], "steps": [ { "name": "column_prune", "before": [ { "type": "DataSource", "property": "", "info": "table:customer", "children": [], "id": 1, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:orders", "children": [], "id": 2, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join", "children": [1, 2], "id": 3, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:lineitem", "children": [], "id": 4, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join", "children": [3, 4], "id": 5, "cost": 0, "selected": false }, { "type": "Selection", "property": "", "info": "eq(test.customer.c_custkey, test.orders.o_custkey), eq(test.customer.c_mktsegment, 'AUTOMOBILE'), eq(test.lineitem.l_orderkey, test.orders.o_orderkey), gt(test.lineitem.l_shipdate, 1995-03-13 00:00:00.000000), lt(test.orders.o_orderdate, 1995-03-13 00:00:00.000000)", "children": [5], "id": 6, "cost": 0, "selected": false }, { "type": "Aggregation", "property": "", "info": "group by:test.lineitem.l_orderkey, test.orders.o_orderdate, test.orders.o_shippriority, funcs:sum(mul(test.lineitem.l_extendedprice, minus(1, test.lineitem.l_discount))), firstrow(test.customer.c_custkey), firstrow(test.customer.c_name), firstrow(test.customer.c_address), firstrow(test.customer.c_nationkey), firstrow(test.customer.c_phone), firstrow(test.customer.c_acctbal), firstrow(test.customer.c_mktsegment), firstrow(test.customer.c_comment), firstrow(test.orders.o_orderkey), firstrow(test.orders.o_custkey), firstrow(test.orders.o_orderstatus), firstrow(test.orders.o_totalprice), firstrow(test.orders.o_orderdate), firstrow(test.orders.o_orderpriority), firstrow(test.orders.o_clerk), firstrow(test.orders.o_shippriority), firstrow(test.orders.o_comment), firstrow(test.lineitem.l_orderkey), firstrow(test.lineitem.l_partkey), firstrow(test.lineitem.l_suppkey), firstrow(test.lineitem.l_linenumber), firstrow(test.lineitem.l_quantity), firstrow(test.lineitem.l_extendedprice), firstrow(test.lineitem.l_discount), firstrow(test.lineitem.l_tax), firstrow(test.lineitem.l_returnflag), firstrow(test.lineitem.l_linestatus), firstrow(test.lineitem.l_shipdate), firstrow(test.lineitem.l_commitdate), firstrow(test.lineitem.l_receiptdate), firstrow(test.lineitem.l_shipinstruct), firstrow(test.lineitem.l_shipmode), firstrow(test.lineitem.l_comment), firstrow(test.lineitem._tidb_rowid)", "children": [6], "id": 7, "cost": 0, "selected": false }, { "type": "Projection", "property": "", "info": "test.lineitem.l_orderkey, Column#35, test.orders.o_orderdate, test.orders.o_shippriority", "children": [7], "id": 8, "cost": 0, "selected": false }, { "type": "Sort", "property": "", "info": "Column#35:desc, test.orders.o_orderdate", "children": [8], "id": 9, "cost": 0, "selected": false }, { "type": "Limit", "property": "", "info": "offset:0, count:10", "children": [9], "id": 10, "cost": 0, "selected": false } ], "steps": [ { "action": "Aggregation_7's columns[test.lineitem._tidb_rowid,test.lineitem.l_comment,test.lineitem.l_shipmode,test.lineitem.l_shipinstruct,test.lineitem.l_receiptdate,test.lineitem.l_commitdate,test.lineitem.l_shipdate,test.lineitem.l_linestatus,test.lineitem.l_returnflag,test.lineitem.l_tax,test.lineitem.l_discount,test.lineitem.l_extendedprice,test.lineitem.l_quantity,test.lineitem.l_linenumber,test.lineitem.l_suppkey,test.lineitem.l_partkey,test.orders.o_comment,test.orders.o_clerk,test.orders.o_orderpriority,test.orders.o_totalprice,test.orders.o_orderstatus,test.orders.o_custkey,test.orders.o_orderkey,test.customer.c_comment,test.customer.c_mktsegment,test.customer.c_acctbal,test.customer.c_phone,test.customer.c_nationkey,test.customer.c_address,test.customer.c_name,test.customer.c_custkey] have been pruned", "reason": "", "type": "Aggregation", "id": 7, "index": 0 }, { "action": "Aggregation_7's aggregation functions[firstrow(test.lineitem._tidb_rowid),firstrow(test.lineitem.l_comment),firstrow(test.lineitem.l_shipmode),firstrow(test.lineitem.l_shipinstruct),firstrow(test.lineitem.l_receiptdate),firstrow(test.lineitem.l_commitdate),firstrow(test.lineitem.l_shipdate),firstrow(test.lineitem.l_linestatus),firstrow(test.lineitem.l_returnflag),firstrow(test.lineitem.l_tax),firstrow(test.lineitem.l_discount),firstrow(test.lineitem.l_extendedprice),firstrow(test.lineitem.l_quantity),firstrow(test.lineitem.l_linenumber),firstrow(test.lineitem.l_suppkey),firstrow(test.lineitem.l_partkey),firstrow(test.orders.o_comment),firstrow(test.orders.o_clerk),firstrow(test.orders.o_orderpriority),firstrow(test.orders.o_totalprice),firstrow(test.orders.o_orderstatus),firstrow(test.orders.o_custkey),firstrow(test.orders.o_orderkey),firstrow(test.customer.c_comment),firstrow(test.customer.c_mktsegment),firstrow(test.customer.c_acctbal),firstrow(test.customer.c_phone),firstrow(test.customer.c_nationkey),firstrow(test.customer.c_address),firstrow(test.customer.c_name),firstrow(test.customer.c_custkey)] have been pruned", "reason": "", "type": "Aggregation", "id": 7, "index": 1 }, { "action": "DataSource_1's columns[test.customer.c_comment,test.customer.c_acctbal,test.customer.c_phone,test.customer.c_nationkey,test.customer.c_address,test.customer.c_name] have been pruned", "reason": "", "type": "DataSource", "id": 1, "index": 2 }, { "action": "DataSource_2's columns[test.orders.o_comment,test.orders.o_clerk,test.orders.o_orderpriority,test.orders.o_totalprice,test.orders.o_orderstatus] have been pruned", "reason": "", "type": "DataSource", "id": 2, "index": 3 }, { "action": "DataSource_4's columns[test.lineitem._tidb_rowid,test.lineitem.l_comment,test.lineitem.l_shipmode,test.lineitem.l_shipinstruct,test.lineitem.l_receiptdate,test.lineitem.l_commitdate,test.lineitem.l_linestatus,test.lineitem.l_returnflag,test.lineitem.l_tax,test.lineitem.l_quantity,test.lineitem.l_linenumber,test.lineitem.l_suppkey,test.lineitem.l_partkey] have been pruned", "reason": "", "type": "DataSource", "id": 4, "index": 4 } ], "index": 1 }, { "name": "predicate_push_down", "before": [ { "type": "DataSource", "property": "", "info": "table:customer", "children": [], "id": 1, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:orders", "children": [], "id": 2, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join", "children": [1, 2], "id": 3, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:lineitem", "children": [], "id": 4, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join", "children": [3, 4], "id": 5, "cost": 0, "selected": false }, { "type": "Selection", "property": "", "info": "eq(test.customer.c_custkey, test.orders.o_custkey), eq(test.customer.c_mktsegment, 'AUTOMOBILE'), eq(test.lineitem.l_orderkey, test.orders.o_orderkey), gt(test.lineitem.l_shipdate, 1995-03-13 00:00:00.000000), lt(test.orders.o_orderdate, 1995-03-13 00:00:00.000000)", "children": [5], "id": 6, "cost": 0, "selected": false }, { "type": "Aggregation", "property": "", "info": "group by:test.lineitem.l_orderkey, test.orders.o_orderdate, test.orders.o_shippriority, funcs:sum(mul(test.lineitem.l_extendedprice, minus(1, test.lineitem.l_discount))), firstrow(test.orders.o_orderdate), firstrow(test.orders.o_shippriority), firstrow(test.lineitem.l_orderkey)", "children": [6], "id": 7, "cost": 0, "selected": false }, { "type": "Projection", "property": "", "info": "test.lineitem.l_orderkey, Column#35, test.orders.o_orderdate, test.orders.o_shippriority", "children": [7], "id": 8, "cost": 0, "selected": false }, { "type": "Sort", "property": "", "info": "Column#35:desc, test.orders.o_orderdate", "children": [8], "id": 9, "cost": 0, "selected": false }, { "type": "Limit", "property": "", "info": "offset:0, count:10", "children": [9], "id": 10, "cost": 0, "selected": false } ], "steps": [ { "action": "The conditions[eq(test.customer.c_mktsegment, AUTOMOBILE)] are pushed down across DataSource_1", "reason": "", "type": "DataSource", "id": 1, "index": 0 }, { "action": "The conditions[lt(test.orders.o_orderdate, 1995-03-13 00:00:00.000000)] are pushed down across DataSource_2", "reason": "", "type": "DataSource", "id": 2, "index": 1 }, { "action": "The conditions[gt(test.lineitem.l_shipdate, 1995-03-13 00:00:00.000000)] are pushed down across DataSource_4", "reason": "", "type": "DataSource", "id": 4, "index": 2 }, { "action": "Selection_6 is removed", "reason": "The conditions[eq(test.customer.c_mktsegment, AUTOMOBILE),eq(test.customer.c_custkey, test.orders.o_custkey),eq(test.lineitem.l_orderkey, test.orders.o_orderkey),lt(test.orders.o_orderdate, 1995-03-13 00:00:00.000000),gt(test.lineitem.l_shipdate, 1995-03-13 00:00:00.000000)] in Selection_6 are pushed down", "type": "Selection", "id": 6, "index": 3 } ], "index": 10 }, { "name": "topn_push_down", "before": [ { "type": "DataSource", "property": "", "info": "table:customer", "children": [], "id": 1, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:orders", "children": [], "id": 2, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join, equal:[eq(test.customer.c_custkey, test.orders.o_custkey)]", "children": [1, 2], "id": 3, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:lineitem", "children": [], "id": 4, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join, equal:[eq(test.orders.o_orderkey, test.lineitem.l_orderkey)]", "children": [3, 4], "id": 5, "cost": 0, "selected": false }, { "type": "Aggregation", "property": "", "info": "group by:test.lineitem.l_orderkey, test.orders.o_orderdate, test.orders.o_shippriority, funcs:sum(mul(test.lineitem.l_extendedprice, minus(1, test.lineitem.l_discount))), firstrow(test.orders.o_orderdate), firstrow(test.orders.o_shippriority), firstrow(test.lineitem.l_orderkey)", "children": [5], "id": 7, "cost": 0, "selected": false }, { "type": "Projection", "property": "", "info": "test.lineitem.l_orderkey, Column#35, test.orders.o_orderdate, test.orders.o_shippriority", "children": [7], "id": 8, "cost": 0, "selected": false }, { "type": "Sort", "property": "", "info": "Column#35:desc, test.orders.o_orderdate", "children": [8], "id": 9, "cost": 0, "selected": false }, { "type": "Limit", "property": "", "info": "offset:0, count:10", "children": [9], "id": 10, "cost": 0, "selected": false } ], "steps": [ { "action": "Limit_10 is converted into TopN_11", "reason": "", "type": "TopN", "id": 11, "index": 0 }, { "action": "Sort_9 passes ByItems[Column#35 true,test.orders.o_orderdate] to TopN_11", "reason": "TopN_11 is Limit originally", "type": "Sort", "id": 9, "index": 1 }, { "action": "TopN_11 is added as Aggregation_7's parent", "reason": "TopN is pushed down", "type": "TopN", "id": 11, "index": 2 } ], "index": 15 }, { "name": "join_reorder", "before": [ { "type": "DataSource", "property": "", "info": "table:customer", "children": [], "id": 1, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:orders", "children": [], "id": 2, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join, equal:[eq(test.customer.c_custkey, test.orders.o_custkey)]", "children": [1, 2], "id": 3, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:lineitem", "children": [], "id": 4, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join, equal:[eq(test.orders.o_orderkey, test.lineitem.l_orderkey)]", "children": [3, 4], "id": 5, "cost": 0, "selected": false }, { "type": "Aggregation", "property": "", "info": "group by:test.lineitem.l_orderkey, test.orders.o_orderdate, test.orders.o_shippriority, funcs:sum(mul(test.lineitem.l_extendedprice, minus(1, test.lineitem.l_discount))), firstrow(test.orders.o_orderdate), firstrow(test.orders.o_shippriority), firstrow(test.lineitem.l_orderkey)", "children": [5], "id": 7, "cost": 0, "selected": false }, { "type": "TopN", "property": "", "info": "Column#35:desc, test.orders.o_orderdate, offset:0, count:10", "children": [7], "id": 11, "cost": 0, "selected": false }, { "type": "Projection", "property": "", "info": "test.lineitem.l_orderkey, Column#35, test.orders.o_orderdate, test.orders.o_shippriority", "children": [11], "id": 8, "cost": 0, "selected": false } ], "steps": [ { "action": "join order becomes ((customer*orders)*lineitem) from original ((customer*orders)*lineitem)", "reason": "join cost during reorder: [[((customer*orders)*lineitem), cost:1.2551220900400399e+08],[(customer*orders), cost:2.504670299506237e+07],[customer, cost:7500],[lineitem, cost:1.00001937e+08],[orders, cost:2.4925e+07]]", "type": "Projection", "id": 8, "index": 0 } ], "index": 17 }, { "name": "column_prune", "before": [ { "type": "DataSource", "property": "", "info": "table:customer", "children": [], "id": 1, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:orders", "children": [], "id": 2, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join, equal:[eq(test.customer.c_custkey, test.orders.o_custkey)]", "children": [1, 2], "id": 12, "cost": 0, "selected": false }, { "type": "DataSource", "property": "", "info": "table:lineitem", "children": [], "id": 4, "cost": 0, "selected": false }, { "type": "Join", "property": "", "info": "inner join, equal:[eq(test.orders.o_orderkey, test.lineitem.l_orderkey)]", "children": [12, 4], "id": 13, "cost": 0, "selected": false }, { "type": "Aggregation", "property": "", "info": "group by:test.lineitem.l_orderkey, test.orders.o_orderdate, test.orders.o_shippriority, funcs:sum(mul(test.lineitem.l_extendedprice, minus(1, test.lineitem.l_discount))), firstrow(test.orders.o_orderdate), firstrow(test.orders.o_shippriority), firstrow(test.lineitem.l_orderkey)", "children": [13], "id": 7, "cost": 0, "selected": false }, { "type": "TopN", "property": "", "info": "Column#35:desc, test.orders.o_orderdate, offset:0, count:10", "children": [7], "id": 11, "cost": 0, "selected": false }, { "type": "Projection", "property": "", "info": "test.lineitem.l_orderkey, Column#35, test.orders.o_orderdate, test.orders.o_shippriority", "children": [11], "id": 8, "cost": 0, "selected": false } ], "steps": [ { "action": "Join_12's columns[test.orders.o_custkey,test.customer.c_mktsegment,test.customer.c_custkey] have been pruned", "reason": "", "type": "Join", "id": 12, "index": 0 }, { "action": "Join_13's columns[test.lineitem.l_shipdate,test.orders.o_orderkey] have been pruned", "reason": "", "type": "Join", "id": 13, "index": 1 } ], "index": 18 } ] }, "physical": { "costs": { "HashAgg_22": { "params": { "IndexJoin_28": { "name": "IndexJoin_28", "cost": 14953946825.125187, "params": null, "desc": "" }, "aggCost": { "name": "aggCost", "cost": 97967583.88966256, "params": { "aggTotalCost": { "name": "aggTotalCost", "cost": 293902751.6689877, "params": { "aggCost": { "name": "aggCost", "cost": 55628281.07299452, "params": { "aggNum": 4, "cpuFactor": 30, "rows": 463569.008941621 }, "desc": "rows*aggNum*cpuFactor" }, "groupCost": { "name": "groupCost", "cost": 41721210.80474589, "params": { "cpuFactor": 30, "groupItemNum": 3, "rows": 463569.008941621 }, "desc": "rows*groupItemNum*cpuFactor" }, "hashBuildCost": { "name": "hashBuildCost", "cost": 113110838.18175551, "params": { "hashBuildCost": { "name": "hashBuildCost", "cost": 41721210.80474589, "params": { "buildRows": 463569.008941621, "cpuFactor": 30, "keyLengths": 3 }, "desc": "buildRows*keyLengths*cpuFactor" }, "hashKeyCost": { "name": "hashKeyCost", "cost": 41721210.80474589, "params": { "buildRows": 463569.008941621, "cpuFactor": 30, "keyLengths": 3 }, "desc": "buildRows*keyLengths*cpuFactor" }, "hashMemCost": { "name": "hashMemCost", "cost": 29668416.572263744, "params": { "buildRowSize": 64, "buildRows": 463569.008941621, "memFactor": 1 }, "desc": "buildRows*buildRowSize*memFactor" } }, "desc": "(hashKeyCost) + (hashMemCost) + (hashBuildCost)" }, "hashProbeCost": { "name": "hashProbeCost", "cost": 83442421.60949178, "params": { "hashKeyCost": { "name": "hashKeyCost", "cost": 41721210.80474589, "params": { "cpuFactor": 30, "keyLength": 3, "probeRows": 463569.008941621 }, "desc": "probeRows*keyLength*cpuFactor" }, "hashProbeCost": { "name": "hashProbeCost", "cost": 41721210.80474589, "params": { "cpuFactor": 30, "keyLength": 3, "probeRows": 463569.008941621 }, "desc": "probeRows*keyLength*cpuFactor" } }, "desc": "(hashKeyCost) + (hashProbeCost)" } }, "desc": "(aggCost) + (groupCost) + (hashBuildCost) + (hashProbeCost)" }, "concurrencyFactor": 3 }, "desc": "(aggTotalCost)/concurrencyFactor" } }, "type": "HashAgg", "desc": "(IndexJoin_28) + (aggCost)", "id": 22, "cost": 15051914409.014849 }, "HashJoin_38": { "params": { "HashJoin_70": { "name": "HashJoin_70", "cost": 5274198010.608428, "params": null, "desc": "" }, "TableReader_79": { "name": "TableReader_79", "cost": 22387827472.21748, "params": null, "desc": "" }, "buildFilterCost": { "name": "buildFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 100001937 }, "desc": "rows*filters*cpuFactor" }, "buildHashCost": { "name": "buildHashCost", "cost": 18800364156, "params": { "hashBuildCost": { "name": "hashBuildCost", "cost": 3000058110, "params": { "buildRows": 100001937, "cpuFactor": 30, "keyLengths": 1 }, "desc": "buildRows*keyLengths*cpuFactor" }, "hashKeyCost": { "name": "hashKeyCost", "cost": 3000058110, "params": { "buildRows": 100001937, "cpuFactor": 30, "keyLengths": 1 }, "desc": "buildRows*keyLengths*cpuFactor" }, "hashMemCost": { "name": "hashMemCost", "cost": 12800247936, "params": { "buildRowSize": 128, "buildRows": 100001937, "memFactor": 1 }, "desc": "buildRows*buildRowSize*memFactor" } }, "desc": "(hashKeyCost) + (hashMemCost) + (hashBuildCost)" }, "probeCost": { "name": "probeCost", "cost": 1370435.9407484406, "params": { "hashJoinConcurrency": 5, "probeTotalCost": { "name": "probeTotalCost", "cost": 6852179.703742203, "params": { "probeFilterCost": { "name": "probeFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 114202.99506237006 }, "desc": "rows*filters*cpuFactor" }, "probeHashCost": { "name": "probeHashCost", "cost": 6852179.703742203, "params": { "hashKeyCost": { "name": "hashKeyCost", "cost": 3426089.8518711017, "params": { "cpuFactor": 30, "keyLength": 1, "probeRows": 114202.99506237006 }, "desc": "probeRows*keyLength*cpuFactor" }, "hashProbeCost": { "name": "hashProbeCost", "cost": 3426089.8518711017, "params": { "cpuFactor": 30, "keyLength": 1, "probeRows": 114202.99506237006 }, "desc": "probeRows*keyLength*cpuFactor" } }, "desc": "(hashKeyCost) + (hashProbeCost)" } }, "desc": "(probeFilterCost) + (probeHashCost)" } }, "desc": "(probeTotalCost)/hashJoinConcurrency" } }, "type": "HashJoin", "desc": "(TableReader_79) + (HashJoin_70) + (buildHashCost) + (buildFilterCost) + (probeCost)", "id": 38, "cost": 46463760074.76666 }, "HashJoin_39": { "params": { "HashJoin_70": { "name": "HashJoin_70", "cost": 5274198010.608428, "params": null, "desc": "" }, "TableReader_79": { "name": "TableReader_79", "cost": 22387827472.21748, "params": null, "desc": "" }, "buildFilterCost": { "name": "buildFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 114202.99506237006 }, "desc": "rows*filters*cpuFactor" }, "buildHashCost": { "name": "buildHashCost", "cost": 9593051.585239084, "params": { "hashBuildCost": { "name": "hashBuildCost", "cost": 3426089.8518711017, "params": { "buildRows": 114202.99506237006, "cpuFactor": 30, "keyLengths": 1 }, "desc": "buildRows*keyLengths*cpuFactor" }, "hashKeyCost": { "name": "hashKeyCost", "cost": 3426089.8518711017, "params": { "buildRows": 114202.99506237006, "cpuFactor": 30, "keyLengths": 1 }, "desc": "buildRows*keyLengths*cpuFactor" }, "hashMemCost": { "name": "hashMemCost", "cost": 2740871.881496881, "params": { "buildRowSize": 24, "buildRows": 114202.99506237006, "memFactor": 1 }, "desc": "buildRows*buildRowSize*memFactor" } }, "desc": "(hashKeyCost) + (hashMemCost) + (hashBuildCost)" }, "probeCost": { "name": "probeCost", "cost": 1200023244, "params": { "hashJoinConcurrency": 5, "probeTotalCost": { "name": "probeTotalCost", "cost": 6000116220, "params": { "probeFilterCost": { "name": "probeFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 100001937 }, "desc": "rows*filters*cpuFactor" }, "probeHashCost": { "name": "probeHashCost", "cost": 6000116220, "params": { "hashKeyCost": { "name": "hashKeyCost", "cost": 3000058110, "params": { "cpuFactor": 30, "keyLength": 1, "probeRows": 100001937 }, "desc": "probeRows*keyLength*cpuFactor" }, "hashProbeCost": { "name": "hashProbeCost", "cost": 3000058110, "params": { "cpuFactor": 30, "keyLength": 1, "probeRows": 100001937 }, "desc": "probeRows*keyLength*cpuFactor" } }, "desc": "(hashKeyCost) + (hashProbeCost)" } }, "desc": "(probeFilterCost) + (probeHashCost)" } }, "desc": "(probeTotalCost)/hashJoinConcurrency" } }, "type": "HashJoin", "desc": "(HashJoin_70) + (TableReader_79) + (buildHashCost) + (buildFilterCost) + (probeCost)", "id": 39, "cost": 28871641778.411148 }, "HashJoin_69": { "params": { "TableReader_73": { "name": "TableReader_73", "cost": 4568485875.962565, "params": null, "desc": "" }, "TableReader_76": { "name": "TableReader_76", "cost": 405932034.6458629, "params": null, "desc": "" }, "buildFilterCost": { "name": "buildFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 24925000 }, "desc": "rows*filters*cpuFactor" }, "buildHashCost": { "name": "buildHashCost", "cost": 3090700000, "params": { "hashBuildCost": { "name": "hashBuildCost", "cost": 747750000, "params": { "buildRows": 24925000, "cpuFactor": 30, "keyLengths": 1 }, "desc": "buildRows*keyLengths*cpuFactor" }, "hashKeyCost": { "name": "hashKeyCost", "cost": 747750000, "params": { "buildRows": 24925000, "cpuFactor": 30, "keyLengths": 1 }, "desc": "buildRows*keyLengths*cpuFactor" }, "hashMemCost": { "name": "hashMemCost", "cost": 1595200000, "params": { "buildRowSize": 64, "buildRows": 24925000, "memFactor": 1 }, "desc": "buildRows*buildRowSize*memFactor" } }, "desc": "(hashKeyCost) + (hashMemCost) + (hashBuildCost)" }, "probeCost": { "name": "probeCost", "cost": 90000, "params": { "hashJoinConcurrency": 5, "probeTotalCost": { "name": "probeTotalCost", "cost": 450000, "params": { "probeFilterCost": { "name": "probeFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 7500 }, "desc": "rows*filters*cpuFactor" }, "probeHashCost": { "name": "probeHashCost", "cost": 450000, "params": { "hashKeyCost": { "name": "hashKeyCost", "cost": 225000, "params": { "cpuFactor": 30, "keyLength": 1, "probeRows": 7500 }, "desc": "probeRows*keyLength*cpuFactor" }, "hashProbeCost": { "name": "hashProbeCost", "cost": 225000, "params": { "cpuFactor": 30, "keyLength": 1, "probeRows": 7500 }, "desc": "probeRows*keyLength*cpuFactor" } }, "desc": "(hashKeyCost) + (hashProbeCost)" } }, "desc": "(probeFilterCost) + (probeHashCost)" } }, "desc": "(probeTotalCost)/hashJoinConcurrency" } }, "type": "HashJoin", "desc": "(TableReader_73) + (TableReader_76) + (buildHashCost) + (buildFilterCost) + (probeCost)", "id": 69, "cost": 8065207910.608428 }, "HashJoin_70": { "params": { "TableReader_73": { "name": "TableReader_73", "cost": 4568485875.962565, "params": null, "desc": "" }, "TableReader_76": { "name": "TableReader_76", "cost": 405932034.6458629, "params": null, "desc": "" }, "buildFilterCost": { "name": "buildFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 7500 }, "desc": "rows*filters*cpuFactor" }, "buildHashCost": { "name": "buildHashCost", "cost": 680100, "params": { "hashBuildCost": { "name": "hashBuildCost", "cost": 225000, "params": { "buildRows": 7500, "cpuFactor": 30, "keyLengths": 1 }, "desc": "buildRows*keyLengths*cpuFactor" }, "hashKeyCost": { "name": "hashKeyCost", "cost": 225000, "params": { "buildRows": 7500, "cpuFactor": 30, "keyLengths": 1 }, "desc": "buildRows*keyLengths*cpuFactor" }, "hashMemCost": { "name": "hashMemCost", "cost": 230100, "params": { "buildRowSize": 30.68, "buildRows": 7500, "memFactor": 1 }, "desc": "buildRows*buildRowSize*memFactor" } }, "desc": "(hashKeyCost) + (hashMemCost) + (hashBuildCost)" }, "probeCost": { "name": "probeCost", "cost": 299100000, "params": { "hashJoinConcurrency": 5, "probeTotalCost": { "name": "probeTotalCost", "cost": 1495500000, "params": { "probeFilterCost": { "name": "probeFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 24925000 }, "desc": "rows*filters*cpuFactor" }, "probeHashCost": { "name": "probeHashCost", "cost": 1495500000, "params": { "hashKeyCost": { "name": "hashKeyCost", "cost": 747750000, "params": { "cpuFactor": 30, "keyLength": 1, "probeRows": 24925000 }, "desc": "probeRows*keyLength*cpuFactor" }, "hashProbeCost": { "name": "hashProbeCost", "cost": 747750000, "params": { "cpuFactor": 30, "keyLength": 1, "probeRows": 24925000 }, "desc": "probeRows*keyLength*cpuFactor" } }, "desc": "(hashKeyCost) + (hashProbeCost)" } }, "desc": "(probeFilterCost) + (probeHashCost)" } }, "desc": "(probeTotalCost)/hashJoinConcurrency" } }, "type": "HashJoin", "desc": "(TableReader_76) + (TableReader_73) + (buildHashCost) + (buildFilterCost) + (probeCost)", "id": 70, "cost": 5274198010.608428 }, "IndexFullScan_53": { "params": { "rowSize": 46, "rows": 300005811, "scanFactor": 100 }, "type": "IndexFullScan", "desc": "rows*log(rowSize)*scanFactor", "id": 53, "cost": 165710068423.56305 }, "IndexHashJoin_30": { "params": { "HashJoin_70": { "name": "HashJoin_70", "cost": 5274198010.608428, "params": null, "desc": "" }, "buildFilterCost": { "name": "buildFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 114202.99506237006 }, "desc": "rows*filters*cpuFactor" }, "probeCost": { "name": "probeCost", "cost": 9679748814.516758, "params": { "indexLookupJoinConcurrency": 5, "probeTotalCost": { "name": "probeTotalCost", "cost": 48398744072.583786, "params": { "probeFilterCost": { "name": "probeFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 463569.00894162105 }, "desc": "rows*filters*cpuFactor" }, "probeReadCost": { "name": "probeReadCost", "cost": 48398744072.583786, "params": { "IndexLookUp_27": { "name": "IndexLookUp_27", "cost": 12713872.533592913, "params": null, "desc": "" }, "batchRatio": 30, "buildRows": 114202.99506237006 }, "desc": "IndexLookUp_27*buildRows/batchRatio" } }, "desc": "(probeReadCost) + (probeFilterCost)" } }, "desc": "(probeTotalCost)/indexLookupJoinConcurrency" } }, "type": "IndexHashJoin", "desc": "(HashJoin_70) + (buildFilterCost) + (probeCost)", "id": 30, "cost": 14953946825.125187 }, "IndexHashJoin_48": { "params": { "TableReader_52": { "name": "TableReader_52", "cost": 4568485875.962565, "params": null, "desc": "" }, "buildFilterCost": { "name": "buildFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 24925000 }, "desc": "rows*filters*cpuFactor" }, "probeCost": { "name": "probeCost", "cost": 2104786489666.7563, "params": { "indexLookupJoinConcurrency": 5, "probeTotalCost": { "name": "probeTotalCost", "cost": 10523932448333.781, "params": { "probeFilterCost": { "name": "probeFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 24925 }, "desc": "rows*filters*cpuFactor" }, "probeReadCost": { "name": "probeReadCost", "cost": 10523932448333.781, "params": { "TableReader_42": { "name": "TableReader_42", "cost": 12666719.095286397, "params": null, "desc": "" }, "batchRatio": 30, "buildRows": 24925000 }, "desc": "TableReader_42*buildRows/batchRatio" } }, "desc": "(probeReadCost) + (probeFilterCost)" } }, "desc": "(probeTotalCost)/indexLookupJoinConcurrency" } }, "type": "IndexHashJoin", "desc": "(TableReader_52) + (buildFilterCost) + (probeCost)", "id": 48, "cost": 2109354975542.719 }, "IndexHashJoin_66": { "params": { "TableReader_73": { "name": "TableReader_73", "cost": 4568485875.962565, "params": null, "desc": "" }, "buildFilterCost": { "name": "buildFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 24925000 }, "desc": "rows*filters*cpuFactor" }, "probeCost": { "name": "probeCost", "cost": 2104786489666.7563, "params": { "indexLookupJoinConcurrency": 5, "probeTotalCost": { "name": "probeTotalCost", "cost": 10523932448333.781, "params": { "probeFilterCost": { "name": "probeFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 24925 }, "desc": "rows*filters*cpuFactor" }, "probeReadCost": { "name": "probeReadCost", "cost": 10523932448333.781, "params": { "TableReader_60": { "name": "TableReader_60", "cost": 12666719.095286397, "params": null, "desc": "" }, "batchRatio": 30, "buildRows": 24925000 }, "desc": "TableReader_60*buildRows/batchRatio" } }, "desc": "(probeReadCost) + (probeFilterCost)" } }, "desc": "(probeTotalCost)/indexLookupJoinConcurrency" } }, "type": "IndexHashJoin", "desc": "(TableReader_73) + (buildFilterCost) + (probeCost)", "id": 66, "cost": 2109354975542.719 }, "IndexJoin_28": { "params": { "HashJoin_70": { "name": "HashJoin_70", "cost": 5274198010.608428, "params": null, "desc": "" }, "buildFilterCost": { "name": "buildFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 114202.99506237006 }, "desc": "rows*filters*cpuFactor" }, "probeCost": { "name": "probeCost", "cost": 9679748814.516758, "params": { "indexLookupJoinConcurrency": 5, "probeTotalCost": { "name": "probeTotalCost", "cost": 48398744072.583786, "params": { "probeFilterCost": { "name": "probeFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 463569.00894162105 }, "desc": "rows*filters*cpuFactor" }, "probeReadCost": { "name": "probeReadCost", "cost": 48398744072.583786, "params": { "IndexLookUp_27": { "name": "IndexLookUp_27", "cost": 12713872.533592913, "params": null, "desc": "(readIndexCost) + (readTableCost)" }, "batchRatio": 30, "buildRows": 114202.99506237006 }, "desc": "IndexLookUp_27*buildRows/batchRatio" } }, "desc": "(probeReadCost) + (probeFilterCost)" } }, "desc": "(probeTotalCost)/indexLookupJoinConcurrency" } }, "type": "IndexJoin", "desc": "(HashJoin_70) + (buildFilterCost) + (probeCost)", "id": 28, "cost": 14953946825.125187 }, "IndexJoin_46": { "params": { "TableReader_52": { "name": "TableReader_52", "cost": 4568485875.962565, "params": null, "desc": "" }, "buildFilterCost": { "name": "buildFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 24925000 }, "desc": "rows*filters*cpuFactor" }, "probeCost": { "name": "probeCost", "cost": 2104786489666.7563, "params": { "indexLookupJoinConcurrency": 5, "probeTotalCost": { "name": "probeTotalCost", "cost": 10523932448333.781, "params": { "probeFilterCost": { "name": "probeFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 24925 }, "desc": "rows*filters*cpuFactor" }, "probeReadCost": { "name": "probeReadCost", "cost": 10523932448333.781, "params": { "TableReader_42": { "name": "TableReader_42", "cost": 12666719.095286397, "params": null, "desc": "(Selection_41+netCost+seekCost)/distSqlScanConcurrency" }, "batchRatio": 30, "buildRows": 24925000 }, "desc": "TableReader_42*buildRows/batchRatio" } }, "desc": "(probeReadCost) + (probeFilterCost)" } }, "desc": "(probeTotalCost)/indexLookupJoinConcurrency" } }, "type": "IndexJoin", "desc": "(TableReader_52) + (buildFilterCost) + (probeCost)", "id": 46, "cost": 2109354975542.719 }, "IndexJoin_64": { "params": { "TableReader_73": { "name": "TableReader_73", "cost": 4568485875.962565, "params": null, "desc": "" }, "buildFilterCost": { "name": "buildFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 24925000 }, "desc": "rows*filters*cpuFactor" }, "probeCost": { "name": "probeCost", "cost": 2104786489666.7563, "params": { "indexLookupJoinConcurrency": 5, "probeTotalCost": { "name": "probeTotalCost", "cost": 10523932448333.781, "params": { "probeFilterCost": { "name": "probeFilterCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 24925 }, "desc": "rows*filters*cpuFactor" }, "probeReadCost": { "name": "probeReadCost", "cost": 10523932448333.781, "params": { "TableReader_60": { "name": "TableReader_60", "cost": 12666719.095286397, "params": null, "desc": "(Selection_59+netCost+seekCost)/distSqlScanConcurrency" }, "batchRatio": 30, "buildRows": 24925000 }, "desc": "TableReader_60*buildRows/batchRatio" } }, "desc": "(probeReadCost) + (probeFilterCost)" } }, "desc": "(probeTotalCost)/indexLookupJoinConcurrency" } }, "type": "IndexJoin", "desc": "(TableReader_73) + (buildFilterCost) + (probeCost)", "id": 64, "cost": 2109354975542.719 }, "IndexLookUp_27": { "params": { "readIndexCost": { "name": "readIndexCost", "cost": 12667273.39536189, "params": { "distSqlScanConcurrency": 15, "readIndexTotalCost": { "name": "readIndexTotalCost", "cost": 190009100.93042833, "params": { "IndexRangeScan_24": { "name": "IndexRangeScan_24", "cost": 6726.317835356039, "params": null, "desc": "rows*log(rowSize)*scanFactor" }, "indexNetCost": { "name": "indexNetCost", "cost": 2374.612592977475, "params": { "networkFactor": 8, "rowSize": 24.375, "rows": 12.177500476807563 }, "desc": "rows*rowSize*networkFactor" }, "indexSeekCost": { "name": "indexSeekCost", "cost": 190000000, "params": { "seekFactor": 9500000, "taskNums": 20 }, "desc": "taskNums*seekFactor" } }, "desc": "(indexNetCost) + (indexSeekCost) + (IndexRangeScan_24)" } }, "desc": "(readIndexTotalCost)/distSqlScanConcurrency" }, "readTableCost": { "name": "readTableCost", "cost": 46599.13823102407, "params": { "indexLookupConcurrency": 5, "readTableTotalCost": { "name": "readTableTotalCost", "cost": 232995.69115512038, "params": { "doubleReadCost": { "name": "doubleReadCost", "cost": 231737.83407364791, "params": { "double-read-cpu": { "name": "double-read-cpu", "cost": 365.3250143042269, "params": { "cpuFactor": 30, "indexRows": 12.177500476807563 }, "desc": "indexRows*cpuFactor" }, "doubleReadSeekCost": { "name": "doubleReadSeekCost", "cost": 231372.5090593437, "params": { "seekFactor": 9500000, "taskNums": 0.024355000953615126 }, "desc": "taskNums*seekFactor" } }, "desc": "(double-read-cpu) + (doubleReadSeekCost)" }, "readTableCost": { "name": "readTableCost", "cost": 1257.857081472455, "params": { "distSqlScanConcurrency": 15, "readTableTotalCost": { "name": "readTableTotalCost", "cost": 18867.856222086826, "params": { "Selection_26": { "name": "Selection_26", "cost": 9466.825853991388, "params": null, "desc": "(filterCost) + (TableRowIDScan_25)" }, "tableNetCost": { "name": "tableNetCost", "cost": 9401.030368095438, "params": { "networkFactor": 8, "rowSize": 96.5, "rows": 12.177500476807563 }, "desc": "rows*rowSize*networkFactor" }, "tableSeekCost": { "name": "tableSeekCost", "cost": 0, "params": { "seekFactor": 9500000, "taskNums": 0 }, "desc": "taskNums*seekFactor" } }, "desc": "(tableNetCost) + (tableSeekCost) + (Selection_26)" } }, "desc": "(readTableTotalCost)/distSqlScanConcurrency" } }, "desc": "(readTableCost) + (doubleReadCost)" } }, "desc": "(readTableTotalCost)/indexLookupConcurrency" } }, "type": "IndexLookUp", "desc": "(readIndexCost) + (readTableCost)", "id": 27, "cost": 12713872.533592913 }, "IndexLookUp_56": { "params": { "readIndexCost": { "name": "readIndexCost", "cost": 14960080104.57087, "params": { "distSqlScanConcurrency": 15, "readIndexTotalCost": { "name": "readIndexTotalCost", "cost": 224401201568.56305, "params": { "IndexFullScan_53": { "name": "IndexFullScan_53", "cost": 165710068423.56305, "params": null, "desc": "rows*log(rowSize)*scanFactor" }, "indexNetCost": { "name": "indexNetCost", "cost": 58501133145, "params": { "networkFactor": 8, "rowSize": 24.375, "rows": 300005811 }, "desc": "rows*rowSize*networkFactor" }, "indexSeekCost": { "name": "indexSeekCost", "cost": 190000000, "params": { "seekFactor": 9500000, "taskNums": 20 }, "desc": "taskNums*seekFactor" } }, "desc": "(indexNetCost) + (indexSeekCost) + (IndexFullScan_53)" } }, "desc": "(readIndexTotalCost)/distSqlScanConcurrency" }, "readTableCost": { "name": "readTableCost", "cost": 1148279853898.03, "params": { "indexLookupConcurrency": 5, "readTableTotalCost": { "name": "readTableTotalCost", "cost": 5741399269490.15, "params": { "doubleReadCost": { "name": "doubleReadCost", "cost": 5709110583330, "params": { "double-read-cpu": { "name": "double-read-cpu", "cost": 9000174330, "params": { "cpuFactor": 30, "indexRows": 300005811 }, "desc": "indexRows*cpuFactor" }, "doubleReadSeekCost": { "name": "doubleReadSeekCost", "cost": 5700110409000, "params": { "seekFactor": 9500000, "taskNums": 600011.622 }, "desc": "taskNums*seekFactor" } }, "desc": "(double-read-cpu) + (doubleReadSeekCost)" }, "readTableCost": { "name": "readTableCost", "cost": 32288686160.150814, "params": { "distSqlScanConcurrency": 15, "readTableTotalCost": { "name": "readTableTotalCost", "cost": 484330292402.2622, "params": { "Selection_55": { "name": "Selection_55", "cost": 233225428595.2622, "params": null, "desc": "(filterCost) + (TableRowIDScan_54)" }, "tableNetCost": { "name": "tableNetCost", "cost": 251104863807, "params": { "networkFactor": 8, "rowSize": 104.625, "rows": 300005811 }, "desc": "rows*rowSize*networkFactor" }, "tableSeekCost": { "name": "tableSeekCost", "cost": 0, "params": { "seekFactor": 9500000, "taskNums": 0 }, "desc": "taskNums*seekFactor" } }, "desc": "(tableNetCost) + (tableSeekCost) + (Selection_55)" } }, "desc": "(readTableTotalCost)/distSqlScanConcurrency" } }, "desc": "(readTableCost) + (doubleReadCost)" } }, "desc": "(readTableTotalCost)/indexLookupConcurrency" } }, "type": "IndexLookUp", "desc": "(readIndexCost) + (readTableCost)", "id": 56, "cost": 1163239934002.6008 }, "IndexRangeScan_24": { "params": { "rowSize": 46, "rows": 12.177500476807563, "scanFactor": 100 }, "type": "IndexRangeScan", "desc": "rows*log(rowSize)*scanFactor", "id": 24, "cost": 6726.317835356039 }, "MergeJoin_23": { "params": { "IndexJoin_46": { "name": "IndexJoin_46", "cost": 2109354975542.719, "params": null, "desc": "" }, "Projection_57": { "name": "Projection_57", "cost": 1165639980490.6008, "params": null, "desc": "" }, "filterCost": { "name": "filterCost", "cost": 0, "params": { "filterLeftRowsCost": { "name": "filterLeftRowsCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 114202.99506237006 }, "desc": "rows*filters*cpuFactor" }, "filterRightRowsCost": { "name": "filterRightRowsCost", "cost": 0, "params": { "cpuFactor": 30, "filters": 0, "rows": 100001937 }, "desc": "rows*filters*cpuFactor" } }, "desc": "(filterLeftRowsCost) + (filterRightRowsCost)" }, "groupRowsCost": { "name": "groupRowsCost", "cost": 3003484199.851871, "params": { "groupLeftRowsCost": { "name": "groupLeftRowsCost", "cost": 3426089.8518711017, "params": { "cpuFactor": 30, "groupItemNum": 1, "rows": 114202.99506237006 }, "desc": "rows*groupItemNum*cpuFactor" }, "groupRightRowsCost": { "name": "groupRightRowsCost", "cost": 3000058110, "params": { "cpuFactor": 30, "groupItemNum": 1, "rows": 100001937 }, "desc": "rows*groupItemNum*cpuFactor" } }, "desc": "(groupLeftRowsCost) + (groupRightRowsCost)" } }, "type": "MergeJoin", "desc": "(IndexJoin_46) + (Projection_57) + (filterCost) + (groupRowsCost)", "id": 23, "cost": 3277998440233.172 }, "Selection_26": { "params": { "TableRowIDScan_25": { "name": "TableRowIDScan_25", "cost": 9101.50083968716, "params": null, "desc": "rows*log(rowSize)*scanFactor" }, "filterCost": { "name": "filterCost", "cost": 365.3250143042269, "params": { "cpuFactor": 30, "filters": 1, "rows": 12.177500476807563 }, "desc": "rows*filters*cpuFactor" } }, "type": "Selection", "desc": "(filterCost) + (TableRowIDScan_25)", "id": 26, "cost": 9466.825853991388 }, "Selection_41": { "params": { "TableRangeScan_40": { "name": "TableRangeScan_40", "cost": 756.2852959583925, "params": null, "desc": "rows*log(rowSize)*scanFactor" }, "filterCost": { "name": "filterCost", "cost": 30, "params": { "cpuFactor": 30, "filters": 1, "rows": 1 }, "desc": "rows*filters*cpuFactor" } }, "type": "Selection", "desc": "(filterCost) + (TableRangeScan_40)", "id": 41, "cost": 786.2852959583925 }, "Selection_51": { "params": { "TableFullScan_50": { "name": "TableFullScan_50", "cost": 53325688139.43848, "params": null, "desc": "rows*log(rowSize)*scanFactor" }, "filterCost": { "name": "filterCost", "cost": 2250000000, "params": { "cpuFactor": 30, "filters": 1, "rows": 75000000 }, "desc": "rows*filters*cpuFactor" } }, "type": "Selection", "desc": "(filterCost) + (TableFullScan_50)", "id": 51, "cost": 55575688139.43848 }, "Selection_55": { "params": { "TableRowIDScan_54": { "name": "TableRowIDScan_54", "cost": 224225254265.2622, "params": null, "desc": "rows*log(rowSize)*scanFactor" }, "filterCost": { "name": "filterCost", "cost": 9000174330, "params": { "cpuFactor": 30, "filters": 1, "rows": 300005811 }, "desc": "rows*filters*cpuFactor" } }, "type": "Selection", "desc": "(filterCost) + (TableRowIDScan_54)", "id": 55, "cost": 233225428595.2622 }, "Selection_59": { "params": { "TableRangeScan_58": { "name": "TableRangeScan_58", "cost": 756.2852959583925, "params": null, "desc": "rows*log(rowSize)*scanFactor" }, "filterCost": { "name": "filterCost", "cost": 30, "params": { "cpuFactor": 30, "filters": 1, "rows": 1 }, "desc": "rows*filters*cpuFactor" } }, "type": "Selection", "desc": "(filterCost) + (TableRangeScan_58)", "id": 59, "cost": 786.2852959583925 }, "Selection_72": { "params": { "TableFullScan_71": { "name": "TableFullScan_71", "cost": 53325688139.43848, "params": null, "desc": "rows*log(rowSize)*scanFactor" }, "filterCost": { "name": "filterCost", "cost": 2250000000, "params": { "cpuFactor": 30, "filters": 1, "rows": 75000000 }, "desc": "rows*filters*cpuFactor" } }, "type": "Selection", "desc": "(filterCost) + (TableFullScan_71)", "id": 72, "cost": 55575688139.43848 }, "Selection_75": { "params": { "TableFullScan_74": { "name": "TableFullScan_74", "cost": 5672139719.687943, "params": null, "desc": "rows*log(rowSize)*scanFactor" }, "filterCost": { "name": "filterCost", "cost": 225000000, "params": { "cpuFactor": 30, "filters": 1, "rows": 7500000 }, "desc": "rows*filters*cpuFactor" } }, "type": "Selection", "desc": "(filterCost) + (TableFullScan_74)", "id": 75, "cost": 5897139719.687943 }, "Selection_78": { "params": { "TableFullScan_77": { "name": "TableFullScan_77", "cost": 224225254265.2622, "params": null, "desc": "rows*log(rowSize)*scanFactor" }, "filterCost": { "name": "filterCost", "cost": 9000174330, "params": { "cpuFactor": 30, "filters": 1, "rows": 300005811 }, "desc": "rows*filters*cpuFactor" } }, "type": "Selection", "desc": "(filterCost) + (TableFullScan_77)", "id": 78, "cost": 233225428595.2622 }, "TableFullScan_50": { "params": { "rowSize": 138.15, "rows": 75000000, "scanFactor": 100 }, "type": "TableFullScan", "desc": "rows*log(rowSize)*scanFactor", "id": 50, "cost": 53325688139.43848 }, "TableFullScan_71": { "params": { "rowSize": 138.15, "rows": 75000000, "scanFactor": 100 }, "type": "TableFullScan", "desc": "rows*log(rowSize)*scanFactor", "id": 71, "cost": 53325688139.43848 }, "TableFullScan_74": { "params": { "rowSize": 189.07999999999998, "rows": 7500000, "scanFactor": 100 }, "type": "TableFullScan", "desc": "rows*log(rowSize)*scanFactor", "id": 74, "cost": 5672139719.687943 }, "TableFullScan_77": { "params": { "rowSize": 177.79000000000002, "rows": 300005811, "scanFactor": 100 }, "type": "TableFullScan", "desc": "rows*log(rowSize)*scanFactor", "id": 77, "cost": 224225254265.2622 }, "TableRangeScan_40": { "params": { "rowSize": 189.07999999999998, "rows": 1, "scanFactor": 100 }, "type": "TableRangeScan", "desc": "rows*log(rowSize)*scanFactor", "id": 40, "cost": 756.2852959583925 }, "TableRangeScan_58": { "params": { "rowSize": 189.07999999999998, "rows": 1, "scanFactor": 100 }, "type": "TableRangeScan", "desc": "rows*log(rowSize)*scanFactor", "id": 58, "cost": 756.2852959583925 }, "TableReader_42": { "params": { "Selection_41": { "name": "Selection_41", "cost": 786.2852959583925, "params": null, "desc": "(filterCost) + (TableRangeScan_40)" }, "distSqlScanConcurrency": 15, "netCost": { "name": "netCost", "cost": 0.14400000000000002, "params": { "networkFactor": 8, "rowSize": 18, "rows": 0.001 }, "desc": "rows*rowSize*networkFactor" }, "seekCost": { "name": "seekCost", "cost": 190000000, "params": { "seekFactor": 9500000, "taskNums": 20 }, "desc": "taskNums*seekFactor" } }, "type": "TableReader", "desc": "(Selection_41+netCost+seekCost)/distSqlScanConcurrency", "id": 42, "cost": 12666719.095286397 }, "TableReader_52": { "params": { "Selection_51": { "name": "Selection_51", "cost": 55575688139.43848, "params": null, "desc": "(filterCost) + (TableFullScan_50)" }, "distSqlScanConcurrency": 15, "netCost": { "name": "netCost", "cost": 12761600000, "params": { "networkFactor": 8, "rowSize": 64, "rows": 24925000 }, "desc": "rows*rowSize*networkFactor" }, "seekCost": { "name": "seekCost", "cost": 190000000, "params": { "seekFactor": 9500000, "taskNums": 20 }, "desc": "taskNums*seekFactor" } }, "type": "TableReader", "desc": "(Selection_51+netCost+seekCost)/distSqlScanConcurrency", "id": 52, "cost": 4568485875.962565 }, "TableReader_60": { "params": { "Selection_59": { "name": "Selection_59", "cost": 786.2852959583925, "params": null, "desc": "(filterCost) + (TableRangeScan_58)" }, "distSqlScanConcurrency": 15, "netCost": { "name": "netCost", "cost": 0.14400000000000002, "params": { "networkFactor": 8, "rowSize": 18, "rows": 0.001 }, "desc": "rows*rowSize*networkFactor" }, "seekCost": { "name": "seekCost", "cost": 190000000, "params": { "seekFactor": 9500000, "taskNums": 20 }, "desc": "taskNums*seekFactor" } }, "type": "TableReader", "desc": "(Selection_59+netCost+seekCost)/distSqlScanConcurrency", "id": 60, "cost": 12666719.095286397 }, "TableReader_73": { "params": { "Selection_72": { "name": "Selection_72", "cost": 55575688139.43848, "params": null, "desc": "(filterCost) + (TableFullScan_71)" }, "distSqlScanConcurrency": 15, "netCost": { "name": "netCost", "cost": 12761600000, "params": { "networkFactor": 8, "rowSize": 64, "rows": 24925000 }, "desc": "rows*rowSize*networkFactor" }, "seekCost": { "name": "seekCost", "cost": 190000000, "params": { "seekFactor": 9500000, "taskNums": 20 }, "desc": "taskNums*seekFactor" } }, "type": "TableReader", "desc": "(Selection_72+netCost+seekCost)/distSqlScanConcurrency", "id": 73, "cost": 4568485875.962565 }, "TableReader_76": { "params": { "Selection_75": { "name": "Selection_75", "cost": 5897139719.687943, "params": null, "desc": "(filterCost) + (TableFullScan_74)" }, "distSqlScanConcurrency": 15, "netCost": { "name": "netCost", "cost": 1840800, "params": { "networkFactor": 8, "rowSize": 30.68, "rows": 7500 }, "desc": "rows*rowSize*networkFactor" }, "seekCost": { "name": "seekCost", "cost": 190000000, "params": { "seekFactor": 9500000, "taskNums": 20 }, "desc": "taskNums*seekFactor" } }, "type": "TableReader", "desc": "(Selection_75+netCost+seekCost)/distSqlScanConcurrency", "id": 76, "cost": 405932034.6458629 }, "TableReader_79": { "params": { "Selection_78": { "name": "Selection_78", "cost": 233225428595.2622, "params": null, "desc": "(filterCost) + (TableFullScan_77)" }, "distSqlScanConcurrency": 15, "netCost": { "name": "netCost", "cost": 102401983488, "params": { "networkFactor": 8, "rowSize": 128, "rows": 100001937 }, "desc": "rows*rowSize*networkFactor" }, "seekCost": { "name": "seekCost", "cost": 190000000, "params": { "seekFactor": 9500000, "taskNums": 20 }, "desc": "taskNums*seekFactor" } }, "type": "TableReader", "desc": "(Selection_78+netCost+seekCost)/distSqlScanConcurrency", "id": 79, "cost": 22387827472.21748 }, "TableRowIDScan_25": { "params": { "rowSize": 177.79000000000002, "rows": 12.177500476807563, "scanFactor": 100 }, "type": "TableRowIDScan", "desc": "rows*log(rowSize)*scanFactor", "id": 25, "cost": 9101.50083968716 }, "TableRowIDScan_54": { "params": { "rowSize": 177.79000000000002, "rows": 300005811, "scanFactor": 100 }, "type": "TableRowIDScan", "desc": "rows*log(rowSize)*scanFactor", "id": 54, "cost": 224225254265.2622 }, "TopN_17": { "params": { "HashAgg_22": { "name": "HashAgg_22", "cost": 15051914409.014849, "params": null, "desc": "" }, "topNCPUCost": { "name": "topNCPUCost", "cost": 92396574.8833357, "params": { "ByItemNums": 2, "cpuFactor": 30, "log2(n)": 3.321928094887362, "rows": 463569.008941621 }, "desc": "rows*log2(n)*ByItemNums*cpuFactor" }, "topNMemCost": { "name": "topNMemCost", "cost": 640, "params": { "memFactor": 1, "n": 10, "rowSize": 64 }, "desc": "n*rowSize*memFactor" } }, "type": "TopN", "desc": "(HashAgg_22) + (topNCPUCost) + (topNMemCost)", "id": 17, "cost": 15144311623.898184 } }, "candidates": { "14": { "type": "Projection", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "test.lineitem.l_orderkey, Column#35, test.orders.o_orderdate, test.orders.o_shippriority", "children": [17], "id": 14, "cost": 0, "selected": true, "mapping": "Projection_8" }, "17": { "type": "TopN", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "Column#35:desc, test.orders.o_orderdate, offset:0, count:10", "children": [22], "id": 17, "cost": 0, "selected": true, "mapping": "TopN_11" }, "22": { "type": "HashAgg", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "group by:test.lineitem.l_orderkey, test.orders.o_orderdate, test.orders.o_shippriority, funcs:sum(mul(test.lineitem.l_extendedprice, minus(1, test.lineitem.l_discount)))->Column#35, funcs:firstrow(test.orders.o_orderdate)->test.orders.o_orderdate, funcs:firstrow(test.orders.o_shippriority)->test.orders.o_shippriority, funcs:firstrow(test.lineitem.l_orderkey)->test.lineitem.l_orderkey", "children": [28], "id": 22, "cost": 0, "selected": true, "mapping": "Aggregation_7" }, "23": { "type": "MergeJoin", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, left key:test.orders.o_orderkey, right key:test.lineitem.l_orderkey", "children": [46, 57], "id": 23, "cost": 0, "selected": false, "mapping": "Join_13" }, "24": { "type": "IndexRangeScan", "property": "", "info": "table:lineitem, index:PRIMARY(L_ORDERKEY, L_LINENUMBER), range: decided by [eq(test.lineitem.l_orderkey, test.orders.o_orderkey)], keep order:false", "children": null, "id": 24, "cost": 0, "selected": true, "mapping": "" }, "25": { "type": "TableRowIDScan", "property": "", "info": "table:lineitem, keep order:false", "children": null, "id": 25, "cost": 0, "selected": true, "mapping": "" }, "26": { "type": "Selection", "property": "", "info": "gt(test.lineitem.l_shipdate, 1995-03-13 00:00:00.000000)", "children": [25], "id": 26, "cost": 0, "selected": true, "mapping": "" }, "27": { "type": "IndexLookUp", "property": "", "info": "", "children": [24, 26], "id": 27, "cost": 0, "selected": true, "mapping": "" }, "28": { "type": "IndexJoin", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, inner:IndexLookUp_27, outer key:test.orders.o_orderkey, inner key:test.lineitem.l_orderkey, equal cond:eq(test.orders.o_orderkey, test.lineitem.l_orderkey)", "children": [70, 27], "id": 28, "cost": 0, "selected": true, "mapping": "Join_13" }, "30": { "type": "IndexHashJoin", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, inner:IndexLookUp_27, outer key:test.orders.o_orderkey, inner key:test.lineitem.l_orderkey, equal cond:eq(test.orders.o_orderkey, test.lineitem.l_orderkey)", "children": [70, 27], "id": 30, "cost": 0, "selected": false, "mapping": "Join_13" }, "38": { "type": "HashJoin", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, equal:[eq(test.orders.o_orderkey, test.lineitem.l_orderkey)]", "children": [70, 79], "id": 38, "cost": 0, "selected": false, "mapping": "Join_13" }, "39": { "type": "HashJoin", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, equal:[eq(test.orders.o_orderkey, test.lineitem.l_orderkey)]", "children": [70, 79], "id": 39, "cost": 0, "selected": false, "mapping": "Join_13" }, "40": { "type": "TableRangeScan", "property": "", "info": "table:customer, range: decided by [test.orders.o_custkey], keep order:false", "children": null, "id": 40, "cost": 0, "selected": false, "mapping": "" }, "41": { "type": "Selection", "property": "", "info": "eq(test.customer.c_mktsegment, 'AUTOMOBILE')", "children": [40], "id": 41, "cost": 0, "selected": false, "mapping": "" }, "42": { "type": "TableReader", "property": "", "info": "data:Selection_41", "children": [41], "id": 42, "cost": 0, "selected": false, "mapping": "" }, "46": { "type": "IndexJoin", "property": "Prop{cols: [{test.orders.o_orderkey false}], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, inner:TableReader_42, outer key:test.orders.o_custkey, inner key:test.customer.c_custkey, equal cond:eq(test.orders.o_custkey, test.customer.c_custkey)", "children": [42, 52], "id": 46, "cost": 0, "selected": false, "mapping": "Join_12" }, "48": { "type": "IndexHashJoin", "property": "Prop{cols: [{test.orders.o_orderkey false}], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, inner:TableReader_42, outer key:test.orders.o_custkey, inner key:test.customer.c_custkey, equal cond:eq(test.orders.o_custkey, test.customer.c_custkey)", "children": [42, 52], "id": 48, "cost": 0, "selected": false, "mapping": "Join_12" }, "50": { "type": "TableFullScan", "property": "", "info": "table:orders, keep order:true", "children": null, "id": 50, "cost": 0, "selected": false, "mapping": "" }, "51": { "type": "Selection", "property": "", "info": "lt(test.orders.o_orderdate, 1995-03-13 00:00:00.000000)", "children": [50], "id": 51, "cost": 0, "selected": false, "mapping": "" }, "52": { "type": "TableReader", "property": "Prop{cols: [{test.orders.o_orderkey false}], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "data:Selection_51", "children": [51], "id": 52, "cost": 0, "selected": false, "mapping": "DataSource_2" }, "53": { "type": "IndexFullScan", "property": "", "info": "table:lineitem, index:PRIMARY(L_ORDERKEY, L_LINENUMBER), keep order:true", "children": null, "id": 53, "cost": 0, "selected": false, "mapping": "" }, "54": { "type": "TableRowIDScan", "property": "", "info": "table:lineitem, keep order:false", "children": null, "id": 54, "cost": 0, "selected": false, "mapping": "" }, "55": { "type": "Selection", "property": "", "info": "gt(test.lineitem.l_shipdate, 1995-03-13 00:00:00.000000)", "children": [54], "id": 55, "cost": 0, "selected": false, "mapping": "" }, "56": { "type": "IndexLookUp", "property": "", "info": "", "children": [53, 55], "id": 56, "cost": 0, "selected": false, "mapping": "" }, "57": { "type": "Projection", "property": "Prop{cols: [{test.lineitem.l_orderkey false}], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "test.lineitem.l_orderkey, test.lineitem.l_extendedprice, test.lineitem.l_discount, test.lineitem.l_shipdate", "children": [56], "id": 57, "cost": 0, "selected": false, "mapping": "DataSource_4" }, "58": { "type": "TableRangeScan", "property": "", "info": "table:customer, range: decided by [test.orders.o_custkey], keep order:false", "children": null, "id": 58, "cost": 0, "selected": false, "mapping": "" }, "59": { "type": "Selection", "property": "", "info": "eq(test.customer.c_mktsegment, 'AUTOMOBILE')", "children": [58], "id": 59, "cost": 0, "selected": false, "mapping": "" }, "60": { "type": "TableReader", "property": "", "info": "data:Selection_59", "children": [59], "id": 60, "cost": 0, "selected": false, "mapping": "" }, "64": { "type": "IndexJoin", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, inner:TableReader_60, outer key:test.orders.o_custkey, inner key:test.customer.c_custkey, equal cond:eq(test.orders.o_custkey, test.customer.c_custkey)", "children": [60, 73], "id": 64, "cost": 0, "selected": false, "mapping": "Join_12" }, "66": { "type": "IndexHashJoin", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, inner:TableReader_60, outer key:test.orders.o_custkey, inner key:test.customer.c_custkey, equal cond:eq(test.orders.o_custkey, test.customer.c_custkey)", "children": [60, 73], "id": 66, "cost": 0, "selected": false, "mapping": "Join_12" }, "69": { "type": "HashJoin", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, equal:[eq(test.customer.c_custkey, test.orders.o_custkey)]", "children": [76, 73], "id": 69, "cost": 0, "selected": false, "mapping": "Join_12" }, "70": { "type": "HashJoin", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "inner join, equal:[eq(test.customer.c_custkey, test.orders.o_custkey)]", "children": [76, 73], "id": 70, "cost": 0, "selected": true, "mapping": "Join_12" }, "71": { "type": "TableFullScan", "property": "", "info": "table:orders, keep order:false", "children": null, "id": 71, "cost": 0, "selected": true, "mapping": "" }, "72": { "type": "Selection", "property": "", "info": "lt(test.orders.o_orderdate, 1995-03-13 00:00:00.000000)", "children": [71], "id": 72, "cost": 0, "selected": true, "mapping": "" }, "73": { "type": "TableReader", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "data:Selection_72", "children": [72], "id": 73, "cost": 0, "selected": true, "mapping": "DataSource_2" }, "74": { "type": "TableFullScan", "property": "", "info": "table:customer, keep order:false", "children": null, "id": 74, "cost": 0, "selected": true, "mapping": "" }, "75": { "type": "Selection", "property": "", "info": "eq(test.customer.c_mktsegment, 'AUTOMOBILE')", "children": [74], "id": 75, "cost": 0, "selected": true, "mapping": "" }, "76": { "type": "TableReader", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "data:Selection_75", "children": [75], "id": 76, "cost": 0, "selected": true, "mapping": "DataSource_1" }, "77": { "type": "TableFullScan", "property": "", "info": "table:lineitem, keep order:false", "children": null, "id": 77, "cost": 0, "selected": false, "mapping": "" }, "78": { "type": "Selection", "property": "", "info": "gt(test.lineitem.l_shipdate, 1995-03-13 00:00:00.000000)", "children": [77], "id": 78, "cost": 0, "selected": false, "mapping": "" }, "79": { "type": "TableReader", "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "data:Selection_78", "children": [78], "id": 79, "cost": 0, "selected": false, "mapping": "DataSource_4" } }, "final": [ { "type": "TableFullScan", "property": "", "info": "table:customer, keep order:false", "children": [], "id": 74, "cost": 0, "selected": false }, { "type": "Selection", "property": "", "info": "eq(test.customer.c_mktsegment, 'AUTOMOBILE')", "children": [74], "id": 75, "cost": 0, "selected": false }, { "type": "TableReader", "property": "", "info": "data:Selection_75", "children": [75], "id": 76, "cost": 0, "selected": false }, { "type": "TableFullScan", "property": "", "info": "table:orders, keep order:false", "children": [], "id": 71, "cost": 0, "selected": false }, { "type": "Selection", "property": "", "info": "lt(test.orders.o_orderdate, 1995-03-13 00:00:00.000000)", "children": [71], "id": 72, "cost": 0, "selected": false }, { "type": "TableReader", "property": "", "info": "data:Selection_72", "children": [72], "id": 73, "cost": 0, "selected": false }, { "type": "HashJoin", "property": "", "info": "inner join, equal:[eq(test.customer.c_custkey, test.orders.o_custkey)]", "children": [76, 73], "id": 70, "cost": 0, "selected": false }, { "type": "IndexRangeScan", "property": "", "info": "table:lineitem, index:PRIMARY(L_ORDERKEY, L_LINENUMBER), range: decided by [eq(test.lineitem.l_orderkey, test.orders.o_orderkey)], keep order:false", "children": [], "id": 24, "cost": 0, "selected": false }, { "type": "TableRowIDScan", "property": "", "info": "table:lineitem, keep order:false", "children": [], "id": 25, "cost": 0, "selected": false }, { "type": "Selection", "property": "", "info": "gt(test.lineitem.l_shipdate, 1995-03-13 00:00:00.000000)", "children": [25], "id": 26, "cost": 0, "selected": false }, { "type": "IndexLookUp", "property": "", "info": "", "children": [24, 26], "id": 27, "cost": 0, "selected": false }, { "type": "IndexJoin", "property": "", "info": "inner join, inner:IndexLookUp_27, outer key:test.orders.o_orderkey, inner key:test.lineitem.l_orderkey, equal cond:eq(test.orders.o_orderkey, test.lineitem.l_orderkey)", "children": [70, 27], "id": 28, "cost": 0, "selected": false }, { "type": "HashAgg", "property": "", "info": "group by:test.lineitem.l_orderkey, test.orders.o_orderdate, test.orders.o_shippriority, funcs:sum(mul(test.lineitem.l_extendedprice, minus(1, test.lineitem.l_discount)))->Column#35, funcs:firstrow(test.orders.o_orderdate)->test.orders.o_orderdate, funcs:firstrow(test.orders.o_shippriority)->test.orders.o_shippriority, funcs:firstrow(test.lineitem.l_orderkey)->test.lineitem.l_orderkey", "children": [28], "id": 22, "cost": 0, "selected": false }, { "type": "TopN", "property": "", "info": "Column#35:desc, test.orders.o_orderdate, offset:0, count:10", "children": [22], "id": 17, "cost": 0, "selected": false }, { "type": "Projection", "property": "", "info": "test.lineitem.l_orderkey, Column#35, test.orders.o_orderdate, test.orders.o_shippriority", "children": [17], "id": 14, "cost": 0, "selected": false } ] }, "final": [ { "type": "TableFullScan", "property": "", "info": "table:customer, keep order:false", "children": [], "id": 74, "cost": 0, "selected": false }, { "type": "Selection", "property": "", "info": "eq(test.customer.c_mktsegment, 'AUTOMOBILE')", "children": [74], "id": 75, "cost": 0, "selected": false }, { "type": "TableReader", "property": "", "info": "data:Selection_75", "children": [75], "id": 76, "cost": 0, "selected": false }, { "type": "TableFullScan", "property": "", "info": "table:orders, keep order:false", "children": [], "id": 71, "cost": 0, "selected": false }, { "type": "Selection", "property": "", "info": "lt(test.orders.o_orderdate, 1995-03-13 00:00:00.000000)", "children": [71], "id": 72, "cost": 0, "selected": false }, { "type": "TableReader", "property": "", "info": "data:Selection_72", "children": [72], "id": 73, "cost": 0, "selected": false }, { "type": "HashJoin", "property": "", "info": "inner join, equal:[eq(test.customer.c_custkey, test.orders.o_custkey)]", "children": [76, 73], "id": 70, "cost": 0, "selected": false }, { "type": "IndexRangeScan", "property": "", "info": "table:lineitem, index:PRIMARY(L_ORDERKEY, L_LINENUMBER), range: decided by [eq(test.lineitem.l_orderkey, test.orders.o_orderkey)], keep order:false", "children": [], "id": 24, "cost": 0, "selected": false }, { "type": "TableRowIDScan", "property": "", "info": "table:lineitem, keep order:false", "children": [], "id": 25, "cost": 0, "selected": false }, { "type": "Selection", "property": "", "info": "gt(test.lineitem.l_shipdate, 1995-03-13 00:00:00.000000)", "children": [25], "id": 26, "cost": 0, "selected": false }, { "type": "IndexLookUp", "property": "", "info": "", "children": [24, 26], "id": 27, "cost": 0, "selected": false }, { "type": "IndexJoin", "property": "", "info": "inner join, inner:IndexLookUp_27, outer key:test.orders.o_orderkey, inner key:test.lineitem.l_orderkey, equal cond:eq(test.orders.o_orderkey, test.lineitem.l_orderkey)", "children": [70, 27], "id": 28, "cost": 0, "selected": false }, { "type": "Projection", "property": "", "info": "mul(test.lineitem.l_extendedprice, minus(1, test.lineitem.l_discount))->Column#44, test.orders.o_orderdate, test.orders.o_shippriority, test.lineitem.l_orderkey, test.lineitem.l_orderkey, test.orders.o_orderdate, test.orders.o_shippriority", "children": [28], "id": 82, "cost": 0, "selected": false }, { "type": "HashAgg", "property": "", "info": "group by:Column#48, Column#49, Column#50, funcs:sum(Column#44)->Column#35, funcs:firstrow(Column#45)->test.orders.o_orderdate, funcs:firstrow(Column#46)->test.orders.o_shippriority, funcs:firstrow(Column#47)->test.lineitem.l_orderkey", "children": [82], "id": 22, "cost": 0, "selected": false }, { "type": "TopN", "property": "", "info": "Column#35:desc, test.orders.o_orderdate, offset:0, count:10", "children": [22], "id": 17, "cost": 0, "selected": false }, { "type": "Projection", "property": "", "info": "test.lineitem.l_orderkey, Column#35, test.orders.o_orderdate, test.orders.o_shippriority", "children": [17], "id": 14, "cost": 0, "selected": false } ], "isFastPlan": false } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/examples/old-format.json ================================================ { "logical": { "final": [ { "id": 1, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t1" }, { "id": 3, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t2" }, { "id": 14, "type": "TopN", "children": [3], "cost": 0, "selected": false, "property": "", "info": "test.t2.b, offset:0, count:1" }, { "id": 5, "type": "Aggregation", "children": [14], "cost": 0, "selected": false, "property": "", "info": "funcs:min(test.t2.b)" }, { "id": 13, "type": "Selection", "children": [5], "cost": 0, "selected": false, "property": "", "info": "not(isnull(Column#7))" }, { "id": 8, "type": "Apply", "children": [1, 13], "cost": 0, "selected": false, "property": "", "info": "inner join, other cond:gt(test.t1.b, Column#7)" }, { "id": 9, "type": "Projection", "children": [8], "cost": 0, "selected": false, "property": "", "info": "test.t1.b" } ], "steps": [ { "index": 1, "before": [ { "id": 1, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t1" }, { "id": 3, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t2" }, { "id": 4, "type": "Selection", "children": [3], "cost": 0, "selected": false, "property": "", "info": "lt(test.t2.a, test.t1.a)" }, { "id": 5, "type": "Aggregation", "children": [4], "cost": 0, "selected": false, "property": "", "info": "funcs:min(test.t2.b), firstrow(test.t2.a), firstrow(test.t2.b), firstrow(test.t2._tidb_rowid)" }, { "id": 6, "type": "Projection", "children": [5], "cost": 0, "selected": false, "property": "", "info": "Column#7" }, { "id": 7, "type": "MaxOneRow", "children": [6], "cost": 0, "selected": false, "property": "", "info": "" }, { "id": 8, "type": "Apply", "children": [1, 7], "cost": 0, "selected": false, "property": "", "info": "left outer join" }, { "id": 2, "type": "Selection", "children": [8], "cost": 0, "selected": false, "property": "", "info": "gt(test.t1.b, Column#7)" }, { "id": 9, "type": "Projection", "children": [2], "cost": 0, "selected": false, "property": "", "info": "test.t1.b" } ], "name": "column_prune", "steps": [ { "action": "Aggregation_5's columns[test.t2._tidb_rowid,test.t2.b,test.t2.a] have been pruned", "reason": "", "id": 5, "type": "Aggregation", "index": 0 }, { "action": "Aggregation_5's aggregation functions[firstrow(test.t2._tidb_rowid),firstrow(test.t2.b),firstrow(test.t2.a)] have been pruned", "reason": "", "id": 5, "type": "Aggregation", "index": 1 }, { "action": "DataSource_3's columns[test.t2._tidb_rowid] have been pruned", "reason": "", "id": 3, "type": "DataSource", "index": 2 }, { "action": "DataSource_1's columns[test.t1._tidb_rowid] have been pruned", "reason": "", "id": 1, "type": "DataSource", "index": 3 } ] }, { "index": 4, "before": [ { "id": 1, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t1" }, { "id": 3, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t2" }, { "id": 4, "type": "Selection", "children": [3], "cost": 0, "selected": false, "property": "", "info": "lt(test.t2.a, test.t1.a)" }, { "id": 5, "type": "Aggregation", "children": [4], "cost": 0, "selected": false, "property": "", "info": "funcs:min(test.t2.b)" }, { "id": 6, "type": "Projection", "children": [5], "cost": 0, "selected": false, "property": "", "info": "Column#7" }, { "id": 7, "type": "MaxOneRow", "children": [6], "cost": 0, "selected": false, "property": "", "info": "" }, { "id": 8, "type": "Apply", "children": [1, 7], "cost": 0, "selected": false, "property": "", "info": "left outer join" }, { "id": 2, "type": "Selection", "children": [8], "cost": 0, "selected": false, "property": "", "info": "gt(test.t1.b, Column#7)" }, { "id": 9, "type": "Projection", "children": [2], "cost": 0, "selected": false, "property": "", "info": "test.t1.b" } ], "name": "decorrelate", "steps": [ { "action": "MaxOneRow_7 removed from plan tree", "reason": "", "id": 7, "type": "MaxOneRow", "index": 0 }, { "action": "Projection_6 is moved as Apply_8's parent", "reason": "Apply_8's join type is left outer join, not semi join", "id": 6, "type": "Projection", "index": 1 } ] }, { "index": 6, "before": [ { "id": 1, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t1" }, { "id": 3, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t2" }, { "id": 4, "type": "Selection", "children": [3], "cost": 0, "selected": false, "property": "", "info": "lt(test.t2.a, test.t1.a)" }, { "id": 5, "type": "Aggregation", "children": [4], "cost": 0, "selected": false, "property": "", "info": "funcs:min(test.t2.b)" }, { "id": 8, "type": "Apply", "children": [1, 5], "cost": 0, "selected": false, "property": "", "info": "left outer join" }, { "id": 6, "type": "Projection", "children": [8], "cost": 0, "selected": false, "property": "", "info": "test.t1.a, test.t1.b, Column#7" }, { "id": 2, "type": "Selection", "children": [6], "cost": 0, "selected": false, "property": "", "info": "gt(test.t1.b, Column#7)" }, { "id": 9, "type": "Projection", "children": [2], "cost": 0, "selected": false, "property": "", "info": "test.t1.b" } ], "name": "projection_eliminate", "steps": [ { "action": "Projection_6 is eliminated", "reason": "Projection_6's Exprs are all Columns", "id": 6, "type": "Projection", "index": 0 } ] }, { "index": 7, "before": [ { "id": 1, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t1" }, { "id": 3, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t2" }, { "id": 4, "type": "Selection", "children": [3], "cost": 0, "selected": false, "property": "", "info": "lt(test.t2.a, test.t1.a)" }, { "id": 5, "type": "Aggregation", "children": [4], "cost": 0, "selected": false, "property": "", "info": "funcs:min(test.t2.b)" }, { "id": 8, "type": "Apply", "children": [1, 5], "cost": 0, "selected": false, "property": "", "info": "left outer join" }, { "id": 2, "type": "Selection", "children": [8], "cost": 0, "selected": false, "property": "", "info": "gt(test.t1.b, Column#7)" }, { "id": 9, "type": "Projection", "children": [2], "cost": 0, "selected": false, "property": "", "info": "test.t1.b" } ], "name": "max_min_eliminate", "steps": [ { "action": "add Selection_10,add Sort_11,add Limit_12 during eliminating Aggregation_5 min function", "reason": "Aggregation_5 has only one function[min] without group by, the columns in Aggregation_5 shouldn't be NULL and needs NULL to be filtered out, the columns in Aggregation_5 should be sorted", "id": 5, "type": "Aggregation", "index": 0 } ] }, { "index": 8, "before": [ { "id": 1, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t1" }, { "id": 3, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t2" }, { "id": 4, "type": "Selection", "children": [3], "cost": 0, "selected": false, "property": "", "info": "lt(test.t2.a, test.t1.a)" }, { "id": 10, "type": "Selection", "children": [4], "cost": 0, "selected": false, "property": "", "info": "not(isnull(test.t2.b))" }, { "id": 11, "type": "Sort", "children": [10], "cost": 0, "selected": false, "property": "", "info": "test.t2.b" }, { "id": 12, "type": "Limit", "children": [11], "cost": 0, "selected": false, "property": "", "info": "offset:0, count:1" }, { "id": 5, "type": "Aggregation", "children": [12], "cost": 0, "selected": false, "property": "", "info": "funcs:min(test.t2.b)" }, { "id": 8, "type": "Apply", "children": [1, 5], "cost": 0, "selected": false, "property": "", "info": "left outer join" }, { "id": 2, "type": "Selection", "children": [8], "cost": 0, "selected": false, "property": "", "info": "gt(test.t1.b, Column#7)" }, { "id": 9, "type": "Projection", "children": [2], "cost": 0, "selected": false, "property": "", "info": "test.t1.b" } ], "name": "predicate_push_down", "steps": [ { "action": "The conditions[not(isnull(test.t1.b))] are pushed down across DataSource_1", "reason": "", "id": 1, "type": "DataSource", "index": 0 }, { "action": "The conditions[lt(test.t2.a, test.t1.a),not(isnull(test.t2.b))] are pushed down across DataSource_3", "reason": "", "id": 3, "type": "DataSource", "index": 1 }, { "action": "Selection_4 is removed", "reason": "The conditions[lt(test.t2.a, test.t1.a)] in Selection_4 are pushed down", "id": 4, "type": "Selection", "index": 2 }, { "action": "Selection_10 is removed", "reason": "The conditions[not(isnull(test.t2.b))] in Selection_10 are pushed down", "id": 10, "type": "Selection", "index": 3 }, { "action": "add Selection_13 to connect Apply_8 and Aggregation_5", "reason": "", "id": 13, "type": "Selection", "index": 4 }, { "action": "Selection_2 is removed", "reason": "The conditions[gt(test.t1.b, Column#7)] in Selection_2 are pushed down", "id": 2, "type": "Selection", "index": 5 } ] }, { "index": 13, "before": [ { "id": 1, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t1" }, { "id": 3, "type": "DataSource", "children": [], "cost": 0, "selected": false, "property": "", "info": "table:t2" }, { "id": 11, "type": "Sort", "children": [3], "cost": 0, "selected": false, "property": "", "info": "test.t2.b" }, { "id": 12, "type": "Limit", "children": [11], "cost": 0, "selected": false, "property": "", "info": "offset:0, count:1" }, { "id": 5, "type": "Aggregation", "children": [12], "cost": 0, "selected": false, "property": "", "info": "funcs:min(test.t2.b)" }, { "id": 13, "type": "Selection", "children": [5], "cost": 0, "selected": false, "property": "", "info": "not(isnull(Column#7))" }, { "id": 8, "type": "Apply", "children": [1, 13], "cost": 0, "selected": false, "property": "", "info": "inner join, other cond:gt(test.t1.b, Column#7)" }, { "id": 9, "type": "Projection", "children": [8], "cost": 0, "selected": false, "property": "", "info": "test.t1.b" } ], "name": "topn_push_down", "steps": [ { "action": "Limit_12 is converted into TopN_14", "reason": "", "id": 14, "type": "TopN", "index": 0 }, { "action": "Sort_11 passes ByItems[test.t2.b] to TopN_14", "reason": "TopN_14 is Limit originally", "id": 11, "type": "Sort", "index": 1 }, { "action": "TopN_14 is added as DataSource_3's parent", "reason": "TopN is pushed down", "id": 14, "type": "TopN", "index": 2 } ] } ] }, "physical": { "final": [ { "id": 20, "type": "TableReader", "children": [], "cost": 50823.833333333336, "selected": false, "property": "", "info": "data:Selection_19" }, { "id": 32, "type": "TableReader", "children": [], "cost": 41600.8168, "selected": false, "property": "", "info": "data:TopN_31" }, { "id": 24, "type": "TopN", "children": [32], "cost": 41603.8188, "selected": false, "property": "", "info": "test.t2.b, offset:0, count:1" }, { "id": 23, "type": "StreamAgg", "children": [24], "cost": 41606.8188, "selected": false, "property": "", "info": "funcs:min(test.t2.b)->Column#7" }, { "id": 21, "type": "Selection", "children": [23], "cost": 41609.8188, "selected": false, "property": "", "info": "not(isnull(Column#7))" }, { "id": 17, "type": "Apply", "children": [20, 21], "cost": 415756889.64533335, "selected": false, "property": "", "info": "CARTESIAN inner join, other cond:gt(test.t1.b, Column#7)" }, { "id": 15, "type": "Projection", "children": [17], "cost": 415762901.64533335, "selected": false, "property": "", "info": "test.t1.b" } ], "selected_candidates": [ { "id": 17, "type": "Apply", "children": null, "cost": 415756889.64533335, "selected": true, "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "CARTESIAN inner join, other cond:gt(test.t1.b, Column#7)", "mapping": "Apply_8" }, { "id": 20, "type": "TableReader", "children": null, "cost": 50823.833333333336, "selected": true, "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "data:Selection_19", "mapping": "DataSource_1" }, { "id": 21, "type": "Selection", "children": null, "cost": 41609.8188, "selected": true, "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "not(isnull(Column#7))", "mapping": "Selection_13" }, { "id": 23, "type": "StreamAgg", "children": null, "cost": 41606.8188, "selected": true, "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "funcs:min(test.t2.b)->Column#7", "mapping": "Aggregation_5" }, { "id": 24, "type": "TopN", "children": null, "cost": 41603.8188, "selected": true, "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "test.t2.b, offset:0, count:1", "mapping": "TopN_14" }, { "id": 15, "type": "Projection", "children": null, "cost": 415762901.64533335, "selected": true, "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "test.t1.b", "mapping": "Projection_9" } ], "discarded_candidates": [ { "id": 22, "type": "HashAgg", "children": null, "cost": 41637.4198, "selected": false, "property": "Prop{cols: [], TaskTp: rootTask, expectedCount: 1.7976931348623157e+308}", "info": "funcs:min(test.t2.b)->Column#7", "mapping": "Aggregation_5" }, { "id": 30, "type": "Selection", "children": null, "cost": 600020, "selected": false, "property": "Prop{cols: [], TaskTp: copSingleReadTask, expectedCount: 1.7976931348623157e+308}", "info": "lt(test.t2.a, test.t1.a), not(isnull(test.t2.b))", "mapping": "DataSource_3" } ] }, "final": [ { "id": 20, "type": "TableReader", "children": [], "cost": 50823.833333333336, "selected": false, "property": "", "info": "data:Selection_19" }, { "id": 32, "type": "TableReader", "children": [], "cost": 41600.8168, "selected": false, "property": "", "info": "data:TopN_31" }, { "id": 24, "type": "TopN", "children": [32], "cost": 41603.8188, "selected": false, "property": "", "info": "test.t2.b, offset:0, count:1" }, { "id": 23, "type": "StreamAgg", "children": [24], "cost": 41606.8188, "selected": false, "property": "", "info": "funcs:min(test.t2.b)->Column#7" }, { "id": 21, "type": "Selection", "children": [23], "cost": 41609.8188, "selected": false, "property": "", "info": "not(isnull(Column#7))" }, { "id": 17, "type": "Apply", "children": [20, 21], "cost": 415756889.64533335, "selected": false, "property": "", "info": "CARTESIAN inner join, other cond:gt(test.t1.b, Column#7)" }, { "id": 15, "type": "Projection", "children": [17], "cost": 415762901.64533335, "selected": false, "property": "", "info": "test.t1.b" } ], "isFastPlan": false } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/index.module.less ================================================ .container { overflow: auto; } .operator_tree { flex-shrink: 0; } .logical_optimize { display: flex; align-items: center; } .arrow { margin: 0 10px; } .physical_operator_tree_container { display: flex; align-items: center; } .physical_operator_tree_modal_container { flex-shrink: 0; width: 100%; height: 600px; } .unselected_candidates { border: dashed 1px #ccc; margin-left: 10px; padding: 5px; } .selected_candidates { border: dashed 1px white; margin-left: 10px; padding: 5px; } .selected_candidates p { color: white; } .steps { width: 200px; } .step_info { max-height: 90px; width: 200px; overflow: hidden; text-overflow: ellipsis; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/index.tsx ================================================ import React, { useCallback, useContext, useState } from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import { Button, Upload, Alert, Tooltip, Modal, Space, Dropdown, Menu } from 'antd' import { UploadOutlined, ArrowRightOutlined, DownOutlined } from '@ant-design/icons' import { ErrorBoundary } from 'react-error-boundary' import { Card, Root } from '@lib/components' import { addTranslations } from '@lib/utils/i18n' import { useLocationChange } from '@lib/hooks/useLocationChange' import LogicalOperatorTree, { LogicalOperatorNode } from './components/LogicalOperatorTree' import PhysicalOperatorTree, { PhysicalOperatorNode, PhysicalOperatorTreeWithFullScreen } from './components/PhysicalOperatorTree' import PhysicalCostTree, { PhysicalCostMap } from './components/PhysicalCostTree' import { OptimizerTraceContext } from './context' import translations from './translations' import styles from './index.module.less' import oldFormatTrace from './examples/old-format.json' import newFormatTrace from './examples/new-format.json' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> ) } export default function OptimizeTraceApp() { const ctx = useContext(OptimizerTraceContext) if (ctx === null) { throw new Error('OptimizerTraceContext must not be null') } return ( ) } interface OptimizerData { logical: { final: LogicalOperatorNode[] steps: LogicalOptimizeActionStep[] } physical: { final: LogicalOperatorNode // old format selected_candidates?: PhysicalOperatorNode[] discarded_candidates?: PhysicalOperatorNode[] // new format candidates?: { [x: string]: PhysicalOperatorNode } costs?: PhysicalCostMap } final: LogicalOperatorNode[] isFastPlan: boolean } interface LogicalOptimizeActionStep { index: number name: string before: LogicalOperatorNode[] steps: { id: number index: number action: string reason: string type: string }[] } function OptimizerTrace() { const [importedData, setImportedData] = useState(null) const [errorMsg, setErrorMsg] = useState('') const [fileName, setFileName] = useState('') const handleBeforeUpload = useCallback(async (file: File) => { setErrorMsg('') setFileName(file.name) const t = await file.text() setImportedData(JSON.parse(t)) return false }, []) function menuItemClick({ key }) { setFileName(key) if (key === 'old') { setImportedData(oldFormatTrace as any) } else if (key === 'new') { setImportedData(newFormatTrace as any) } } const menu = ( Old Format New Format ) return (
{errorMsg && ( )} { setImportedData(null) setErrorMsg(error.message) resetErrorBoundary() return null }} > {importedData && ( // reset all state after uploading a new file
)}
) } function LogicalOptimization({ data }: { data: OptimizerData }) { const logicalData = data.logical const Steps = () => ( <> {logicalData.steps.map((s) => { const Action = () => (

{s.name}

{s.steps.map((actionStep, index) => { const content = `action ${actionStep.index}: ${actionStep.action} ${actionStep.reason && `, reason: ${actionStep.reason}`}` return (

{content}

) })}
) return ( ) })} ) return (

Logical Optimization

) } function PhysicalOptimization({ data }: { data: OptimizerData }) { const [physicalNodeName, setPhysicalNodeName] = useState('') const [logicalNodeName, setLogicalNodeName] = useState('') const [showCostTreeModal, setShowCostTreeModal] = useState(false) const [fullScreenPhysicalNode, setFullScreenPhysicalNode] = useState< PhysicalOperatorNode | undefined >(undefined) const physicalData = data.physical let allCandidatesMap: { [x: string]: PhysicalOperatorNode } = {} if (physicalData.candidates) { // new format allCandidatesMap = physicalData.candidates } else { // old format const selectedCandidates = physicalData.selected_candidates || [] const discardedCandidates = physicalData.discarded_candidates || [] const allCandidates = [...selectedCandidates, ...discardedCandidates] allCandidatesMap = allCandidates.reduce((acc, c) => { acc[c.id] = c return acc }, {} as { [props: string]: PhysicalOperatorNode }) } // convert to tree Object.values(allCandidatesMap).forEach((c) => { c.childrenNodes = (c.children || []).map((i) => allCandidatesMap[i]) // fix cost c.cost = physicalData.costs?.[`${c.type}_${c.id}`]?.cost ?? c.cost }) const operatorCandidates = Object.values(allCandidatesMap).reduce( (acc, c) => { if (c.mapping === '') { return acc } if (!acc[c.mapping]) { acc[c.mapping] = [] } acc[c.mapping].push(c) return acc }, {} as { [props: string]: PhysicalOperatorNode[] } ) function updatePhysicalNodeName(name: string) { setPhysicalNodeName(name) setShowCostTreeModal(true) } const OperatorCandidates = () => { const selectedCandidates = operatorCandidates[logicalNodeName].filter( (c) => c.selected ) const unselectedCandidates = operatorCandidates[logicalNodeName].filter( (c) => !c.selected ) return (
{!!selectedCandidates.length && (

selected candidates

{selectedCandidates.map((c) => ( setFullScreenPhysicalNode(c)} /> ))}
)} {!!unselectedCandidates.length && (

unselected candidates

{unselectedCandidates.map((c) => ( setFullScreenPhysicalNode(c)} /> ))}
)}
) } return (

Physical Optimization {logicalNodeName && `for ${logicalNodeName}`}

{logicalNodeName && }
setFullScreenPhysicalNode(undefined)} footer={null} destroyOnClose={true} > setShowCostTreeModal(false)} footer={null} destroyOnClose={true} >
) } function Final({ data }: { data: OptimizerData }) { const finalData = data.final return (

Final

) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/translations/en.yaml ================================================ optimizer_trace: ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/OptimizerTrace/translations/zh.yaml ================================================ optimizer_trace: ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Overview/components/Instances.tsx ================================================ import { Link } from 'react-router-dom' import React, { useContext, useMemo } from 'react' import { Card, AnimatedSkeleton, Descriptions } from '@lib/components' import { useTranslation } from 'react-i18next' import { useClientRequest } from '@lib/utils/useClientRequest' import { Typography, Row, Col, Space } from 'antd' import { STATUS_OFFLINE, STATUS_TOMBSTONE, STATUS_UP } from '@lib/apps/ClusterInfo/status/status' import { RightOutlined, WarningOutlined } from '@ant-design/icons' import { Stack } from 'office-ui-fabric-react/lib/Stack' import styles from './Styles.module.less' import { OverviewContext } from '../context' function ComponentItem(props: { name: string resp: { data?: { status?: number }[]; isLoading: boolean; error?: any } }) { const { name, resp } = props const [upNums, allNums] = useMemo(() => { if (!resp.data) { return [0, 0] } let up = 0 let all = 0 for (const instance of resp.data) { all++ if ( instance.status === STATUS_UP || instance.status === STATUS_TOMBSTONE || instance.status === STATUS_OFFLINE ) { up++ } } return [up, all] }, [resp]) // query TiCDC and TiProxy components returns 404 under TiDB 7.6.0 const notFoundError = resp.error?.response?.status === 404 return ( {!resp.error && ( {upNums} / {allNums} )} {resp.error && !notFoundError && ( Error )} ) } export default function Nodes() { const { t } = useTranslation() const ctx = useContext(OverviewContext) const tidbResp = useClientRequest(ctx!.ds.getTiDBTopology) const storeResp = useClientRequest(ctx!.ds.getStoreTopology) const tiKVResp = { ...storeResp, data: storeResp.data?.tikv } const tiFlashResp = { ...storeResp, data: storeResp.data?.tiflash } const pdResp = useClientRequest(ctx!.ds.getPDTopology) const tiCDCResp = useClientRequest(ctx!.ds.getTiCDCTopology) const tiProxyResp = useClientRequest(ctx!.ds.getTiProxyTopology) const tsoResp = useClientRequest(ctx!.ds.getTSOTopology) const schedulingResp = useClientRequest(ctx!.ds.getSchedulingTopology) return ( {t('overview.instances.title')} } noMarginRight > ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Overview/components/Metrics.tsx ================================================ import { Space, Typography, Button, Tooltip } from 'antd' import React, { useCallback, useContext, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useMemoizedFn } from 'ahooks' import { MetricsChart, SyncChartPointer, TimeRangeValue } from 'metrics-chart' import { Link } from 'react-router-dom' import { Stack } from 'office-ui-fabric-react' import { LoadingOutlined, FileTextOutlined } from '@ant-design/icons' import { debounce } from 'lodash' import { AutoRefreshButton, Card, DEFAULT_TIME_RANGE, TimeRange, Toolbar, ErrorBar, LimitTimeRange } from '@lib/components' import { useTimeRangeValue } from '@lib/components/TimeRangeSelector/hook' import { OverviewContext } from '../context' import { telemetry } from '../utils/telemetry' export default function Metrics() { const ctx = useContext(OverviewContext) const promAddrConfigurable = ctx?.cfg.promAddrConfigurable || false const [timeRange, setTimeRange] = useState(DEFAULT_TIME_RANGE) const [chartRange, setChartRange] = useTimeRangeValue(timeRange, setTimeRange) const loadingCounter = useRef(0) const [isSomeLoading, setIsSomeLoading] = useState(false) const { t } = useTranslation() // eslint-disable-next-line const setIsSomeLoadingDebounce = useCallback( debounce(setIsSomeLoading, 100, { leading: true }), [] ) const onLoadingStateChange = useMemoizedFn((loading: boolean) => { loading ? (loadingCounter.current += 1) : loadingCounter.current > 0 && (loadingCounter.current -= 1) setIsSomeLoadingDebounce(loadingCounter.current > 0) }) const handleManualRefreshClick = () => { telemetry.clickManualRefresh() return setTimeRange((r) => ({ ...r })) } const handleOnBrush = (range: TimeRangeValue) => { setChartRange(range) } const ErrorComponent = (error: Error) => ( {promAddrConfigurable && ( {t('overview.change_prom_button')} )} ) return ( <> { setTimeRange(v) telemetry.selectTimeRange(v) }} onZoomOutClick={(start, end) => telemetry.clickZoomOut([start, end]) } /> {ctx?.cfg.metricsReferenceLink && ( telemetry.clickDocumentationIcon()} /> )} {isSomeLoading && } {ctx?.cfg.metricsQueries.map((item) => ( {t(`overview.metrics.${item.title}`)} telemetry.clickSeriesLabel(item.title, seriesName) } /> ))} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Overview/components/MonitorAlert.tsx ================================================ import React, { useContext, useEffect, useState } from 'react' import { RightOutlined, WarningOutlined } from '@ant-design/icons' import { Card, AnimatedSkeleton } from '@lib/components' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useClientRequest } from '@lib/utils/useClientRequest' import { Space, Typography } from 'antd' import { Stack } from 'office-ui-fabric-react/lib/Stack' import { OverviewContext } from '../context' export default function MonitorAlert() { const ctx = useContext(OverviewContext) const { t } = useTranslation() const [alertCounter, setAlertCounter] = useState(0) const { data: amData, isLoading: amIsLoading } = useClientRequest( ctx!.ds.getAlertManagerTopology ) const { data: grafanaData, isLoading: grafanaIsLoading } = useClientRequest( ctx!.ds.getGrafanaTopology ) useEffect(() => { if (!amData) { return } async function fetch() { let resp = await ctx!.ds.getAlertManagerCounts( `${amData!.ip}:${amData!.port}`, { handleError: 'custom' } ) setAlertCounter(resp.data) } fetch() }, [amData, ctx]) return ( {!grafanaData && ( {t('overview.monitor_alert.view_monitor_warn')} )} {grafanaData && ( {t('overview.monitor_alert.view_grafana_monitor')} )} {!amData && ( {t('overview.monitor_alert.view_alerts_warn')} )} {amData && ( 0 ? 'danger' : undefined}> {alertCounter === 0 ? t('overview.monitor_alert.view_zero_alerts') : t('overview.monitor_alert.view_alerts', { alertCount: alertCounter })} )}
{t('overview.monitor_alert.run_diagnose')}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Overview/components/Styles.module.less ================================================ .big { font-size: larger; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Overview/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { TopologyPDInfo, TopologyTiDBInfo, TopologyGrafanaInfo, TopologyAlertManagerInfo, ClusterinfoStoreTopologyResponse, MetricsQueryResponse, TopologyTiCDCInfo, TopologyTiProxyInfo, TopologyTSOInfo, TopologySchedulingInfo } from '@lib/client' import { IContextConfig, ReqConfig } from '@lib/types' import { QueryConfig, TransformNullValue } from 'metrics-chart' export interface OverviewMetricsQueryType { title: string queries: QueryConfig[] unit: string nullValue?: TransformNullValue } interface IMetricConfig { metricsQueries: OverviewMetricsQueryType[] promAddrConfigurable?: boolean timeRangeSelector?: { recent_seconds: number[] customAbsoluteRangePicker: boolean } metricsReferenceLink?: string } export interface IOverviewDataSource { getTiDBTopology(options?: ReqConfig): AxiosPromise> getStoreTopology( options?: ReqConfig ): AxiosPromise getPDTopology(options?: ReqConfig): AxiosPromise> getTiCDCTopology(options?: ReqConfig): AxiosPromise> getTiProxyTopology( options?: ReqConfig ): AxiosPromise> getTSOTopology(options?: ReqConfig): AxiosPromise> getSchedulingTopology( options?: ReqConfig ): AxiosPromise> getGrafanaTopology(options?: ReqConfig): AxiosPromise getAlertManagerTopology( options?: ReqConfig ): AxiosPromise getAlertManagerCounts( address: string, options?: ReqConfig ): AxiosPromise metricsQueryGet(params: { endTimeSec?: number query?: string startTimeSec?: number stepSec?: number }): Promise } export type IOverviewConfig = IContextConfig & IMetricConfig & { showMetrics: boolean } export interface IOverviewContext { ds: IOverviewDataSource cfg: IOverviewConfig } export const OverviewContext = createContext(null) export const OverviewProvider = OverviewContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Overview/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router } from 'react-router-dom' import { Col, Row } from 'antd' import { Root } from '@lib/components' import { addTranslations } from '@lib/utils/i18n' import translations from './translations' import { OverviewContext } from './context' import { useLocationChange } from '@lib/hooks/useLocationChange' import Instances from './components/Instances' import Metrics from './components/Metrics' import MonitorAlert from './components/MonitorAlert' addTranslations(translations) function AppRoutes() { useLocationChange() const ctx = useContext(OverviewContext) if (ctx?.cfg.showMetrics) { return ( ) } return ( ) } export default function () { const ctx = useContext(OverviewContext) if (ctx === null) { throw new Error('OverviewContext must not be null') } return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Overview/translations/en.yaml ================================================ overview: nav_title: Overview panel_no_data_tips: Documentation view_more_metrics: View More Metrics change_prom_button: Change Prometheus Address top_statements: title: Time Consuming SQL Statements recent_slow_query: title: Recent Slow Queries instances: title: Alive Instances monitor_alert: title: Monitor & Alert view_grafana_monitor: View Grafana Metrics view_monitor_warn: Metrics unavailable view_alerts: 'View {{alertCount}} Alerts' view_zero_alerts: 'View Alerts' view_alerts_warn: Alert unavailable run_diagnose: Run Diagnostics metrics: total_requests: QPS latency: Latency cpu: TiDB CPU Usage memory: TiDB Memory Usage io: IO Usage ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Overview/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Overview/translations/zh.yaml ================================================ overview: nav_title: 概况 panel_no_data_tips: 文档 view_more_metrics: 查看更多指标 change_prom_button: 更改 prometheus 地址 top_statements: title: 最耗时的 SQL 语句 recent_slow_query: title: 最近慢查询 instances: title: 在线实例 monitor_alert: title: 监控和告警 view_grafana_monitor: 查看监控 view_monitor_warn: 监控不可用 view_alerts: '查看 {{alertCount}} 条告警' view_zero_alerts: 查看告警 view_alerts_warn: 告警不可用 run_diagnose: 运行诊断 metrics: total_requests: QPS latency: 延迟 cpu: TiDB CPU 使用 memory: TiDB 内存使用 io: IO 使用 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Overview/utils/telemetry.ts ================================================ import { TimeRange } from '@lib/components' import { mixpanel } from '@lib/utils/telemetry' export const telemetry = { // time range picker clickZoomOut(timestamps: [number, number]) { mixpanel.track('Overview: Click Zoom Out Button', { timestamps }) }, selectTimeRange(v: TimeRange) { mixpanel.track('Overview: Select Time Range', v) }, clickManualRefresh() { mixpanel.track('Overview: Click Manual Refresh') }, selectAutoRefreshOption(seconds: number) { mixpanel.track('Overview: Select Auto Refresh Option', { seconds }) }, clickDocumentationIcon() { mixpanel.track('Overview: Click Documentation Icon') }, clickViewMoreMetrics() { mixpanel.track('Overview: Click View More Metrics Button') }, clickSeriesLabel(chartTitle: string, seriesName: string) { mixpanel.track('Overview: Click to Hide Series', { chartTitle, seriesName }) } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/Editor.module.less ================================================ .editorContainer { flex-grow: 1; position: relative; overflow: hidden; :global(.ace_editor) { position: absolute; z-index: 1; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/Editor.tsx ================================================ import React, { useRef } from 'react' import AceEditor, { IAceEditorProps } from 'react-ace' import { useSize } from 'ahooks' import 'ace-builds/src-noconflict/mode-sql' import 'ace-builds/src-noconflict/ext-searchbox' // import './editorThemes/oneHalfDark' // import './editorThemes/oneHalfLight' import styles from './Editor.module.less' interface IEditorProps extends IAceEditorProps {} function Editor({ ...props }: IEditorProps, ref: React.Ref) { const containerRef = useRef(null) const containerSize = useSize(containerRef) return (
) } export default React.memo(React.forwardRef(Editor)) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/ResultTable.module.less ================================================ .resultTable { position: absolute; top: @padding-page; // FIXME: This is hacky. Can we provide a component? bottom: 0; left: 0; width: 100%; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/ResultTable.tsx ================================================ import React, { useMemo } from 'react' import { QueryeditorRunResponse } from '@lib/client' import { CardTable } from '@lib/components' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import styles from './ResultTable.module.less' interface IResultTableProps { results?: QueryeditorRunResponse } function ResultTable({ results }: IResultTableProps) { const columns: IColumn[] = useMemo(() => { if (!results) { return [] } if (results.error_msg) { return [ { name: 'Error', key: 'error', minWidth: 100, fieldName: 'error', isMultiline: true } ] } else { return (results.column_names ?? []).map((cn, idx) => ({ name: cn, key: cn, minWidth: 200, maxWidth: 500, fieldName: String(idx) })) } }, [results]) const items = useMemo(() => { if (!results) { return [] } if (results.error_msg) { return [{ error: results.error_msg }] } else { return results.rows ?? [] } }, [results]) return (
) } export default ResultTable ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { QueryeditorRunRequest, QueryeditorRunResponse } from '@lib/client' import { ReqConfig } from '@lib/types' export interface IQueryEditorDataSource { queryEditorRun( request: QueryeditorRunRequest, options?: ReqConfig ): AxiosPromise } export interface IQueryEditorContext { ds: IQueryEditorDataSource } export const QueryEditorContext = createContext( null ) export const QueryEditorProvider = QueryEditorContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/editorThemes/oneHalfDark.js ================================================ /* eslint-disable no-multi-str */ const ace = require('ace-builds/src-noconflict/ace') ace.define( 'ace/theme/oneHalfDark', ['require', 'exports', 'module', 'ace/lib/dom'], function (require, exports, module) { exports.isDark = true exports.cssClass = 'ace-one-half-dark' exports.cssText = '.ace-one-half-dark .ace_gutter {\ background: #282c34;\ color: rgb(130,134,140)\ }\ .ace-one-half-dark .ace_print-margin {\ width: 1px;\ background: #e8e8e8\ }\ .ace-one-half-dark {\ background-color: #282c34;\ color: #dcdfe4\ }\ .ace-one-half-dark .ace_cursor {\ color: #a3b3cc\ }\ .ace-one-half-dark .ace_marker-layer .ace_selection {\ background: #474e5d\ }\ .ace-one-half-dark.ace_multiselect .ace_selection.ace_start {\ box-shadow: 0 0 3px 0px #282c34;\ border-radius: 2px\ }\ .ace-one-half-dark .ace_marker-layer .ace_step {\ background: rgb(198, 219, 174)\ }\ .ace-one-half-dark .ace_marker-layer .ace_bracket {\ margin: -1px 0 0 -1px;\ border: 1px solid #5c6370\ }\ .ace-one-half-dark .ace_marker-layer .ace_active-line {\ background: #313640\ }\ .ace-one-half-dark .ace_gutter-active-line {\ background-color: #313640\ }\ .ace-one-half-dark .ace_marker-layer .ace_selected-word {\ border: 1px solid #474e5d\ }\ .ace-one-half-dark .ace_fold {\ background-color: #61afef;\ border-color: #dcdfe4\ }\ .ace-one-half-dark .ace_keyword {\ color: #c678dd\ }\ .ace-one-half-dark .ace_constant {\ color: #e5c07b\ }\ .ace-one-half-dark .ace_constant.ace_numeric {\ color: #e5c07b\ }\ .ace-one-half-dark .ace_constant.ace_character.ace_escape {\ color: #56b6c2\ }\ .ace-one-half-dark .ace_support.ace_function {\ color: #61afef\ }\ .ace-one-half-dark .ace_support.ace_class {\ color: #e5c07b\ }\ .ace-one-half-dark .ace_storage {\ color: #c678dd\ }\ .ace-one-half-dark .ace_invalid.ace_illegal {\ color: #dcdfe4;\ background-color: #e06c75\ }\ .ace-one-half-dark .ace_invalid.ace_deprecated {\ color: #dcdfe4;\ background-color: #e5c07b\ }\ .ace-one-half-dark .ace_string {\ color: #98c379\ }\ .ace-one-half-dark .ace_string.ace_regexp {\ color: #98c379\ }\ .ace-one-half-dark .ace_comment {\ color: #5c6370\ }\ .ace-one-half-dark .ace_variable {\ color: #e06c75\ }\ .ace-one-half-dark .ace_meta.ace_selector {\ color: #c678dd\ }\ .ace-one-half-dark .ace_entity.ace_other.ace_attribute-name {\ color: #e5c07b\ }\ .ace-one-half-dark .ace_entity.ace_name.ace_function {\ color: #61afef\ }\ .ace-one-half-dark .ace_entity.ace_name.ace_tag {\ color: #e06c75\ }' var dom = require('../lib/dom') dom.importCssString(exports.cssText, exports.cssClass) } ) ;(function () { ace.require(['ace/theme/oneHalfDark'], function (m) { if (typeof module == 'object' && typeof exports == 'object' && module) { module.exports = m } }) })() ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/editorThemes/oneHalfLight.js ================================================ /* eslint-disable no-multi-str */ const ace = require('ace-builds/src-noconflict/ace') ace.define( 'ace/theme/oneHalfLight', ['require', 'exports', 'module', 'ace/lib/dom'], function (require, exports, module) { exports.isDark = false exports.cssClass = 'ace-one-half-light' exports.cssText = '.ace-one-half-light .ace_gutter {\ background: #fafafa;\ color: rgb(153,154,158)\ }\ .ace-one-half-light .ace_print-margin {\ width: 1px;\ background: #e8e8e8\ }\ .ace-one-half-light {\ background-color: #fafafa;\ color: #383a42\ }\ .ace-one-half-light .ace_cursor {\ color: #383a42\ }\ .ace-one-half-light .ace_marker-layer .ace_selection {\ background: #bfceff\ }\ .ace-one-half-light.ace_multiselect .ace_selection.ace_start {\ box-shadow: 0 0 3px 0px #fafafa;\ border-radius: 2px\ }\ .ace-one-half-light .ace_marker-layer .ace_step {\ background: rgb(198, 219, 174)\ }\ .ace-one-half-light .ace_marker-layer .ace_bracket {\ margin: -1px 0 0 -1px;\ border: 1px solid #a0a1a7\ }\ .ace-one-half-light .ace_marker-layer .ace_active-line {\ background: #f0f0f0\ }\ .ace-one-half-light .ace_gutter-active-line {\ background-color: #f0f0f0\ }\ .ace-one-half-light .ace_marker-layer .ace_selected-word {\ border: 1px solid #bfceff\ }\ .ace-one-half-light .ace_fold {\ background-color: #0184bc;\ border-color: #383a42\ }\ .ace-one-half-light .ace_keyword,\ .ace-one-half-light .ace_meta.ace_selector,\ .ace-one-half-light .ace_storage {\ color: #a626a4\ }\ .ace-one-half-light .ace_constant,\ .ace-one-half-light .ace_constant.ace_numeric,\ .ace-one-half-light .ace_entity.ace_other.ace_attribute-name,\ .ace-one-half-light .ace_support.ace_class {\ color: #c18401\ }\ .ace-one-half-light .ace_constant.ace_character.ace_escape {\ color: #0997b3\ }\ .ace-one-half-light .ace_entity.ace_name.ace_function,\ .ace-one-half-light .ace_support.ace_function {\ color: #0184bc\ }\ .ace-one-half-light .ace_invalid.ace_illegal {\ color: #fafafa;\ background-color: #e06c75\ }\ .ace-one-half-light .ace_invalid.ace_deprecated {\ color: #fafafa;\ background-color: #e5c07b\ }\ .ace-one-half-light .ace_string,\ .ace-one-half-light .ace_string.ace_regexp {\ color: #50a14f\ }\ .ace-one-half-light .ace_comment {\ color: #a0a1a7\ }\ .ace-one-half-light .ace_entity.ace_name.ace_tag,\ .ace-one-half-light .ace_variable {\ color: #e45649\ }' var dom = require('../lib/dom') dom.importCssString(exports.cssText, exports.cssClass) } ) ;(function () { ace.require(['ace/theme/oneHalfLight'], function (m) { if (typeof module == 'object' && typeof exports == 'object' && module) { module.exports = m } }) })() ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .container { height: 100vh; display: flex; flex-direction: column; &:before, &:after { // Handle margin collapse content: ' '; display: table; } } .contentContainer { flex: 1; min-height: 0; > :global(.gutter.gutter-vertical) { background-color: @gray-3; cursor: row-resize; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII='); background-repeat: no-repeat; background-position: center center; margin: 0 @padding-page; } &.isCollapsed > :global(.gutter) { display: none; } } .successText { color: @success-color; } .resultTableContainer { position: relative; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/index.tsx ================================================ import React, { useState, useCallback, useRef, useContext } from 'react' import cx from 'classnames' import { Root, Card } from '@lib/components' import Split from 'react-split' import { Button, Space, Typography } from 'antd' import { CaretRightOutlined, LoadingOutlined, WarningOutlined, CheckOutlined } from '@ant-design/icons' import { Routes, Route, HashRouter as Router } from 'react-router-dom' import Editor from './Editor' import ResultTable from './ResultTable' import styles from './index.module.less' import { QueryeditorRunResponse } from '@lib/client' import ReactAce from 'react-ace/lib/ace' import { getValueFormat } from '@baurine/grafana-value-formats' import translations from './translations' import { addTranslations } from '@lib/utils/i18n' import { QueryEditorContext } from './context' import { useLocationChange } from '@lib/hooks/useLocationChange' const MAX_DISPLAY_ROWS = 1000 addTranslations(translations) function QueryEditor() { const ctx = useContext(QueryEditorContext) if (ctx === null) { throw new Error('QueryEditorContext must not be null') } const [results, setResults] = useState() const [isRunning, setRunning] = useState(false) const editor = useRef(null) const isResultsEmpty = !results || (!results.error_msg && (!results.column_names?.length || !results.rows)) const handleRun = useCallback(async () => { try { setRunning(true) setResults(undefined) const resp = await ctx!.ds.queryEditorRun({ max_rows: MAX_DISPLAY_ROWS, statements: editor.current?.editor.getValue() }) setResults(resp.data) } finally { setRunning(false) } editor.current?.editor.focus() }, [ctx]) return (
{ {isRunning && } {results && results.error_msg && ( Error ( {getValueFormat('ms')(results.execution_ms || 0, 1)}) )} {results && !results.error_msg && ( Success ( {getValueFormat('ms')(results.execution_ms || 0, 1)}, {(results.actual_rows || 0) > (results.rows?.length || 0) ? `Displaying first ${results.rows?.length || 0} of ${ results.actual_rows || 0 } rows` : `${results.rows?.length || 0} rows`} ) )} }
{!isResultsEmpty && }
) } function AppRoutes() { useLocationChange() return ( } /> ) } function App() { return ( ) } export default App export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/translations/en.yaml ================================================ query_editor: nav_title: Query Editor ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/QueryEditor/translations/zh.yaml ================================================ query_editor: nav_title: 查询编辑器 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/Configuration.tsx ================================================ import { Card, CardTable } from '@lib/components' import React, { useMemo } from 'react' import { useResourceManagerContext } from '../context' import { useClientRequest } from '@lib/utils/useClientRequest' import { Space, Switch, Typography } from 'antd' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { ResourcemanagerResourceInfoRowDef } from '@lib/client' import { useTranslation } from 'react-i18next' type ConfigurationProps = { info: ResourcemanagerResourceInfoRowDef[] loadingInfo: boolean } export const Configuration: React.FC = ({ info, loadingInfo }) => { const ctx = useResourceManagerContext() const { data: config, isLoading: loadingConfig } = useClientRequest( ctx.ds.getConfig ) const { t } = useTranslation() const columns: IColumn[] = useMemo(() => { return [ { name: t('resource_manager.configuration.table_fields.resource_group'), key: 'resource_group', minWidth: 100, maxWidth: 400, onRender: (row: any) => { return {row.name} } }, { name: t('resource_manager.configuration.table_fields.ru_per_sec'), key: 'ru_per_sec', minWidth: 100, maxWidth: 150, onRender: (row: any) => { return {row.ru_per_sec} } }, { name: t('resource_manager.configuration.table_fields.priority'), key: 'priority', minWidth: 100, maxWidth: 150, onRender: (row: any) => { return {row.priority} } }, { name: t('resource_manager.configuration.table_fields.burstable'), key: 'burstable', minWidth: 100, maxWidth: 150, onRender: (row: any) => { return {row.burstable} } } ] }, [t]) return ( {t('resource_manager.configuration.enabled')} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/EstimateCapacity.tsx ================================================ import { Card, CardTabs, ErrorBar, Pre, TimeRangeSelector, toTimeRangeValue } from '@lib/components' import { Alert, Col, Row, Select, Space, Statistic, Tooltip, Typography } from 'antd' import ReactMarkdown from 'react-markdown' import React, { useEffect, useMemo } from 'react' import { useResourceManagerContext } from '../context' import { useClientRequest } from '@lib/utils/useClientRequest' import { InfoCircleOutlined } from '@ant-design/icons' import { useResourceManagerUrlState } from '../uilts/url-state' import { TIME_WINDOW_RECENT_SECONDS, WORKLOAD_TYPES } from '../uilts/helpers' import { useTranslation } from 'react-i18next' const { Option } = Select const { Paragraph, Text, Link } = Typography const CapacityWarning: React.FC<{ totalRU: number; estimatedRU: number }> = ({ totalRU, estimatedRU }) => { const { t } = useTranslation() if (estimatedRU > 0 && totalRU > estimatedRU) { return (
) } return null } const HardwareCalibrate: React.FC<{ totalRU: number }> = ({ totalRU }) => { const ctx = useResourceManagerContext() const { workload, setWorkload } = useResourceManagerUrlState() const { data, isLoading, sendRequest, error } = useClientRequest( (reqConfig) => ctx.ds.getCalibrateByHardware({ workload }, reqConfig) ) useEffect(() => { sendRequest() }, [workload]) const estimatedRU = data?.estimated_capacity ?? 0 const { t } = useTranslation() return (
{t('resource_manager.estimate_capacity.workload_select_tooltip')} } >
RUs/sec } />
{error && (
{' '} {' '}
)}
) } const WorkloadCalibrate: React.FC<{ totalRU: number }> = ({ totalRU }) => { const ctx = useResourceManagerContext() const { timeRange, setTimeRange } = useResourceManagerUrlState() const { data, isLoading, sendRequest, error } = useClientRequest( (reqConfig) => { const [start, end] = toTimeRangeValue(timeRange) return ctx.ds.getCalibrateByActual( { startTime: start, endTime: end }, reqConfig ) } ) useEffect(() => { sendRequest() }, [timeRange]) const estimatedRU = data?.estimated_capacity ?? 0 const { t } = useTranslation() return (
{t( 'resource_manager.estimate_capacity.time_window_select_tooltip' )} } >
RUs/sec } />
{error && (
{' '} {' '}
)}
) } export const EstimateCapacity: React.FC<{ totalRU: number }> = ({ totalRU }) => { const { t } = useTranslation() const tabs = useMemo(() => { return [ { key: 'calibrate_by_hardware', title: t('resource_manager.estimate_capacity.calibrate_by_hardware'), content: () => }, { key: 'calibrate_by_workload', title: t('resource_manager.estimate_capacity.calibrate_by_workload'), content: () => } ] }, [totalRU, t]) return (
{t('resource_manager.estimate_capacity.ru_desc_line_1')}
{t('resource_manager.estimate_capacity.ru_desc_line_2')}

{t( 'resource_manager.estimate_capacity.change_resource_allocation' )} {t( 'resource_manager.estimate_capacity.resource_allocation_line_1' )}
{`ALTER RESOURCE GROUP RU_PER_SEC=<#ru> [BURSTABLE];`}
{t( 'resource_manager.estimate_capacity.resource_allocation_ref' )}{' '} {t( 'resource_manager.estimate_capacity.resource_allocation_user_manual' )} .
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/Metrics.tsx ================================================ import { TimeRangeSelector } from '@lib/components' import { Space, Typography, Row, Col } from 'antd' import React, { useCallback, useRef, useState } from 'react' import { useMemoizedFn } from 'ahooks' import { MetricsChart, SyncChartPointer, TimeRangeValue } from 'metrics-chart' import { debounce } from 'lodash' import { Card, TimeRange, ErrorBar } from '@lib/components' import { tz } from '@lib/utils' import { useTimeRangeValue } from '@lib/components/TimeRangeSelector/hook' import { useResourceManagerContext } from '../context' import { useResourceManagerUrlState } from '../uilts/url-state' import { MetricConfig, metrics } from '../uilts/metricQueries' import { useTranslation } from 'react-i18next' export const Metrics: React.FC = () => { const { timeRange, setTimeRange } = useResourceManagerUrlState() const [, setIsSomeLoading] = useState(false) const { t } = useTranslation() return (
) } interface MetricsChartWrapperProps { metrics: MetricConfig[] timeRange: TimeRange setTimeRange: (timeRange: TimeRange) => void setIsSomeLoading: (isLoading: boolean) => void } const MetricsChartWrapper: React.FC = ({ metrics, timeRange, setTimeRange, setIsSomeLoading }) => { const ctx = useResourceManagerContext() const loadingCounter = useRef(0) const [chartRange, setChartRange] = useTimeRangeValue(timeRange, setTimeRange) // eslint-disable-next-line const setIsSomeLoadingDebounce = useCallback( debounce(setIsSomeLoading, 100, { leading: true }), [] ) const handleOnBrush = (range: TimeRangeValue) => { setChartRange(range) } const onLoadingStateChange = useMemoizedFn((loading: boolean) => { loading ? (loadingCounter.current += 1) : loadingCounter.current > 0 && (loadingCounter.current -= 1) setIsSomeLoadingDebounce(loadingCounter.current > 0) }) const ErrorComponent = (error: Error) => ( ) return ( {metrics.map((item) => ( {item.title} ))} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/index.ts ================================================ export * from './Configuration' export * from './EstimateCapacity' export * from './Metrics' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/context/index.ts ================================================ import { MetricsQueryResponse, ResourcemanagerCalibrateResponse, ResourcemanagerGetConfigResponse, ResourcemanagerResourceInfoRowDef } from '@lib/client' import { ReqConfig } from '@lib/types' import { AxiosPromise } from 'axios' import { createContext, useContext } from 'react' export interface IResourceManagerDataSource { getConfig(options?: ReqConfig): AxiosPromise getInformation( options?: ReqConfig ): AxiosPromise getCalibrateByHardware( params: { workload: string }, options?: ReqConfig ): AxiosPromise getCalibrateByActual( params: { startTime: number; endTime: number }, options?: ReqConfig ): AxiosPromise metricsQueryGet(params: { endTimeSec?: number query?: string startTimeSec?: number stepSec?: number }): Promise } export interface IResourceManagerConfig {} export interface IResourceManagerContext { ds: IResourceManagerDataSource cfg: IResourceManagerConfig } export const ResourceManagerContext = createContext(null) export const ResourceManagerProvider = ResourceManagerContext.Provider export const useResourceManagerContext = () => { const ctx = useContext(ResourceManagerContext) if (ctx === null) { throw new Error('ResourceManagerContext must not be null') } return ctx } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/index.tsx ================================================ import React from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import { Root } from '@lib/components' import { useLocationChange } from '@lib/hooks/useLocationChange' import { addTranslations } from '@lib/utils/i18n' import translations from './translations' import { useResourceManagerContext } from './context' import { Home } from './pages' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> ) } export default function () { useResourceManagerContext() return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/pages/index.tsx ================================================ import React, { useMemo } from 'react' import { Configuration, EstimateCapacity, Metrics } from '../components' import { useClientRequest } from '@lib/utils/useClientRequest' import { useResourceManagerContext } from '../context' export const Home: React.FC = () => { const ctx = useResourceManagerContext() const { data: info, isLoading: loadingInfo } = useClientRequest( ctx.ds.getInformation ) const totalRU = useMemo(() => { return (info ?? []) .filter((item) => item.name !== 'default') .reduce((acc, cur) => { const ru = Number(cur.ru_per_sec) if (!isNaN(ru)) { return acc + ru } return acc }, 0) }, [info]) return (
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/en.yaml ================================================ resource_manager: nav_title: Resource Manager configuration: title: Configuration enabled: TiDB Resource Manager Enabled table_fields: resource_group: Resource Group ru_per_sec: RUs/sec priority: Priority burstable: Burstable estimate_capacity: title: Estimate Capacity ru_desc_line_1: Request Unit (RU) is a unified abstraction unit in TiDB for system resources, which is relavant to resource comsuption. ru_desc_line_2: Please notice the "estimated capacity" refers to a result that is hardware specs or past statistics, and may deviate from actual capacity. change_resource_allocation: Change the Resource Allocation resource_allocation_line_1: 'To change the resource allocation for resource group:' resource_allocation_ref: For detail information, please refer to resource_allocation_user_manual: user manual calibrate_by_hardware: Calibrate by Hardware calibrate_by_workload: Calibrate by Workload estimated_capacity: Estimated Capacity total_ru: Total RU of user resource groups exceed_warning: The total RU of all customized resource groups exceeds the "estimated capacity". The RU allocated to some resource groups could not be satisfied. workload_select_tooltip: | Select a workload type which is similar with your actual workload. - **oltp_read_write**: applies to workloads with even data read and write. It is estimated based on a workload model similar to `sysbench oltp_read_write` - **oltp_read_only**: applies to workloads with heavy data read. It is estimated based on a workload model similar to `sysbench oltp_read_only` - **oltp_write_only**: applies to workloads with heavy data write. It is estimated based on a workload model similar to `sysbench oltp_write_only` - **tpcc**: applies to workloads with heavy data write. It is estimated based on a workload model similar to `TPC-C` time_window_select_tooltip: | Select the time window with classic workload in the past, with which TiDB can come a better estimation of RU capacity. Time window length: 10 mins ~ 24 hours metrics: title: Metrics ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/zh.yaml ================================================ resource_manager: nav_title: 资源管控 configuration: title: 配置 enabled: 启用 TiDB 资源管控 table_fields: resource_group: 资源分组 ru_per_sec: 每秒请求单元 priority: 优先级 burstable: 是否可突发 estimate_capacity: title: 容量估算 ru_desc_line_1: 请求单元 (RU) 是一个统一的抽象单元,用于表示 TiDB 系统资源,与资源消耗相关。 ru_desc_line_2: 请注意,"估算容量" 是基于硬件配置或过去的统计数据,可能与实际容量有所偏差。 change_resource_allocation: 修改资源分配 resource_allocation_line_1: '执行以下命令修改资源分配:' resource_allocation_ref: 想了解更多,请参考 resource_allocation_user_manual: 用户手册 calibrate_by_hardware: 通过硬件配置校准 calibrate_by_workload: 通过负载校准 estimated_capacity: 估算容量 total_ru: 用户资源分组总请求单元 exceed_warning: 所有自定义资源分组的总请求单元超过了 "估算容量"。部分资源分组的请求单元可能无法满足。 workload_select_tooltip: | 选择一个与实际工作负载相似的工作负载类型。 - **oltp_read_write**: 数据读写平衡的负载,根据类似 `sysbench oltp_read_write` 的负载模型预测 - **oltp_read_only**: 数据读取较重的负载,根据类似 `sysbench oltp_read_only` 的负载模型预测 - **oltp_write_only**: 数据写入较重的负载,根据类似 `sysbench oltp_write_only` 的负载模型预测 - **tpcc**: 数据写入较重的负载,根据类似 `TPC-C` 的负载模型预测 time_window_select_tooltip: | 选择一个过去的典型工作负载时间窗口,TiDB 会基于该时间窗口的统计数据来估算 RU 容量。 时间窗口长度:10 分钟 ~ 24 小时 metrics: title: 监控指标 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/helpers.ts ================================================ import { TimeRange } from '@lib/components/TimeRangeSelector' export const TIME_WINDOW_RECENT_SECONDS = [ 15 * 60, 30 * 60, 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60 ] export const DEFAULT_TIME_WINDOW: TimeRange = { type: 'recent', value: TIME_WINDOW_RECENT_SECONDS[1] } export const WORKLOAD_TYPES = [ 'oltp_read_write', 'oltp_read_only', 'oltp_write_only', 'tpcc' ] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/metricQueries.ts ================================================ import { QueryConfig, TransformNullValue } from 'metrics-chart' export type MetricConfig = { title: string queries: QueryConfig[] unit: string nullValue?: TransformNullValue } export const metrics: MetricConfig[] = [ { title: 'Total RU Consumed', queries: [ { promql: 'sum(rate(resource_manager_resource_unit_read_request_unit_sum[1m])) + sum(rate(resource_manager_resource_unit_write_request_unit_sum[1m]))', name: 'Total RU', type: 'line' } ], unit: 'short', nullValue: TransformNullValue.AS_ZERO }, { title: 'RU Consumed by Resource Groups', queries: [ { promql: 'sum(rate(resource_manager_resource_unit_read_request_unit_sum[1m])) by (name) + sum(rate(resource_manager_resource_unit_write_request_unit_sum[1m])) by (name)', name: '{name}', type: 'line' } ], unit: 'short', nullValue: TransformNullValue.AS_ZERO }, { title: 'TiDB CPU Usage', queries: [ { promql: 'rate(process_cpu_seconds_total{job="tidb"}[30s])', name: '{instance}', type: 'line' }, { promql: 'tidb_server_maxprocs', name: 'Limit-{instance}', type: 'line' } ], unit: 'percentunit', nullValue: TransformNullValue.AS_ZERO }, { title: 'TiKV CPU Usage', queries: [ { promql: 'sum(rate(tikv_thread_cpu_seconds_total[$__rate_interval])) by (instance)', name: '{instance}', type: 'line' }, { promql: 'tikv_server_cpu_cores_quota', name: 'Limit-{instance}', type: 'line' } ], unit: 'percentunit' }, { title: 'TiKV IO MBps', queries: [ { promql: 'sum(rate(tikv_engine_flow_bytes{db="kv", type="wal_file_bytes"}[$__rate_interval])) by (instance) + sum(rate(tikv_engine_flow_bytes{db="raft", type="wal_file_bytes"}[$__rate_interval])) by (instance) + sum(rate(raft_engine_write_size_sum[$__rate_interval])) by (instance)', name: '{instance}-write', type: 'line' }, { promql: 'sum(rate(tikv_engine_flow_bytes{db="kv", type=~"bytes_read|iter_bytes_read"}[$__rate_interval])) by (instance)', name: '{instance}-read', type: 'line' } ], unit: 'Bps' } ] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/url-state.ts ================================================ import useUrlState from '@ahooksjs/use-url-state' import { TimeRange, toURLTimeRange, urlToTimeRange } from '@lib/components/TimeRangeSelector' import { useCallback, useMemo } from 'react' import { DEFAULT_TIME_WINDOW, WORKLOAD_TYPES } from './helpers' type UrlState = Partial> export function useResourceManagerUrlState() { const [queryParams, setQueryParams] = useUrlState() const timeRange = useMemo(() => { const { from, to } = queryParams if (from && to) { return urlToTimeRange({ from, to }) } return DEFAULT_TIME_WINDOW }, [queryParams.from, queryParams.to]) const setTimeRange = useCallback( (newTimeRange: TimeRange) => { setQueryParams({ ...toURLTimeRange(newTimeRange) }) }, [setQueryParams] ) const workload = queryParams.workload || WORKLOAD_TYPES[0] const setWorkload = useCallback( (w: string) => { setQueryParams({ workload: w || undefined }) }, [setQueryParams] ) return { timeRange, setTimeRange, workload, setWorkload } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/component/IndexInsightList.tsx ================================================ import React, { useEffect, useContext, useState, useRef, MutableRefObject } from 'react' import IndexInsightTable, { useSQLTunedListGet } from './IndexInsightTable' import { Space, Button, Typography, notification, // Alert, Modal, Tooltip, Drawer, Checkbox } from 'antd' import { InfoCircleOutlined } from '@ant-design/icons' import { Card, Toolbar } from '@lib/components' import { SQLAdvisorContext } from '../context' import dayjs from 'dayjs' import { PerfInsightTask, PerfInsightTaskStatus } from '../types' interface IndexInsightListProps { onHandleDeactivate?: () => void isDeactivating?: boolean } const CHECK_TASK_INTERVAL = 60 * 1000 const IndexInsightList = ({ onHandleDeactivate, isDeactivating }: IndexInsightListProps) => { const ctx = useContext(SQLAdvisorContext) const [showDeactivateModal, setShowDeactivateModal] = useState(false) const { sqlTunedList, refreshSQLTunedList, loading } = useSQLTunedListGet() // const [showAlert, setShowAlert] = useState(false) const [showSetting, setShowSetting] = useState(false) const [showCheckUpModal, setShowCheckUpModal] = useState(false) const [taskStatus, setTaskStatus] = useState() const isTaskRunning = taskStatus === 'running' || taskStatus === 'created' const latestTask = useRef(null) as MutableRefObject< PerfInsightTask | undefined > const dontRemindCheckUpNotice = useRef( JSON.parse( localStorage.getItem('index_insight_dont_remind_checkup_notice') || 'false' ) ) const timer = useRef(0) const checkStatusLoop = useRef(async () => { clearTimeout(timer.current) try { const res = await ctx?.ds.tuningLatestGet() latestTask.current = res // No tasks if (!res) { return } setTaskStatus(res.status) if (res.status === 'failed') { notification.error({ message: 'Last Task Error', description: res.last_failed_message || 'Unknown error' }) } if (res.status !== 'succeeded' && res.status !== 'failed') { timer.current = window.setTimeout(async () => { const nextRes = await checkStatusLoop.current() // refresh when status change: !successed -> successed if (nextRes?.status === 'succeeded') { refreshSQLTunedList() } }, CHECK_TASK_INTERVAL) } return res } catch (e) { latestTask.current = undefined setTaskStatus('failed') throw e } }) useEffect(() => { checkStatusLoop.current() return () => window.clearTimeout(timer.current) }, [ctx]) const handleCheckUpTask = async () => { try { await ctx?.ds.tuningTaskCreate( (dayjs().unix() - 3 * 60 * 60) * 1000, dayjs().unix() * 1000 ) notification.success({ message: 'Successed' }) } catch (e: any) { notification.error({ message: e.message }) } finally { checkStatusLoop.current() } } const handleCancelTask = async () => { try { await ctx?.ds.tuningTaskCancel(latestTask.current!.task_id) notification.success({ message: 'Successed' }) } catch (e: any) { notification.error({ message: e.message }) } finally { checkStatusLoop.current() } } const hanleDeactivate = () => { setShowDeactivateModal(false) setShowSetting(false) onHandleDeactivate?.() } const handleDeactivateModalCancel = () => { setShowDeactivateModal(false) setShowSetting(false) } return ( <> Performance Insight {isTaskRunning && ( )} setShowSetting(false)} destroyOnClose={true} >

After deactivation, the system will delete all historical insight data.

After disabling, all insight data generated by this feature will be deleted.

setShowCheckUpModal(false)} destroyOnClose={true} footer={null} >

When performing checks, system tables are queried. It is not recommended to perform checks when the cluster is already under heavy load.

(dontRemindCheckUpNotice.current = e.target.checked) } > Don't remind me again.
{/* {showAlert && ( )} */}
) } export default IndexInsightList ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/component/IndexInsightListWithRegister.module.less ================================================ .container { margin: 2rem; } .commandBlock { display: flex; justify-content: space-between; background: #f1f1f1; font-size: 12px; padding: 1rem; } .instructionCard { margin-bottom: 1rem; } .siteFormItemIcon { fill: '#c1c1c1'; } .loginFormButton { width: 100%; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/component/IndexInsightListWithRegister.tsx ================================================ import React, { useCallback, useContext, useEffect, useState } from 'react' import { SQLAdvisorContext } from '../context' import { Typography, Form, Input, Button, Card, Row, Col, notification, Skeleton, Alert } from 'antd' import styles from './IndexInsightListWithRegister.module.less' import { HighlightSQL, CopyLink } from '@lib/components' import { LockOutlined, UserOutlined } from '@ant-design/icons' import IndexInsightList from './IndexInsightList' import { DbassSecuritySettingImg } from '../utils/dbaasSecuritySetting' const { Title } = Typography const sql = [ `CREATE user 'yourusername'@'%' IDENTIFIED by 'yourpassword';`, `GRANT SELECT ON information_schema.* TO 'yourusername'@'%';`, `GRANT SELECT ON mysql.* TO 'yourusername'@'%';`, `GRANT PROCESS, REFERENCES ON *.* TO 'yourusername'@'%';`, `FLUSH PRIVILEGES;` ] const RegisterForm = ({ setIsUserDBRegistered }: UnRegisteredUserDBProps) => { const ctx = useContext(SQLAdvisorContext) const [isPosting, setIsPosting] = useState(false) const handleOnFinish = useCallback( async (values: any) => { setIsPosting(true) const params = { userName: values.username, addr: values.addr, port: values.port, password: values.password } try { const res = await ctx?.ds.activateDBConnection(params) notification.success({ message: res }) setIsUserDBRegistered(true) } catch (e: any) { notification.error({ message: e.message }) } finally { setIsPosting(false) } }, [ctx, setIsUserDBRegistered] ) return (
} placeholder="Username" /> } type="password" placeholder="Password" />
) } interface UnRegisteredUserDBProps { setIsUserDBRegistered: (isUserDBRegistered: boolean) => void } const UnRegisteredUserDB: React.FC = ({ setIsUserDBRegistered }) => { return (
Performance Insight (BETA):

Improve your database performance with ease by using our advanced algorithms to analyze your collection metadata and slow query logs. Trust our feature to identify areas where indexes or changes to the schema can improve query performance and make the necessary adjustments for optimal performance. Activate now for faster and more efficient database performance.

Permissions required:

This feature requires read access to database `information_schema` and `mysql`. You can create a new sql user on your SQL client to activate this feature.

{sql.map((s) => ( ))}
Network required:

During the Beta phase, this feature requires users to{' '} manually open the IP access list before it can be enabled. In subsequent versions, a more user-friendly method of enabling this feature will be supported.

You should

1. Open "Allow Access From Anywhere" on IP Access List.

2. Click "Apply" Buttom to complete the change.

) } const IndexInsightListWithRegister = () => { const ctx = useContext(SQLAdvisorContext) const [isLoading, setIsLoading] = useState(true) const [isUserDBRegistered, setIsUserDBRegistered] = useState(false) const [isDeactivating, setIsDeactivating] = useState(false) useEffect(() => { const registerUserDBStatusGet = async () => { try { setIsLoading(true) const status = await ctx?.ds.checkDBConnection() setIsUserDBRegistered(status) } catch (e) { setIsUserDBRegistered(false) } finally { setIsLoading(false) } } registerUserDBStatusGet() }, [ctx]) const handleDeactivate = async () => { try { setIsDeactivating(true) const res = await ctx?.ds.deactivateDBConnection() setIsUserDBRegistered(false) notification.success({ message: res }) } catch (e: any) { notification.error({ message: e.message }) } finally { setIsDeactivating(false) } } return ( <> {isLoading ? ( ) : ( <> {isUserDBRegistered ? ( ) : ( )} )} ) } export default IndexInsightListWithRegister ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/component/IndexInsightTable.tsx ================================================ import React, { useEffect, useMemo, useState, useContext, useRef } from 'react' import { Card, HighlightSQL, TextWrap } from '@lib/components' import { Tooltip, Table } from 'antd' import { Link } from 'react-router-dom' import { getSuggestedCommand } from '../utils/suggestedCommandMaps' import { SQLAdvisorContext } from '../context' import { SQLTunedListProps } from '../types' import dayjs from 'dayjs' import tz from '@lib/utils/timezone' const DEF_PAGINATION_PARAMS = { pageNumber: 1, pageSize: 20 } export const useSQLTunedListGet = () => { const ctx = useContext(SQLAdvisorContext) const [sqlTunedList, setSqlTunedList] = useState( null ) const [loading, setLoading] = useState(false) const sqlTunedListGet = useRef( async (pageNumber?: number, pageSize?: number) => { setLoading(true) try { const res = await ctx?.ds.tuningListGet( pageNumber || DEF_PAGINATION_PARAMS.pageNumber, pageSize || DEF_PAGINATION_PARAMS.pageSize ) setSqlTunedList(res!) } catch (e) { console.log(e) } finally { setLoading(false) } } ) useEffect(() => { sqlTunedListGet.current() }, []) return { sqlTunedList, refreshSQLTunedList: sqlTunedListGet.current, loading } } interface IndexInsightTableProps { sqlTunedList: SQLTunedListProps | null loading: boolean onHandlePaginationChange?: (pageNumber: number, pageSize: number) => void } const IndexInsightTable = ({ sqlTunedList, loading, onHandlePaginationChange }: IndexInsightTableProps) => { const columns = useMemo( () => [ { title: 'Impact', dataIndex: 'impact', key: 'impact', ellipsis: true, render: (_, record) => { return <>{record.impact} } }, { title: 'Type', dataIndex: 'insight_type', key: 'type', ellipsis: true, render: (_, record) => { return <>{record.insight_type} } }, { title: 'Suggested Command', dataIndex: 'suggested_command', key: 'suggested_command', ellipsis: true, render: (_, record) => { return ( <> {record.suggested_command?.map((command, idx) => ( {getSuggestedCommand( command.suggestion_key, command.params )} ))} ) } }, { title: 'Related Slow SQL', dataIndex: 'sql_statement', key: 'related_slow_sql', ellipsis: true, render: (_, record) => { return ( } placement="left" > ) } }, { title: `Check Up Time (UTC${ tz.getTimeZone() < 0 ? '-' : '+' }${tz.getTimeZone()})`, dataIndex: 'checked_time', key: 'check_up_time', ellipsis: true, render: (_, record) => { return ( <> {dayjs(record.checked_time) .utcOffset(tz.getTimeZone()) .format('YYYY-MM-DD HH:mm:ss')} ) } }, { title: 'Results', dataIndex: 'detail', key: 'detail', render: (_, record) => { return Detail } } ], [] ) return ( { onHandlePaginationChange?.(pageNumber, pageSize) } }} /> ) } export default IndexInsightTable ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/component/index.tsx ================================================ import IndexInsightTable from './IndexInsightTable' import IndexInsightListWithRegister from './IndexInsightListWithRegister' import IndexInsightList from './IndexInsightList' export { IndexInsightTable, IndexInsightListWithRegister, IndexInsightList } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/context/index.tsx ================================================ import { createContext } from 'react' import { TuningDetailProps, SQLTunedListProps, PerfInsightTask } from '../types/' export interface ISQLAdvisorDataSource { tuningListGet( pageNumber?: number, pageSize?: number ): Promise tuningDetailGet(id: number): Promise tuningLatestGet(): Promise tuningTaskCreate(startTime: number, endTime: number): Promise tuningTaskCancel(id: number): Promise activateDBConnection(params: { userName: string password: string }): Promise deactivateDBConnection(): Promise checkDBConnection(): Promise } export interface ISQLAdvisorContext { ds: ISQLAdvisorDataSource orgId?: string clusterId?: string registerUserDB?: boolean } export const SQLAdvisorContext = createContext(null) export const SQLAdvisorProvider = SQLAdvisorContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import { Root } from '@lib/components' import { addTranslations } from '@lib/utils/i18n' import { List, Detail } from './pages' import { SQLAdvisorContext } from './context' import translations from './translations' import { useLocationChange } from '@lib/hooks/useLocationChange' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> } /> ) } export default function () { const ctx = useContext(SQLAdvisorContext) if (ctx === null) { throw new Error('SQLAdvisorContext must not be null') } return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/pages/Detail/index.module.less ================================================ .InlineCodeBlock { background: 1#f1f1f1; padding: 1px 8px; border: 1px solid #dedede; } .SuggestedCommand { background: #f1f1f1; padding: 3px 10px; display: flex; width: 100%; flex-flow: row; justify-content: space-between; } .HighImpact { color: #f70000; } .MediumImpact { color: #fc6f03; } .LowImpact { color: #e5c613; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/pages/Detail/index.tsx ================================================ import React, { useState, useMemo, useEffect, useContext } from 'react' import { Head, Descriptions, Expand, HighlightSQL, CopyLink } from '@lib/components' import { Link } from 'react-router-dom' import { ArrowLeftOutlined } from '@ant-design/icons' import useQueryParams from '@lib/utils/useQueryParams' import { Space, Collapse, Tooltip, Table } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import { getSuggestedCommand } from '../../utils/suggestedCommandMaps' import { TuningDetailProps } from '../../types' import { SQLAdvisorContext } from '../../context' import dayjs from 'dayjs' import tz from '@lib/utils/timezone' import styles from './index.module.less' const { Panel } = Collapse const PanelMaps: Record = { basic: 'Basic Information', table_clause: 'Existing Indexes', table_healthies: 'Table Healthies' } export default function SQLAdvisorDetail() { const ctx = useContext(SQLAdvisorContext) const { id } = useQueryParams() const [sqlTunedDetail, setSqlTunedDetail] = useState(null) const [sqlExpanded, setSqlExpanded] = useState(false) const [loading, setLoading] = useState(true) const toggleSqlExpanded = () => setSqlExpanded((prev) => !prev) const tableClausesColumns = useMemo( () => [ { title: 'Table', dataIndex: 'table_name', key: 'table_name', ellipsis: true, render: (_, row) => { return ( {row.table_name} ) } }, { title: 'Index Name', dataIndex: 'index_name', key: 'index_name', ellipsis: true, render: (_, row) => { return <>{row.index_name} } }, { title: 'Column', dataIndex: 'columns', key: 'columns', ellipsis: true, render: (_, row) => { return <>{row.columns} } }, { title: 'Clustered', dataIndex: 'clusterd', key: 'clusterd', width: 100, ellipsis: true, render: (_, row) => { return <>{row.clusterd ? 'Yes' : 'No'} } }, { title: 'Visible', dataIndex: 'visible', key: 'visible', ellipsis: true, render: (_, row) => { return <>{row.visible ? 'Yes' : 'No'} } } ], [] ) const tableHealthiesColumns = useMemo( () => [ { title: 'Table', dataIndex: 'table_name', key: 'table_name', ellipsis: true, render: (_, row) => { return ( {row.table_name} ) } }, { title: 'Healthy', dataIndex: 'healthy', key: 'healthy', ellipsis: true, render: (_, row) => { return <>{row.healthy} } }, { title: `Analyzed Time (UTC${ tz.getTimeZone() < 0 ? '-' : '+' }${tz.getTimeZone()})`, dataIndex: 'checked_time', key: 'checked_time', ellipsis: true, render: () => { return ( <> {dayjs(sqlTunedDetail?.checked_time) .utcOffset(tz.getTimeZone()) .format('YYYY-MM-DD HH:mm:ss')} ) } } ], [sqlTunedDetail] ) const existingIndexes = sqlTunedDetail?.table_clauses?.map( (item) => item.index_list ) const suggestedCommands = sqlTunedDetail?.suggested_command const suggestedCommandsCopyData = suggestedCommands && suggestedCommands.map((command) => getSuggestedCommand(command.suggestion_key, command.params) ) const suggestedCMDExplanation = suggestedCommands && suggestedCommands .map((cmd) => { const fields = cmd.cmd_explanation.fields const table_name = cmd.cmd_explanation.table_name const explanation = { fields: fields, table_name: table_name } return fields && table_name ? explanation : null }) .filter((cmd) => cmd) useEffect(() => { const sqlTunedDetailGet = async () => { try { const res = await ctx?.ds.tuningDetailGet(id) setSqlTunedDetail(res!) } finally { setLoading(false) } } sqlTunedDetailGet() }, [ctx, id]) return (
} >
{loading && }
{sqlTunedDetail && ( SQL Statement } > } > SQL Digest } > {sqlTunedDetail.sql_digest} Plan Digest } > {sqlTunedDetail.plan_digest} {suggestedCMDExplanation && suggestedCMDExplanation.length > 0 && ( <>

The query is {sqlTunedDetail.insight_type} on

    {suggestedCMDExplanation.map((cmdExp) => (
  • fields{' '} {cmdExp?.fields.join(', ')} {' '} in the{' '} {cmdExp!.table_name} {' '} table
  • ))}

which is expected to have a{' '} {sqlTunedDetail.impact.toUpperCase()}{' '} on query performance.

)}

You can execute this command on create the corresponding index:{' '}

{suggestedCommands && suggestedCommandsCopyData && (
{suggestedCommands.map((command) => (
{getSuggestedCommand( command!.suggestion_key, command!.params )}
))}
)} {sqlTunedDetail.table_clauses && existingIndexes && existingIndexes.flat().length > 0 && (
)} {sqlTunedDetail.table_healthies && sqlTunedDetail.table_healthies.length > 0 && (
)} )} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/pages/List/List.module.less ================================================ @import 'antd/es/style/themes/default.less'; .list { &_container { display: flex; flex-direction: column; height: 100vh; } &_toolbar { @media only screen and (max-width: @screen-md) { flex-direction: column; } } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/pages/List/index.tsx ================================================ import React, { useContext } from 'react' import styles from './List.module.less' import { IndexInsightListWithRegister, IndexInsightList } from '../../component' import { SQLAdvisorContext } from '../../context' export default function SQLAdvisorOverview() { const ctx = useContext(SQLAdvisorContext) return (
{ctx?.registerUserDB ? ( ) : ( )}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/pages/index.tsx ================================================ import List from './List' import Detail from './Detail' export { List, Detail } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/translations/en.yaml ================================================ sql_advisor: nav_title: SQL Advisor ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/translations/zh.yaml ================================================ sql_advisor: nav_title: SQL 诊断 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/types/index.ts ================================================ export interface TuningDetailProps { analyzed_time: string checked_time: number id: number impact: string insight_type: string plan: string plan_digest: string sql_digest: string sql_statement: string suggested_command: { cmd_explanation: { table_name: string fields: string[] } suggestion_key: string params: string[] }[] table_clauses: { table_name: string where_clause: string[] selected_fields: null index_list: { table_name: string columns: string index_name: string clusterd: boolean visible: boolean }[] }[] table_healthies: { table_name: string healthy: string analyzed_time: string }[] use_Stats: boolean use_index: boolean } export interface SQLTunedListProps { tuned_results: TuningDetailProps[] count: number } export type PerfInsightTaskStatus = | 'succeeded' | 'created' | 'running' | 'failed' export interface PerfInsightTask { cluster_id: number created_at: number dbaas_cluster_id: number dbaas_org_id: number perform_insight_cluster_id: number status: PerfInsightTaskStatus task_id: number type: 'unstable_plan_insight' | 'index_insight' | 'hint_insight' update_at: number last_failed_message: string } export type TuningTaskStatus = boolean ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/utils/dbaasSecuritySetting.ts ================================================ export const DbassSecuritySettingImg = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAsLCwsLCwwNDQwREhASERgWFBQWGCUaHBocGiU4IykjIykjODI8MS4xPDJZRj4+RllnVlJWZ31wcH2dlZ3Nzf8BCwsLCwsLDA0NDBESEBIRGBYUFBYYJRocGhwaJTgjKSMjKSM4MjwxLjE8MllGPj5GWWdWUlZnfXBwfZ2Vnc3N///CABEIBZQJlgMBEQACEQEDEQH/xAAcAAEAAgMBAQEAAAAAAAAAAAAABAUBAwYCBwj/2gAIAQEAAAAA+uAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHybkek+yAAHO/OvrHO9iCB8eqfof0MY4buqBb/DPvYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc58J/SnzPt9HCX/e89wd/wB58u+p/NPoPzXf0VRT8D9noPqHzzq7j5fQfafk317h+a76q+HfcPm8X7Tx/X89TdV2HzT14+lcBRd10gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPhVH3P1f81fW/mX134f8Abfn3074L+m/zz9g/Pv2Xfwn0H5b+iPzd97/Pn6RkU/wvf9Ivfh31347+hPzd+jPnUD6Z8O+ufKPqvxr7Z8f+wfCf0T+evttV9JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACjqOy/P8A33xz6k6v4N+oqLf+eP07+c/svwD9TcZ87+l/LPv/AMvpon3Rxd8/Of2L5f32r6l+Y/1D82id38I+ucT9o/MP2TifuP5k/Q3IcbA/QuQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABr/OcmD9++YVED7v8mqYv6K/Ou+B9v+AfqbjOA+wfnb7Xc/m37n2zkvi0qZ99/PNtE+/fmn6RdfHftnxn6px3238v/pn892nMfob4ZdQvvwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMa/XtjJ5zkBjJ8/8Akv6ZyPPpjz7AMZxo+U9R8a/Sm7HoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHH3FwAAAHLUHY24AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGM4zgyYZAYyMZBjLGcZxlgMgxkwzhkxnAzjLAZBjJhnBljOBnGWDIYZxkwzgzhnAz59MGQwzjJjLAAZMDIYZxkxlgM4DIwZGMmDOMjBlgyGDLGcZMGcZAYZxljJhkYyYM4zjIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYzjIAAAAAAAAAAAAAAAMZAAAAAAAR93oAAAAAAAAAAAAAAAAAAAAAAAAAAIvD9Pu92WKG+qJ2iLa1dnG02tFa8t2SHMiwZfv3uzRzN9bd0N1B57sNfK9RW3amtYGKuzteIutV/A1W8aBIj3FDfcR22qstI8S2ob+gu9lfI1RZsTZzvZeqS3rFtRxOohwtEXqaC69w93iDc0UHrOW0T9HUV2q0pYdpz3Vwd3i0o7ygv6ex0x7bmNNpizqd9X1QAAAAAAAAAAAAAAAAAAAAAAAAAHK9JzmvreZuoUSLix8bYVPmx6bTUdMq4mlo3T9s/kvcL3se2es98rrufFxwfXcV7vrzdyk/bWVGfoFMqpdd1NC6ml118yg6WttaPpOdiWmmdA6bmnYY5fR4ke+g5XsudorPLo+Y81l1VJ3Scx2nMxJm3oOcobbo+Z8YsJ3IujhWVNc8+kWd/VSKOLaw+irboAAAAAAAAAAAAAAAAAAAAAAAAADl+o4r32VNEsee2TpkSfXQt0+Hac90Fo+b/ROP27YPTzqTTrWtHa1ELutnFTdF9Ycvp8TJy45SZb0MWR1FJModFvo86OspqXZri9VQWUKdQ3yBpmdTx0Lvq3nt273t1xOu4uNZe5kf3S3+uH7nwdHX8u6g4iN0UWOrumteVk2XPW9bMg2uKTuqedSp9Bf01xbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVax6TowAAAAAABVbp4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxj0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKHXf+wYZAAABXWIAAAAj7NgBCmgxkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABw3YcZ2nM9Z55+35v312qp09DRaui5yTbc10EXZJhS+e6OBBtqWB20Hgfo9DIu1fT9HGpOpq67pabTf0Ob3l7iwr+D7Wks7+g93iFR9Rx9/ZV2ylv/AJz9FrPF7ST5wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD5vZe9HccVs6zi+k9WtdzV7qc72XF9dXrXmt3jxp7Xi3Zcb3HA9+4u6jR7uwrIdXq7ep5vsYfH20/nOqj6Lixcbbw4V7zunsJ/EdnWUXvseK09JP5Xo+P33lF3IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxfRUsPt+I3dlwl3IuK3nbiNSyek08j28Pn7OEn13bcR77Xh+34D6A4y5xAuLLg+motXbQuY62HyfUSNHJ9ro5ru3GXSotqqd0GeI7au5p2HGdRU+4HRc1d2FL0wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVWtRZ8l0cvkry05vpK7lrLqOadHy0275K8s6rfJ98l0Wi1ic3cW6jtKN0ipgTttD11ZS9dSwOp5t0XJXVs5y4R7PnNXUK7n+rrt9RO20nW1e+Bo6WusQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADVCsgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYyAYyAAGMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjIAD5TK+lc50kHxQWl78zsuh0X9Xu9xJvvf4pb2nm1tN2wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD5R7ves43Vmy6Kb8xndXy+jx0FJZ648d1XOaZvS8R9HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcQ3a1c6yv6rie2hcKtqPp+T+i8FXfReabrikhfRQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxnDIAADGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADGQBXwzIAAYAATpwYhgAMZABgGZG0AAAAAAAAAAAAAAAAAAAAAAPHsAAAAAAAAAAAAAYyAqvdkMGcBkYAAM4q/dkOc8gAAAABbTwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUN8AAAAACjvDzzgAAAAAJN4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAChvgAAAAAUt0eecAAAAABKuwAAAAAAAAAAAAAAAAAAAAAAEfRPAaos7IABp3AAAAAABp2R9U8ABQ3wBoqLjcAGjeAClujzzgA2TfUWM9So3gAAJV2AAAAAAAAAAAAAAAAAAAAAAAhTK70lQ5lXdQJ1Zu1y4c6HvkRPXjXv8eYl2AAAAAAEHxuj75eQAUN8Act0FXth2VfYQ2xo21XQ1U+2AKW6PPOAGZ3jXugp8KRjZo368I4Ak3gAAAAAAAAAAAAAAAAAAAAAACq1ztOFlX7Jlfjd4hWkOVGm1O/3jbotK7xaAAAAAABVWdHZS62zABQ3wBzVtRaJi8pIEnf0XORZk+p3dKAUt0eOdAMysw5kNOg7JkaVEkYQgBJvAAAAAAAAAAAAAAAAAAAAAAAESXC8eJceVDso8hX6pejfpkR5fnRqkyq3zaAAAAAAArNWm1kgAUN8AU8SXp0TrCsjJNvzSw21ky6AKW6PPOAGZvv1C0pG7V69+NmpiMAJN4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI0kAChvgAAAAAUt0eecAG3PjwA2+PMqNgASrsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFHeAAAAAApLs884AAAAACTeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAornYAAAAANVPejmgAAAAATrcAAAAAAAAAAAAAAAAAAAAAAefQAAAAAAAAAAAAA07gGui3YAAAAANV97EaswAAAAAb7YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFfYEKaj78gBV2gAAACBtibLErrEAAIktVWoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYyAA0fLfrOKCj7yjqb6080N7qqLblLCyi7baiunO9M52s7Oiu9mNO7VWe7TRrl0tvt4udKo+y3PmPb29LstqCZZ0Ftvob2FCvXEdup7Tneh+d93OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOUtKvxb8h18GDf2nE3/L7ep5ezseY7blMXHLzb7dY1UCD0XKd24rdZxI91E0WnNdw4udpre3308atu4MmPK11lvzFrPmcn1Njs4fuHGrvbzXbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD57Z187reH6NVXdnw91pr+0421n8x2vG6b+FbV0DsK2LTdpw3dOP03XuHLhwOhjXri58u5y4jfUdZmTRdHGoLiLawanouY6K34fuHHdJW1+ntQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADzS3lPP5Wb03Iukla+UtPVzSXHOSrylteWsJVJfWOOT6rk7i3c+6DlXVcb66aLaKb3Y7Shvq7dQuo5Gde8rPn87exazrtlRSTpk3m7r14tgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADkutAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHKLa0zS3OcZ56r2dhnGVLNl0l5jHvGWM4zhkMZxUy5NHf01ntZHj3gZoI6dc44nt8M+fXn1jLGaC/1RJ2aO9xlipjb7mgvs4yw56/8AdBfM1m+VnLGcDOMjz6or2JGs2cZYM4yeYWm1wUV6y8+gAAAAAAAAAAAAAAAAAAAAAABzXS8tW7LJZRrzloW6/sOQ8PF1WxdlpRyo13WTOnpJVDq27LLdZcppLGDF7nlrGg29lz1jzvpYSIG3ouX0WcHqudg9Xx1nG3e4Uml6jnby/wCZ6aFU0vT8z3lFEx70LK7+fy7CJL6Xm6KymeFP77vnlT77Omr7GjSlhRSdUW8jT6HuqaBBdtWeKbT6mXPN6+25rO/TLgVtzUX/ADM7R1cwAAAAAAAAAAAAAAAAAAAAAAAcZd13vdF6j5x9Jcnr6vbB5fG9LkU86zoJ+2kvuc79807+ki+13t5Zt35l1HbU0ig8dm+a9lpgY8yuj98vp6WS4qy0aLmnt6zbPgQbei7vlLpAjedHZY+f++z51ZXfCztUmxt+FjWt1UKm26uq8Unvr6Lfim8yOr5qbBrJ0iZvoOvgVtd66/PznuKLRJ3QLm55i0i+YMa4obG0orDx0O4AAAAAAAAAAAAAAAAAAAAAAAYr7HXs8xKDrEfO8qZ+yvs6qd72QJettg2MbnOsrZuYFmqZu+HNq529D97/AFDoOrrJ6FP9NXv0a9muJK2VdrVWtdN14k662w21NvElvOPeraQPNjT2+YHmxqrTWbUPfFnqm10e9hjHuptqm10SFfMhWPnkuvrt8mvl+tmKq2xWzGduiLK3+NgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADVEAAAAAAAAAAAAGGQAMzwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJ8w1gAAAAAAAAAAAAAAebv6KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA57lPpgAAAAAAAAAAAAAAGj5B9j9gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc9y/0kAAAAAAAAAAAAAADz8a+x7AAAAAAAAAAAAAAAAAAAAAAAAAheQAAAAAAAAAKOh+khpigAAAAAAAAABulAAAefjX2PYAAAAAAAAAAAAAAAAAAAAAAAAY5ux3AAAAAAAAAAUFB9MFTonZwywAGcZwAM4M4ZMADOM4ZwZwZRofRgAA8fG/sewAAAAAAAAAAAAAAAAAAAAAAAAqpcoAAAAAAAAABz3CfVJBQX4AAAAAAAAAAFVvnAAB5+NfY9gAAAAAAAAAAAAAAAAAAAAAAABTW/oAAAAAAAAABz3EfSZ5QX4AAAAAAAAAAELRaAAB4+N/Y9gAAAAAAAAAAAAAAAAAAAAAAABT23oEWUjyEeREl6m0V0vcABH8abDOI0pGko8gAHPcR9JnlBfgjbdeJICNJAA81VuAaPW0BGkgAQdNoAAHj439j2AAAAAAAAAAAAAAAAAAAAAAAAFPbegg8p3NJUdTTeOl4fr6H3caLPnpMKzkxoMn3DsoXjPjoOYWUHo6ug7OhrOkrPGyfW3dPPi7rQc9xH0meUF+Dkp+rVe01nIg7LPVwH0Cs1WNNeUlpFbYt9QRJM6uu6jPnoXKSZ8Gx2aPWiTSSLmt9+kWbZhB12QAAePjf2PYAAAAAAAAAAAAAAAAAAAAAAAAU1xkOTi9HA0++g53p+aaJ0f10nNdLopLOnn19hWZ97peyy5rTts7PkNHRRYW2/oGvfmJb1U+8HPcR9JnlBfhq4ffZa/PR85E9TOn56sstF9QdLQaCT4dLW+Kr30POvEy32cPssr6gZVMyT0HOV0iXb85t68Ice0AADx8b+x7AAAAAAAAAAAAAAAAAAAAAAAACmt/Qc5f0cTTcw89HQx7DFXfTayr9bokmxgW3PT/HuXAv6Bfsc9d0sXVaxvfvbDs6ndjx0w57iPpM8oL8KSdWePW2HZ0llvt+b6HntXqTFsauxi6Jld19f4hTKydob7GFshRunoIHRV26pn3tBDle7nmZ1+ELRaAAB4+N/Y9gAAAAAAAAAAAAAAAAAAAAAAABT23oAAMc70YAAAAAAHPcR9JnlBfgAAAAAAB55zpQAAQNVoAAHn419j2AAAAAAAAAAAAAAAAAAAAAAAAFPbegAAAAAAAAAHPcR9JnlBfgAAAAAAAAAAQdVmAAHn419j2AAAAAAAAAAAAAAAAAAAAAAYyABW75YAAAAAAAAADnuE+q7ygvwAAAAAAAAAAKmTNAADz8a+x7AAAAAAAAAAAAAAAAAAAAAAAADHPTPZkAAABhkAAAAYUVL9KFdAmgAAAAAAAAABoj9EAADx8b+x7AAAAAAAAAAAAAAAAAAAAAAAAAjeTZrAAAAAAAAAKLnvpIeI4AAAAAAAMMgAGzeAAB5+NfY9gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHPcv9JAAAAAAAAAAAAAAA8fG/sewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADnuX+kgAAAAAAAAAAAAAAePjf2PYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABz3L/SQAAAAAAAAAAAAAAPHxv7HsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoOV+kgAAAAAAAAAAAAAAa/jv2LYAAAAAAAAAAAAAAAAAAAAAAAAKKldtmk2W6nuIUv0AAAAAAAAgfMJoAAAAAAAAAAAAAAGvb9QAAAAAAAAAAAAAAAAAAAAAAAADg8XjoeD9dFQVlrUdPQWdb1VB0nLdRagAAAAAAQ60AAAAobGp9VtjUW9X792MWgmy7eugJGrpLIAAABgAA92u8AAAAAAAAAAAAAAAAAAAAAAAAOIo+o6vz8pXndfP91gn8Xb9Hzl3SW3SAAAAAAAAAAAByXT8bomSoqN2/Idpw3nf66/hpsN3PKdaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACn3WTV4kQeZst9R02yvout5LqOX6KzAAAAAAAAAAADTyllfcX1HPTfcC7sqbhOo6ullVK2sYE8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiygAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1bQAAAAAAAAAAAABjIBhkAAAAAAAAAAAABjIBq0AAAAAAkbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGMgAAAAAAAAAAAAB59AIkgAAAAAHnaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEOSAAAAAB7AAAAAAAAAAAAADGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiSAAADGQxkA9gAAAAAAAAAAAAACAme0aSHjXA22KvlbkOZDmAAAAAAAAAAAFXunEeQAIU0A07nn1p3AAAAAAAAAIckFZYtHrOibF2emyP51p0OZ4849xJwPWQAAAAAAAAAAAAACq2z6jbpmxt8ayRo61zWezx41T/GhuxujT9oAAAAAAAAAKSbrzugyNe/Ra6IE/fUa5urfDsIWzxK8wN+JujTs8YsNoAAAAAAABDkhqi7PPvz61T4u2Mmx8avU2HM16NMvRNB7AAAAAAAAAAAAAAEGb5iY9e4vr3P1xI1lujQfO6dA9eN0+jmboU2NKkgAAAAAAAAArrCvsa/GiVjdJrdcuXXasyWNHqVrlwPGPdjU79XqVneAAAAAAAAQ5IQt8c9+PFjoxr8T4/nZuQvfrHh4ng9gAAAAAAAAAAAAADVGl5YxH3+vbW2NW0hStLZt1apMOZCnAAAAAAAAAAQ0vPjTIhTjVplZ0t0KbDkNW73qrp2/TnOPfsAAAAAAAAIkgDAMsGQYDOMg9gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDkgAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhyQAAAAAYz7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1bQGI3sAAAZGAGNU0AAAAAAAAAAAAAAAADGQAAAABjIAAABhkAAAAAAAAAAAAAADz6AAAAAAMZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDpdnRNG8ABDmQJ7znIAAAAAAAAAAeOYv5YBXWLx6iTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4Rc56HhfXY8Z0/PT/dT2XF9jx3VWHDdzz2afZu64AAAAAAAAAAOSt+V7jiux5ifH1Tm3n59R1CplZq+79AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4SptO69/JHU9fq5vq/mVpcUXUcp1F1w/ccVa0lzLtwAAAAAAAAAA5u452qtEayqO84rtuGjzXZcJPhu2oOjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABzG3okWHvg0l7RytkLoudt6e26Hg7CR7ouzouoAAAAAAAAAACr5zb0/KX9BIRbTpOY4y+66BprFp0VJdgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFJdgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHz76DznQ+lY5aT2biO19Q661oOm8ed2GfHsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcTZaobtavxzzuMchtStEOLYXfIe7GR7jdV6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOWm1OHZV0aH07nPdTvm6oMa26jgrady/V851FiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYz4zHlec+fbz6whyXnZ686d8aTFlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIWAAAAMmAAAMmAZAwADIxl5zkYyBgAGRjLznIwADJgMjGTAMhgGTAZGMmAZDAyGAZMZAYyGAZBgyADBkwMgMZGMgGAepgP/xAAZAQEBAQEBAQAAAAAAAAAAAAAAAQIDBAX/2gAIAQIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqsAAFubANpkNZqbwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF3z1FktZ1nWdJWsXNTVxrNsbzWbKk0Z0kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtM9M3O8azvnvO8zWdY3nfNdJNTXPpi3O8azvGs7xvLIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVNzWU3zs6c953zus6xqsKbzrLPTnpN41jpm46YtYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtcaMbMbXG+d1jphvEXcY2Y3JvGpcdOe1xowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGtcwAAA1N8wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAAFsgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAAAAAAAAAAAAAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAALvKRrOoudQmpvmstiGoudRrF3i5ahrM6ZQlsTWenNqLnWdSWWw1hqE1cWrjWVlTVxsYqaudZsTWdZ1CbRLNYAAAAAAAAAAAAAAAAAAAAAAAAAA3nTG8tEstTLWFtEjShGG0Z6Z3JhuLqc9KZ0xpZrNmsasGdMN1Gd89ahnTWamdY2TOt5zuWTbBZpJnU1c3NyAAAAAAAAAAAAAAAAAAAAAAAAABvG2NFJc6Imsx156LiapLLebZmbWRncmjGppFY1qVjUs1JTHRzu4hcbsQ1mkrG2DozVZm5neauWueppneNZgAAAAAAAAAAAAAAAAAAAAAAAAAAAABrLWQAAAAAABUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAauYAAAAAsAAAAFQAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOmN41mbzuYauNM6Z3kFizV53py0yustYusaZ0zvMvTFmdMrrG8yrOnOs6kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdZLz6Mbyzd5NZ1ms7iufRjeOnJuLmaaYbxdyawuY6Ztzq85056rHRmbxtnXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6Zt59HPplm6lrLfO6lS8+jn059eTplczpnTnd4bzLvm3zdMrNTDpzu2N40uN5msAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGs6mstZm8XcxtnUmsyjWUuplqamWrJrNuNXG5NZjeSbYXWTUXNluLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaZsKmoQAssqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANJLUjUlqCkqTWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFColjSXOpUBcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO+wAAAABz4gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9DzgAAAAD0TgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHs8YAAAAAPZ4wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9fkAAAAAB7PGAAAAAAAAAAAAAAAAAAAAAAAAAAAAABbkBbIAAWAAAAAAAAAB6/IAa9fkyAGsgA9njAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3nUTWd89Z3E1nUlErXMAAAAAAAAAPX5AD6Xg9E68O3Hok6Y9nzvTx8wB7PGAAAAAAAAAAAAAAAAAAAAAAAAAAAAABu5VjbG0azZc7RLm3AAAAAAAAAAevyAH0fJ7t8Xi9vfjPn+/tw5enHzwD2eMAAAAAAAAAAAAAAAAAAAAAAAAAAAAADWdVLnWNZbZ1LnUmkztgAAAAAAAAAPX5AD1d+N6cOPo6THl+g4Z9PDygHs8YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAevyAAAAAA9njAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD1+QAAAAAHs8YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAezyQAAAAAX1eQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL7JAAAAAFvjgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALCwAAsAAAAVZCwAALFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC9eLWuermNRZpmyak1lrXPUilZqak6SaxHbnnTOpNSai5dOazWevOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG5U3hrE6Z3Mbmd42zvMk1bjfN0kqVNc3SLiaazZYrWWd5y6c3SSb5gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADrnUxvK5m5WOmZrHRm50xbrn05ujKy3LLpJk6TWE1lvNzq51nPTm6Y1XMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAazrO2NsLqM6mpnU3lcxvOplpnbG2UakGs1pjcmpNRcNWJrNZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG0y1ka1Mwak1kAAA1JrOpAANVMunMADWVjWQ0Z3gDWWslQAAGs1AACoNZAAAAAAAAAAAAAAAAAAAAAAAAN43URndmZsubWdS5rGlGU3ZYvPc1MaUSzO0uNXO80suN5zvFusb56oTPSEzrUFc9NOernaJqVm53z1TFtJNXntLLc6zoxAAAAAAAAAAAAAAAAAAAAAAABuUZ6cm7iXaE1JrLWdc3XnoZbQmuepph1wow2zHSEsqW51z3laYdZjSZ6QmelzKsxpWNFMblsSaxbWHXnoJncVc6k1GYAAAAAAAAAAAAAAAAAAAAAAAFgusAUSosol1iiKJUUl1iiACks1miUlliwFTWVSxUUiyoCyyxSN4qWDWVEpLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEgAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABn5wAAAAAAAAAAAAAACdvcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4eb6AAAAAAAAAAAAAAAE+X9SgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcPN9AAAAAAAAAAAAAAAB8r6lAAAAAAAAAAAAAAAAAAAAAAAADpQAAAAAAAAAPN4/oBrQAAAAAAAAAATAAAD5X1KAAAAAAAAAAAAAAAAAAAAAAAAPdwwAAAAAAAAABw8fvHo1xAAAAAAAAAADfbxAAAfK+pQAAAAAAAAAAAAAAAAAAAAAAADvjmAAAAAAAAAA4PTk9njAAAAAAAAAAA755AAA+V9SgAAAAAAAAAAAAAAAAAAAAAAAHp88AAAAAAAAAAcJ7OZ7PGAAAAAAAAAAB01xAAB8r6lAAAAAAAAAAAAAAAAAAAAAAAAPT54DeGstZ3i3I78sgAa1ribw1lrIAOE9nM9njBvNZAdOYAF7+cA1IA1kADpriAAD5X1KAAAAAAAAAAAAAAAAAAAAAAAAenzwO31Pi+v1/M9XT532vk+7Pk1w93Lt5+e+/OdeHTVeL6N83bwen3fI9np+d6dZ49/H6uPTPnHCezmezxg+nw1rx+rz47Z4X7fxO+uPp8nr4bY6+T19uXLv5PSvifS5+fvwzub5+vHm7SOnLgHTfAAAHyvqUAAAAAAAAAAAAAAAAAAAAAAAA9PngfU6fP9G8+D6PzPoN8t58H0fna93m9XDvx9OZjnjh9HfPz+f62/m9u+PB77rnnt5PV5/IOE9eD2eML9jPm1fB7+jl8/3ejz78Xt8Hs6Jzt8HfXonh91vLy5+xnz+L3XL1cceH398c/L7s/MDpriAAD5X1KAAAAAAAAAAAAAAAAAAAAAAAAenzwPf4vZ134+z5/t6ee+rwcvT3Y68+HXz+7hrPLv4vc8J7vJ6u2/LuZnXzerFvzxwns5ns8Yern3Jvh7OHPze7x+zUx04ejhvpy7fO6Xtjvytxw6zrrxez0fP759PDyezrynl+hw8gdNcQAAfK+pQAAAAAAAAAAAAAAAAAAAAAAAD0+eAABfb4QAAAAAAOE9nM9njAAAAAAAC+3wgAA6a4gAA+V9SgAAAAAAAAAAAAAAAAAAAAAAAHp88AAAAAAAAAAcJ7OZ7PGAAAAAAAAAAB01xAAB8r6lAAAAAAAAAAAAAAAAAAAAAAAAO+OYAAAAAAAAADg9OT2eMAAAAAAAAAADvjmAAD5X1KAAAAAAAAAAAAAAAAAAAAAAAAPdyyAAAAAAAAAB5/D7x368gAAAAAAAAAA108QAAHyvqUAAAAAAAAAAAAAAAAAAAAAAAANaAAAAAAAAAA83k+gF1QAAAAAAAAABnIAAD5X1KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABw830AAAAAAAAAAAAAAAHyvqUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADh5voAAAAAAAAAAAAAAAPlfUoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHDzfQAAAAAAAAAAAAAAAfK+pQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOHm+gAAAAAAAAAAAAAABPl/UoAAAAAAAAAAAAAAAAAAAAAAAApFQAAAAAAAABj52wAAAAAAAAAAAAAAJr6AAAAAAAAAAAAAAAAAAAAAAAAAUSiyywSwAAAAAABnmAAAAUACoAgAAAAAAXpQAAAAAAAAAAAAAAAAAAAAAAAAtkaSagshUAAAAAAAAAAACyhLLKJRLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFEVLLAAAAAAAAAAAAKllENSWVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGrmLAa1MNSNZ1kAAAAAAAAAAA3MlgA1kAsWWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANs7lzVwtYbRVyolwAAAAAAAAAB0zU1EuLrM3c2axqWWxnQZgAAAAAAAAAAAAAAAAAAAAAAAAAAAABvFtSxm3WJdWZ0MdJNZsyAAAAAAAAADeN41bJJu5zuxKZ1jZM7lZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtgWQqLDUWSlzrIAAAAAAAAAGmVLktyqazqCXWZViAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFQAAAAAAAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABRKllEssWUIAAAAAAAAAAFJZQlLAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC2RpIs0gSyiAAAAAAAAAAAstgSy2JRLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAWFSiVKlgAAAAAAAAAAKlllJbIogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB15bwW6mHTEtmsWFlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6QYt05tgsz0kGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABuajFtw01BZjpM7xvEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAFlQLLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8QAGwEBAQEAAwEBAAAAAAAAAAAAAAECAwQFBwb/2gAIAQMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJaAARZQMrQliygAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJqBUWWWBKsqLLBLFBYWFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlbmyyk1mypZrOs6MqllmoSyyyyyqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACXNlJpJrNmpLNRNJZLKudRc2azZrOomgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZKjUjWSzUms25tSGsms1LKudZLDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASaAAACJoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACSlSwWCxNCFEWWWTTOpUsFzVioWXOkqWWVLAmksWKialSxZNSVYsJRZZYLJVhQAAAAAAAAAAAAAAAAAAAAAAAAAJZNQRSClQBcgGmVXNi1moaiWWNRLLLLBZY0kKmpBYBZrKrIsKiksLYiigAAAAAAAAAAAAAAAAAAAAAAAAAS5ahFCKS1jUFsCWLcqsilzSWxFJqQWWWVFuWpFC5FJUqNZtMrBUqWWVNQJSgAAAAAAAAAAAAAAAAAAAAAAAAAAAAEqUAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzZrOkRpFixYCwE0xuKixYsWLFkstioubYM7ixQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRrKyrJSWUQNZWazpmixGpKiypazUsW51I1lbmxZoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzY1lrNWSoqVBZrLWdZ0zRc2NSVKSppmpZazqRZZZZSgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEsubc2yyWxc25tis1ZLUFQTUmosXNqCxUmoE1BZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQixQABKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhYKiwABZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAssqAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpwaAAAAATn7QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOje4AAAAAOlruAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB5fqAAAAAA8v1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA830gAAAAAeZ6YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUBFAAEoAAAAAAAAAeb6QBxdDv8oAY2ADzPTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzYWXO82WWWVKlibAAAAAAAAADzfSAPD9bp64O30+71m3Fyef6/S7XeAPM9MAAAAAAAAAAAAAAAAAAAAAAAAAAAAADM1DWWsrJqWXNCxoAAAAAAAAAPN9IA8X0PK4u1fR8rr8+va8fg7HZ6HL7QB5npgAAAAAAAAAAAAAAAAAAAAAAAAAAAAASyLLNSsrLLKi3LQAAAAAAAAAeb6QB53V7XHxdzs9HhvN6Hi3tb6Pa9IA8z0wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA830gAAAAAeZ6YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeb6QAAAAAPM9MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPK9LYAAAAAx5/pgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjy+SgAAAAJj1NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlAAAlAAAAEUlAAASgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGNpNSWpUSiypUmpQRRKzU0Y1YsWLLFZ0llzaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARFlS3NixZrKxViWaZVKJpmxpEoJYqWs6ZVNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADI1mpbmprNTWVE1E1nTKhKrNUymiVKJZbnTNRoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASxLctGassWXNS1my1Fi5VUUSwXNualTSSkpQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASWpRI0EWUAAAiyygAIKzoACVKlCFlAlSkUAAEsUAAiiUAAAAAAAAAAAAAAAAAAAAAAAAlyUXJbkWFhZbEFXIqaiNRCpblVksohZbLEs1EsW5WWyKRqI1CKQUmojUQqNQIssNAAAAAAAAAAAAAAAAAAAAAAABmpZrOmWkgWLKlmmahUCzURpjSDTK1mxZYqWazSGmVi3ItyWDURqEW5qKTURpmpUtgixZZoAAAAAAAAAAAAAAAAAAAAAAAEok0BKllJRKllSpUsoDOpUoASpZZQlSgBFlRZQAlAJZRLKzqVKJUoSgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABecAAAAAAAAAARQBFAiicAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABzfttAAAAAAAAAAAAAAAXzPyYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD1ff8AxYAAAAAAAAAAAAAAG/oPz3IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHre3+OAAAAAAAAAAAAAAAv0T55kAAAAAAAAAAAAAAAAAAAAAAAAOCAAAAAAAAAAez+g/HBx8YAAAAAAAAAAa5gAAL9E+eZAAAAAAAAAAAAAAAAAAAAAAAAJ5Pc5AAAAAAAAAAPW/Rfjh0uPtAAAAAAAAAABx9X1QAAX6J88yAAAAAAAAAAAAAAAAAAAAAAAAdLm5wAAAAAAAAAHrd/8rynl+oAAAAAAAAAAB099kAAL9E+eZAAAAAAAAAAAAAAAAAAAAAAAAOh3dAAAAAAAAAAPW9L8f2DzfSAAAAAAAAAAA6+O2AAF+ifPMgAAAAAAAAAAAAAAAAAAAAAAAHQ7ugcPM4uVxcvDzYnITq8/IAJXFnj7VnFzOLlcfIAD1vS/H9g830gcW8zlAcPMABOn3QDj1oBx8gAHXx2wAAv0T55kAAAAAAAAAAAAAAAAAAAAAAAA6Hd0HV8L9N53ne50OL2/zH6DyuT0Mdvyefr9zm4epz66/b4Myer4ju9b1+j5P6TzPN93o532up6PQ7XByd0et6X4/sHm+kDwe3nj9Lo9vk6++3j8x+n6vH2uj6HQ7fDd8Po+d1ufsdXv9JPTvh83c6va3x3i5/O5e91dVw9jth18dsAAL9E+eZAAAAAAAAAAAAAAAAAAAAAAAAOh3dB4HB7HV4tet4/u+Jrg7nBv2PD9vHl9zodvqdno7vLzcnd8TG+93fz3F7HD0+X1fLznfJ1vQ6Pb9Met6X4/sHm+kGPznJ3ON6vlcE7Ps+V0e9x+l5fr+dwLzTPr9XHS16nlsdjv7/N673oeW3Oh2Of0vK6nNz93yt+8HXx2wAAv0T55kAAAAAAAAAAAAAAAAAAAAAAAA6Hd0Hj+p5vW4/S619ny+HtvO9fs9Ho75Or2e31O/wCR3McnL1vV8m+pZ5Po9DrcXf4eW663f8/lzn2x63pfj+web6QdDl6sb6/d87tc3e8n0vMxrl4O30u5wcfP1Pd4M9bm6fZzjsdngcHH6nndT1urvpdv0PN63Pvv+N2vTDr47YAAX6J88yAAAAAAAAAAAAAAAAAAAAAAAAdDu6ACUJ5PrkoEoAASksoHrel+P7B5vpABKAEsoAlAGfL9YAAHXx2wAAv0T55kAAAAAAAAAAAAAAAAAAAAAAAA6Hd0AAAAAAAAAA9b0vx/YPN9IAAAAAAAAAADr47YAAX6J88yAAAAAAAAAAAAAAAAAAAAAAAAdPk7AAAAAAAAAAD1u/8AleU8v1AAAAAAAAAAAOly9gAAL9E+eZAAAAAAAAAAAAAAAAAAAAAAAAJ5XPyAAAAAAAAAAev+l/HDp9fsgAAAAAAAAABxcXqgAAv0T55kAAAAAAAAAAAAAAAAAAAAAAAAOLIAAAAAAAAAHte9+ODPEAAAAAAAAAAGuUAAC/RPnmQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPW9v8cAAAAAAAAAAAAAABfonzzIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHre3+OAAAAAAAAAAAAAAAv0T55kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD1vb/ABwAAAAAAAAAAAAAAF+ifPMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAer7v40AAAAAAAAAAAAAADX0L57kAAAAAAAAAAAAAAAAAAAAAAAAIoAAAAAAAAAOz+36sAAAAsLFiiWFgAAAAAABycX4sAAAAAAAAAAAAAAAAAAAAAAAAIVFgCyygAAAAAAOftgAAACWazQsAAAAAAAAE6GQAAAAAAAAAAAAAAAAAAAAAAAAhULALBQAAAAAAAAAAAlgFlgsFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEqUlllAAAAAAAAAAAAlSygikUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlalBI0zallAAAAAAAAAAAMtEoAlAJUsoAAAAAAAAAAAAAAAAAAAAAAAAAAAAADKxNQqGmQSgs0AAAAAAAAABigGpLcyrLCyVYFoAAAAAAAAAAAAAAAAAAAAAAAAAAAABmkBqJbJLYLm2WWgAAAAAAAAAzZYFuVuRYWayLBVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIVBRKEBYLKAAAAAAAAABCoUiossCyWwKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQqLLBZZUsCgAAAAAAAAABLLLAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIVCiCypYKAAAAAAAAAACWAWWCwUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFBAWWLAsoAAAAAAAAAAIsAsKhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABnUpEaZ0iygAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzUaiNMlQsKmgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmw1CoiotzUstAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/xAA7EAABBAEDAwEFBwQCAgMBAAMDAAECBAUGEhQRExUwECAyNDUWITEzNmCAB0BQcCIjJEElQqA3F0eQ/9oACAEBAAEIAP8A8csZQn1eP9s0oSd2b+A8pRizylnNfm7sw4p9WaiWE/qFeqFaGQDZDZAM4fW1gG8+EM1LQocsLLvJZa+LD442QLiNd0cncFSB72TyVXEUp2bGR1/mrU2arHVuoRuzvpfXLXTtVv8AuFKwxzmhf1Lx5jCGzP1ZnWe1BXwIgTPp3VVPOcpgZsdnxN/iaZrZqGdA8P4D6smQWAyEh4cdU2Tpjtwo4mAGGPO6AMe/MtDTeOt4bERo287qSjgxM5y/1OyTz6iwev6tuUK9pZ3UtDARZjF/qfknn1Hg9e0bpI17l0/EpWbEaX9RrluzWrOs3rizi8lYpwwmSlkcLVvyb+pdyZmE+Z1+9S1OvQs6iPX0yDOPprWhsxk4Up621KZiX8IsRkp4i+K7DFapuZrDZK81D+pJy2gAMs7rk2EyR6IdPZu1mca1s6/qf1h4sUdCVcdZvne3cweLu15AL/8A44zIjPMQmlEUGJnNc0cdOdes39TclvWndWY/NQlCGsdQmwD1wQCRwlGVtK6ys5289Oep9RmzU4gJgNRGwE7Djr5mZdNPmnxGvbVzJVqy1NmC4PHRtwwWvWuczm2v6m3Jlfsad1vWzUmrmeTRZ3fL6/FWM4aOJ/qV3jMHIX/6kXKd+1XbF2mtY2tblmdd1KJZgqQ/qbk2+PTmsKGZZxN/vu3XDbrGrmzOh8tjiPOtWzWbxf8AwFh/6k2oTYeTFcrnrQODJ3i5fJmsExmM0PVpME+pKVCjf/8AjdKZaVzTgTnvXLGWyBbBMdonBVqA4WczonJgyJmxwIXYaQJC9ifquO9mr/1DkFpL9OUVJ3abu2H0JjGqQnf1fUDR0pKsDQP6kAteYfHtirt9tK1a13N1Q2KWKx2OEQNPPUXxmYtgbB3WuYavdnZIbLZUs1jgRoUK1Qa1PpwGdouNX8BmsQTdOnqvP0XZm03roF8zVrmrr08XhbBx4KnVv5IcLtrHaDPUcEa5p4nKwIDLY3GZihC1bqQiS1XhKjg8XQkxav8AUPEY7GtTnU0Li6GSNea7mKteppnKBBpv67jV/Ur6IJacxPmMmOo+Y0Lg3xx+HjDTrZKmSGtL86WDK48BifM5UFR7WiMGepMQrA5hOURAd37Hw2VXA1sL2mDoTLU3rgxWg3o3AXG/33n802CpNbfAa4Blrj02t4vHXYNG3q7FU8RlXDU0G5rWmr4ngNpHgMj/ANMXaDTaf9NYCfZPF4B8Rgclj4UyMC5WLIU4zFCcbmrMFirLgtmuVslpy3aq4ycRZKgScXZ4s7apMM+eyEx6R/TdBN+cyF+VBa7/AE6daA/UgFrb9M31of8AUlD2f1OxvSdPJNiM89bReYqLQWM5+cgV/ZntXxwN2FWemdRAz9WZIZPAYTKhJ37EGrWzQHq1z29EUDTwGIhmr7U5T/pe8Pxb+mo3/CYOLh3AqXzlVQ+CC/qj+Vjl/S75nJrUv0TJrTf13Gr+pX0QS/pv9fdWvl7CrfPAX9Qh9cBVdf0+swBqETJ3aMXlLJnjZyN08MI7Nh6PXL6IxV87lpXtDZmpCZYYDUN/E3ARaDtKMZN/vrMYsGWxx6RclhMphLD91tW59m2qpjsrmrXUeCxIsFixUoaw0nbq2y36lDWeaxoGrR6ZrU17esDj/DYsFJtWaVtU7ZrtOlqzOY+s1YNDF5XUV53ahjR0sUGg2dwVvCXCjnSzGpLUIUKmYx08VdlUJpL9N0U35zIX5cFrv9OHWgP1RVWocZ5DC3KkISvYe7Ca0NnTZWnanb1TjoZHD2asesmZ4r+ndGFLEPan7NbadfN1QnBGWWwdl3a1qfOWxOE2A0tey1gciXMZWt40lAuSw2W07daaL/UHURQuJYDT1/M3IGK4YdntLK4q5hrpAH0RqfJ37xalzX2HNfxwZ1sdlsjhDknUfIkymhD2zab+u41f1C+hQX9OvrhVbbpUsM1b54CyOPDk8ROoXJaezGHsuz1j6vzrNTWRqtSv26rU6ccjpQFSdyhl9OX2nKz/AFCz9mu4VprTd3J3AmmzNFmZv99kGObbZyw2IbrKQxCE20fsLiMYaW4gghDHoJOzP9zlxWLm7kIMQhR6DRABLHaQNSrX/KlXrzfrOMYwi0Y8Sr7CQhNts2CCH/JkajTsfmiACvHoLNa6q0DW6Y6FKxl8iKuKpWHRqhrw9pqtaw3QsMPixvugzNFujKY4Ti8ZthsU0t7RjGLM0UesCw3QoadSr94kbG44jvMrCHs2JgVY/fEg4EbpNxBH8CerWf8ADK2bdDG2LVap/Ua3yoNcLrzAQC5IO1jMZKbwoA41KsFyDGSPSbYbFM+5oxjFukf4J2v6dYm0cppYfAY3DDfjf2+T0FiMkTupv6ZUYO+/DaZxWDbui/8A18vXl/6evNceScElx5rjzT15pwSXYfonrzT15pwSXHn0XGmnBJOCScEmZceacEmZOCScEot1dwSi3V3BKLdX48lxprjzTgknBJmTgkuPNceacEk9eaevL/09ea48k4JMy481x5pwSTgknBJmT15rjzTgkuNNceacEk4JMy7Eui4804JMy48k4JRbq/HknBKLdX48k4JJwSZk4JJwSZk4JRbq7glFur8aacEl2JdE4H/9PXmuPJOCTMuxLouPNOCScEmZOCTMnrzXHmnBJmXGmuxJOCScEmZdiXROCTMnBJmTgkzJwSi3V+PJOCUW6vx5JwSTgkzJwSTgkzJwSi3V3BKLdX4004JLsS6JwP8A+nrzXHknBJmXYl0XHmnBJOCTMuPNPXmuPNOCTMnBJmXYl0TgknBJmXHknBJmTgkzJwSZk4JMy48k4JRbq/HkuNNOCTMnBJOCTMuPJcea4804JLjyTgkzJwSZlx5JwSZk9ea4804JJwSZlx5pwSXHmnBJmTgkzLsS6JwSZk4JMy48k4JMycEmZOCTMnBJmXHkpBlBceS4004JMycEot1dwSZlx5LjTXHknBJceScEmZOCTMuPJOCTMnry/wDXHmnBJOCTMuPJOCS4804JMycEot1fsSTgkzJwSZk4JMycEmZOCTMnBJmTgkzLjTTgkzLjyTglFuruCTMnBKLdXcEmZcaa48lx5JwSXYl0TgkzJwSZlx5JwSZk9eX/AK4804JJwSZlx5JwSXHmnBJmTglFur9iScEmZPXmuPNOCTMnBJmTgkzJwSZlxppwSZlx5LjTTgkzJwSi3V3BJmTgkuPJceScEk4JRZ3d6809ea48k4JMycEmZceacEk4JMy7CevNcaa4004JRbq/YknBJmT15rjzTgkzJwSZk4JMyevJceScElx5LjTUguzJwSi3V3BJmTgkuPJOCUWd3cEk4JRZ3fjyT15rjyTgkzJwSZk4JRbq7gknBJmXHknrzTgkuPNceS7EuievNPXmnBKLdXcEmZceacEmZPXkuPJOCTMuPJcaa4004JRbq7gkzJwSXHmuPJOCScEos7u9eX/p681x5JwSZk9eUW6u4JRbq7gknBJmXHknBL/9nEpwh038uuzKBgdWZv8ADSlGDdZfj68pRizvJnZ2Z2/v5SjBusvQaUZO7R9spxg3WUpxg3WXpNKDyeLJ5wjJovIgxfHEwX+6PvNOMndoqUowbrL162Ux0yuGAjVy79n+obWIqZWEGt5jCY+ndxggh03iaZxnBbzuNoFYZ6OVo5GP/jIhIChKc/tRiuu1DKMoozHYz+MpFcRauUp5MTPTNkqday1coNTYchIiazZDUDM5rGpMcFh7qt6tersWtdydLHRZ7NPO42/PtBnOA4SnOWqcU89rmv1KlKV18BqQbEMKz7Lt+tj4wnYQMlWtksBrWc/jKJXGWlkqWRG71efWHkA0iWbAqoCGLXsCPXGcUpNGLyd9R4mFYZnpZWlkYu9a1crUhd09XUuHPNhN1bp1U9S4gBXhKldrWR94V7LUcd05GbytLI4ay1fHfL11fugx9d5k07qKHUobVy9XpC7titqbEFlEftPqHFVCuIlHI0cgHuhvZahjujWKWax+Qftg1NlGogcAcVlamRA0Ru/RndPqQMM5GbahOOzptzCHn8bRq1Qmo3Kd4LFDdylLHMz2aWcx1+XbCrdwdMEzmfUOLgYQVcydLHMz2amext2bCHKUYReUp6pxTO0HDarmAxhQ1MCGccrhsCMAZoWbIqgJnMXUeJrbN+Ov0cgPeC5k6ONjus1M/jbc2AMhRgHIpW1PiXntTEH2+4p6mxUJ7V5CmKlzWrWQ2a8Dip5OneKcQJZOpWsgrTuZKljotK1Uz+MuT7A7t2vQB37D56lO61JrmXoY6UWs5vK1MjiStXrTbhg9hNSYkBXg9G9SugYoSZugJ7LEp57G3idkJjiFCUyfajFlm0Fp2UefnX9l7J0sfBnPV1DjLUmBDWn0sa1V8hTTkgMLTm+qMTv2qVqu1blQp3q9uoxgV8pSs2T1RmydWmYAi28hSx8WnbqZ/GXSMIc5xhF5PPVWLlPY+JnGWoctKKy8dmpMOtUCgY2LFMukcM8X6aesHga/j7Cu5WjjtvJqZ3HXiMERCDDCRCC1Nh3JsWnnbyGddXcxQxzxaxncpTyOIJx7NkFCkOyd89jWsQrK9laOOZns0s3jr8u2GwcNYMimDqfDPNoKZBwG5JfafENPahlGUcJjPqLFUyuMtW/Vvg31bmUpY3a9qnm8den2grUuSDVpFrLTk8VOAx18NDFQfIvTnqHFiDMkg5KlwmuoWqMM89qy14VCk5Z6dzQ7QK9Q/wDp3UbM2RwStTkGqUy0xVG9Od0mdFDHZXG3a61Q85eOpM1Gq1fjM9XweEuOHTdMQscI73RRx2fonr5GsKzqioIupK1eeKsO1x3No3c+BqAFjK3TAxjXz2TrwDfq+avWbebyNW/CBKuoTzMDHV0KhUjW40aNAWPrzrj0qw2Dl3f2Zuo17GmC1TKxfTLXJ4sMsdpo9paapihQHZe/AeM1DjbAMh+rqCz30e8sJ9KpKz+VNaRqhag9hyihW1WDs5m6GefjC1k8njr1MgVcvHhpITyo5XF06www02VvM24hwooXcnkLpdXVASqNZjj/AJaupMzxdn0szdi6rAo3tTMA2XpAt48o303ZlYw8HISO4c4tjbkcIxa1/ENhpue3j8fkqjXr1u1lL4LRap6mqej4NptReMawHZEjH7VC66pZmwhViKgAY6vFsAOIszl6sAZCq2XvWbeXyFa5ERKopuQA5vqWLNgLbrCVAgx9Z4YsLZLN37BtT0hPjWsxzFwtnE42Kr06wKsK0MDHi5m/QjCEPtedvZqL6PdWAphBjQyXahR1QCIMjPDVb0LNvN5IN2FaYdRVj3cSKAB5jEFrtUvXMf3cEUFKjl6VSpCneo1KD44oquKuPTxGQFLE13xV7Fzel0t6jvWVQHDI53IWDamoiljXsNnyOfTFQ70qlepXCMWBFC5eyF8urKYnqwtNSfpTru9iI3rlY1fK4CgJgU9ME3ZjJM2MqiPqLKkJqiAhyo2x6mk5fF01w6zV+KtLC7FnKh9mUnh69sNi5nsmG8CDh1O+/Bgdaq+QprUk5zHQqNClUHW4zYGDAu5THrC2PHBytWeOFOifFX59Gt6mRr1bz9strNZCnfrdA5yyYuJowQKNYFWFeOABGrm8qGKzH6nwy1UTtFxk2fP5CTMwcJjbFTk2riulwlG/3j5jIjtloFDqXeZ8bSUaNXjPWWkgsC7khLAijbvZC8bV9QL1oWo6k+gTWKqArUgMOrkarZS9ZtZjIVrnZNU1NXPkMfTJX8vhbYWqXMzR7+GatSq5jHQrRp3TsCjp45KeGyeOo1BM+OsjfPdypkYGpZmN8gbWAytoM4rVf0gyxrRbHUemm/z8ytM1AzLkbEtTUjHohavHM4S3VencOAA8OWAdHs3hgP8A6e1EN3yGDdjQYo5jejbJgHJSubjZ/JVSxWpaJLYKx6zarFEGx6YLVzEGFkaGRJhBvQvgY+cyoLj24y+11SazzO+IuMxoy+xXRYlnbGU2fGQdtT5V0Tv4DJ2bCfPEtlELG6hozuVK71x6qFADNPFTvzquS7i7TYi9bqWb2UhjiVIv7LNUzZQmIiWpA1Monx+RnhIPRvi72cy1e4tQDNWyVPJV8plC5XHHFVwzO2LpM5/yiLSLSHh3Z78JPq3HyWZq2cfkg5QJdSiKJh0MlQLk8M9YlXUEK4YAv4m7duOYhN5tPZO0SWdvlytN2qY6LccLO6xFrxVo1K1mQWKGWhk693PterPVoYqq9CgKuib9ku2DUcRRIDKYCuWV+7eaEy6cvWWLHNmvWABx+oa07WKIIOGyor4Igksz3MdnA32zx4XdOTKHHt/4FGD45umoc26m5tP5KyZ5Z0l0wRY1lqNnliLjNjGdqFVnL3cBmD2Vkb886ONCjlcO58QIdYWqhDAw7GCqWWNbyNrKTli8+96QbELVYRoai+j3Vi8x42mAF7HQPk8s+SLYK2Mz1i7ZzeRnk64eNkbNypTGaldzVK/TmBqQcjQ080BPqGqau47Omq1inRLE2cpHhmp1w6krShjq5w6aBMON3lM1jA5U9psjkZ5sbUaGpKjgwAQjF+VBQIfTWTtOTOXy5Sr/AONT+VAstXLax1oIsfmqlKoKrLTnIlnMiU+IjJs3mXfVY5SpUemoKM7Faqas2qoTCw46VgYNjMchXHbHZyd2zm8lPJU2jV1HCUsHXZtTwlKjTaOoKM7dOtOuPVQYibfgKZw8q7a1JWPDKda2cpwbCtEOlBTjWsXLFwdjB5Yt5i6hc7DDjM9SJfxwoDBqZmrwCbTcbLZvIktLIQd9R4V1qWDvZwzx9rkjiczcPbzN4uT4bh1Ljy2K9Y9RtTMUMYNpoRx2cmxxkPprJWu5nbpspV/8XUkZPp3pGp91Oqyk59O5KySfnCXTBDjcwe7SAA9bI5enk6k64oQydDBAYJc/Ss1nGXEYsz4ElG1Ryz4kMaORxmRt37U5Nay9vG3HHZtSHmclSnQWo4O+GOzY76dRitNf8J5zdpb/AIBym7MHvUwCPWv5inkak64qFOYcGOmbTl5qkIYuw+VIHLipv/p52Z26P06e3a3Xr7HjGX3P77M0W6Mtrdevs6MsjjynyuPM6vys16kiAxVK3K6fJ3U8Wl+PoNGMfwTxjLp1TszpmZm6N7OnsaMY/h7HjF+nVOzOmZmboy6N77M0fub3Gi0fwW2PXr7maAWxjLQQ44UxUK4yJ2Z26OzMzdGW2LO7stseu73mZot0b2OzO3R2Zm+5vZtj13e70Zn6+12Z26OzMzdG9u2PXr7zM0fwW1uvX0HZnbo/4ezo3Xr7HZnbo7Mze4zMzdGTRi34LbHru9jxjL8U7M/3OzMzdG95oxbq7Lo3/wCWp5wh9z8iK5EUx4v+Hfiu/Fd+K5EV34rvxXIiu/Fd+K78VyIrvxXfiu+y5EV34rkRXfiuRFd+KY8Xfo3finMzLkRXfZd+P4LkRXIiu/FMeH4N34rkRTHi/wCHfiu/FciK5EV34rvxXIiu/FciK78VyIrvxXIiu+y5EV34rkRXfiu+y78Ux4u/Ru/Fd+K5EV32Xfj+C78VyIrvxTHh+Dd+K5EUx4v+Hfiu/Fd+K5EV34rvxXfiu/FciK78V32XfiuRFd9l3V34rkRXfiu/Fd+KY8Xfo3fiu/FciK77Lvx/Bd+K5EV34pjw/Bu/FciKY8X/AA78V34rvxXIiu/Fd+K78V34rkRXfiuRFd+K5EV32XIiu/FciK78V34rkRTHi79G78V34rvxXfZd+P4LvxXIiu/FMeH4N34rkRTHi/4d+K78VyIrkRXfiu/Fd+K78VyIrvxXIiu/FciK77LkRXfiuRFd+K78VyIpjxd+jd+K78V34rvsu/H8F34pzMy78Ux4fg3fiuRFMeL/AId+K78VyIrkRXfiu/FciK78VyIrvxXIiuRFciK77LvxXIiuRFd+K78V34pjN+Dd+K78V34rvsu/H8F34rkRXfimPD8G78VyIpjxf8O/Fd+K5EVyIrvxXIiu70XIiuRFd+K78VyIrkRXfZd+K78VyIrvxXIiu/FMeLv0bvxXfiu/Fd9l34/gu/FciK78Ux4fg3IiuRFMeL/h34prEXfo3Iiu/Fd+K5EV32XfiuRFd5uvRd+K78V34rvsu/FciK5EV34rvxXIiu+3Xou/FciK5EVyIrvx/Bd+K5EV34pjw/BuRFciK77LvxTWIu/RuRFciK78VyIrvsu/FciK7zdei78V34rvxXfZd+K7q5EV34rvxXfiu+3Xou/Fd+K78VyIrvx/Bd+H/tpNL729Ij7WZmhBo/wnKLtf84M7SZn9L8T/AMKa/wAD+k3zMv4U1/gl6TfMS/hTX+B/Sb5iX8Ka/wAD+k3zMvdPqbF0rBAHHq7CuqVytah3A+9OcIR3TjKMotKP9wPK1Xvzot7IFFKTwj/cDKIjPsu2xUwTOSheFbrDMH0aeVqnvWKkPeYot/b/ANpV/gl6TfMy92qMZdX5GBJY3Hli8XxjPi9Qmoit3K9ALlO+r6W5UMjTyYmKE9kFUUiFlrWlF2aGMylLJQeYdV5UbAJQbTueB26dL2WbMawCnm+q6EKQjtS1bjyk7Umfqrmo8dQK4pV9V0LE4AdWtT0KxXFGhqKhkJ9gZjirjkUzawoQ+Gnla+VB3a1PM17F6xTjkLosbVeybGZKrkKTWAWMxXp3K9Sdu5XoBkU7ayx7Oq2Sr5IDFrX8tXxxADK2qKPMhWFeyFbHC7tgWsMc8maVUsC6tNMav3IUKs7E6GcDVzF+69OyK1TGeFvU+PqlcSpakx16fYb2XsnUxsGnYhrGgztuq2Q2hRMO9kauNF3bMNZY7r0cBQWQMQV/M08W8XODWGNnNoEhOE4NOF/PUMZNoFqaux5JMKbOzsztc1HQoG7T0dS0LxGrsUsAQkQj6vx8HZoUMrTyY91dZS/DFgGUmn84DFvc36geE9PGm1DN1MZh6ECUdT4+8RgMrmpsfRL2lR1Ljr0+y3svZCrjhd2zDWWOaXR6doFoDGDgP1NlkXL161+vSmq+YrW7VqiK9kqmLF3LMNY4/q24l8EaErQ45oDZ2V9YvJivVWNC7qLH48rilT1Rjrc2EjHiARCO+sMe1aJ1i8gPI052G/2XX+CXpN+e/ulyLY7U1+wiazkFurYGhcJcLlLmWi+Uz9WhNhV4i7Uaw2wuqOOHU2+1k6GLYFYFYMQitCbC6kqErau+kFWD+lUfZl/plxaRpC4z256hxoL2OKRaYtcrEQkQVXA4c5Tz1NlsTfqwHXrPOxp8S09kqOMgWtcJUw2SsCvB1dXKXFCcWOzuEnSFXapjaNIpT0rzvjtVU7UNSs57ONxy03/4drI45Un8hqezZWWZ8pn6uOlEAYi7TVYNh9TxALV8O7Yx0ECpVphiEFyLZjVPYJKnXcDiJhq0KuqDA9uG/UmYWorE6WJKWGnMcGrQEVaroBem14WKsPbx9Y8lkcXiCWh3LtrO6fatMRdFF/6rSAKGY1HZewWjXOFwlxWLhiRFFHAghlb93I2LtGtfrTAepX8PjZjWBuYyBLF2/mchgcjSLBaZLK3huj4W9VxFu4K8Wrh8w4TR1OAxsUWIsHnMLCmIEq+NxtY07tL2aL+O+tR/SLa0vSCHGhOtZAGN610WcuNVw5TQ0/dwWPpxmXUlzDXAMerjTytUKp5LtQy+pTDPOtWIKQSYvERw7HjHCfqLMLM/qzGezB/qLLrUL8fL0bdgeSwGVA4XxtAFILBDDo2tZrJnerQtHbT1vDVASsWtQ3cFfpSkKhZe3piZpaQo13rEtTZmb8P9l1/gf0m+Yl7tH9ZZFZmgLI0JAWl78jAnRPk5Ni9S1rs2nB472EVslqqBw6m3UszRyDhMIwolFfLHK6kpBBqZnfD2FgSRJh6XRZb6ZcWj7g51J1Xz94VPHG3YBjUMC7tpzG08mM9u5qeGOp0OwHGHiDAhJNvD54DGWSqjweVoyx1/KVccIbnPhMRej3VgN1bL3KIdW13PQ5DYY0MvnZXXzhPEZ2NxaTA8KBrM8rNsZqetdJGcJRabQLHK6ngUOqfnMV7DEbD6scxZlhGDkliLMbmqDmj7MXOMNV5aK1NUezh5xhpy6C3jYrVdwYqD1WxNeVXHVQyVwQ8nqbiWi0cHjgTO2iX3PddUixxWpLYzkKII5FJisxWygDyFpc0Mdft449y2GkCZzAsizGPJMenauNKx6lu1S05SBI1mjOg1Jj0hExGfFOSz+OrYQ9Wxj7WSq48Aj27GJw+RgxmwLvj9QkpB9mjJM1i9BakbphTrTv0aitZ/IAWarcrBThDTtPD5CmFpZEGmsdGEjV+wMAogQyQxeqLDmmQQxuSeLzNbJMZw4b9RZhZ6bVM9jLMyWqoQ96emTtYzeUOz5HHc1sdYyun8RxymjpGyUuKKxnnEOtneeVBK1jrIY6Zq4i7VYR7tbTVATms7KjYM0qujXZsQb/Ztf4Jek3zEvdHhQV8me/FS0/XHkPIwyOKrZEPZK+lG+CNDHVcWLtVrmPDcC4TPpSMX2ixuJqYyEmCYITgkItbTQqF2JhLL/TLiw2Fr5PDBm49JhGWJLTRZotFrWlq8iOeo2kqLVpwIGrGrQHVRdLhYsyVaemqtA7HJfxde+BwF+yu3/jDG4mrixvENmmO1WKCeJxAMQMkB5nC18oAQy0wDqAGGORxla+DtG+yjfBGjjauLF2q2SxNfJSrFmrmIrZYGyxDSsWkzEFg6lS9zA+zI6cp5CffenX4VQdVW9LVZzc9WjpurVM1gvsy2EpZXoVhaUgEkCWsViA4pjdvJ4qnlINE32UaTsxKdIFIMRjy2Eo5XoVD0oNyM9gYxhHEY7+naV9++m0kHdutQGMIoiha0tXkXv1KemaoDMc92gC4GQDfZXZ/xDjMPUxcZOL2X9LUrJXsCPjY2sbOsWjUhQqBqwy2KBlq0BkjBowaCu6WqGn361XS9QJWNY9mSxlTJi2G+ybP/AMZ0qIKQYiDWxAal+3bhksZVyYe0aGjqgJM5amGBRvWbQsthqWWZpv8AZVpv0PUpgoggOGXwlHK9CvjKL46rx1kdMVLJOSIWkgDIxbRgQLWJXbFUIYqtKuL/AGZX+CXpN8zL+xuAa5SOJ8TR8ZTaq38Fq3wP6TfMS/hTX+CXpN8xL+FNf4Jek3zEv4U1/gl6TfMy/hTX+CXpSYnJk0Oh10sLpYXSwuh10sLpYXQ66WF0sLpYTNYXSwulhdLC6WF0sLpYXSwulhdLC6WF0sLpYXSwulhdLC6WF0OulhdLC6WF0sLoddLC6WF0sLoddLC6WF0OulhdLC6WEzWF0sLpYXSwulhdLC6WF0sLpYXSwulhdLCdjszrpYXSwulhdLC6HXSwulhdLC6WF0OulhdLC6WF0OulhdLC6HXSwulhdLCZrC6WF0sLpYXQ66WF0sLpYXSwulhdLC6WE7HZnXSwulhdLC6WF0OulhdLC6WF0sLoddLC6WF0sLoddLC6WF0OulhdLC6WEzWF0sLpYXSwuh10sLpYXSwulhdLC6WF0sJ2OzOulhdLC6WF0sLoddLC6WF0sLpYXQ66WF0sLpYXQ66WF0sLoddLC6WF0sJmsLpYXSwulhdDrpYXSwulhdLC6WF0sLpYXSwulhdLC6WF0sLpYXSwulhdLC6WF0OulhdLC6WF0OulhdLC6WF0sLpYXSwulhdLC6WF0sLoddLC6WF0sLpYXSwulhdLC6WF0sLpYXSwulhdLC6WF0sLpYXSwuh10sLpYXSwuh10sLpYXSwulhdLC6WF0sLpYXSwulhdDrpYXSwulhdLC6WF0sLpYXSwulhdLC6WF0sLpYT99mXSwulhdLC6WF0sLpYXSwulhdLC6WF0sLpYXSwulhdLC6HXSwulhdLC6WF0sLpYXSwulhdLC6WF0sLpYXSwulhdLC6WE/fZl0OulhdLCfvt9y6WF0sLpYXSwulhdLC6WF0OulhdLC6WF0OulhdLC6WF0sLpYXSwmawulhdLC6WF0sLpYXSwulhdLC6WE/fZl0OulhVvgf0m+Zl/CkHwP6TfMy/hTX+CXpN8zL+FNf4Jek3zEv4U1/gl6TfMS91igb8YyjJusfQcgoP0lCcJt/x/sGlF3eLfwHA/QcvSb5mXu4PA0c3k8+9zNae+ztN8tiaV+J8ZXuTfWmAAV4PVsVrIInBfz2IwrtG3itSYfJNsq6vzsMZQkIOns5RzNauCCuYurl9aWatnJ6Hr0qpbeM0jk55fEitFPrDCUiuKeOyVDJAiWnkM7isU8Y3MTn8Rk2nGrkcpQxQ2JcxmqsJfIwAyyVEGRDjzX8nSwtdrV0FkNiuOwPF5SllBTnTLrLT1OfZnRv1MiBj1Z6hxdI1oRcTmcdlKxLVb7a6chNhLCkYuus3Mf8B6/wAD+k3zMvdweTyuPymf4F3KZrVVtsFPWLzo4LF46tXw2Lo0YUm0t1xWdz2HhpCsDK2cll7msqAqQqmXqayYZdLnKsHEIsPjZQV/JviNcWTtcz+WyoCVMbhcPDC4BqJqmW0dhIPVqaLswnqLMMC1PwOqr13JYyelstk2yFLUwjUs/QylgVzSGobNZ21mJ6fh8xDVAh5rLYXDjoZidXQ1xiWmNgtBCgPB4fG47EgCsXBsFra1jghx1a/r7JMfWj8LDBBWo4bGUscKqtMVB43VefqR/gPX+CXpN8zL3dEfUtTLWVA1SdHP0s9SfUeBr26NfXuLHV6XNK07hLWSzV+lcnonJX61vKX31iepjsVquga1pyxUBpXO1MpVr472VW6f1DtezPUy2MHka4MFqjB4jGBpl0uazZ1RmT2T6olicmermg9jN6po3cNmM/bwWTHyMzZoamuUB4LN0PI4K1VWhWs3D28jZy9Cy2qp4mGosS+VwJaYsbrSnj6rVMxgA28rm7efs4v9fZlaxxhsnjGiCjrmgKnANzSj3H1PnDXf4D1/gf0urNYk7+/sG8mlJOzSbpKMYxbpFdG6u/uvAby3eycYzbpJmZmZmdmdujxjGDdI+5OEJuzv7dkN27+BIPgf0m+Zl/Cmt8D+k3zMv4U1/gf0m+Zl7syRG3V+6eXwb7S32VvtLfaW+0t9pb7S32lvsrfaW+yt9lb7S32VvsrfZW+0t9lb7S32VvtLfZW+0t9lb7S32lvsrfaW+0t9lb7S32VvsrfaW+yt9pb7K32lvsrfZW+0t9lb7K32lvsrfaW+yt9pb7K32VvsrfaW+yt9pb7S32lvsrfaW+yt9lb7S32lvsrfaW+yt9pb7S32VvtLfaW+0t9lb7S32lvsrfZW+0t9pb7S32U1gkG6ShOM26x9G1kqtR2iTzMv/XmSLzM15ia8xNeYmvMTXmJrzE15ia8wReYmvMETZma8xNeYmmzE02YmvMTXmJrzE02ZmmzM15ma8zNeYmmzM15ma8zNeZmvMzXmCLzM15mTfjVyFS31YfuntDA3ReXmvLkXlyJ8uReXIvLkT5cq8uReXIny5F5ci8uReXIvMTT5ia8uReXIvLkXlyLy5F5ci8uReXIvLkXlyJ8uReXIvLkXlyJ8uVeXKvMEXlyLy5EHKBl0jNnZ/wB9V36Dk7+i3zEvck7Ri7oUH/Mn+0jwYDsWDP1b0MnbnWFGAqWOHVjvl0ZdF0ZdF0ZdGXRl0XRl0ZdGXRl0ZdF0XRdF0ZdGXRl0ZdGXRl0ZdGXRl0ZdGXRl0ZdGXRl0XRlkMdCwzGFjbb26/WftMRgilNSk85PKX+Dx1j8Qz/fIHZoSf0m+Zl7lpv8Apkm+5m/aZPvHNA/Kh6B2aebqwf8AwlL/AIZbJwb25X5X/C0/mRfvqv8AA/pN8zL3Ln5D/tSfwTQPyYehLo2dD/ha/wBbynuZX5X/AAtP5oX76r/BL0m+Zl7lz8l/dsWRV9u9slXdQJAkWlD3YzhJ3aKMeAIb5+SrIJhHj1h65jQBFpT/AMTeqz2RKOleiWG0hbN9iPGLZGyP4w2w2G6e7P4JoH5MPQl9dD6R7Aaw5EK2oqG7ogHDYGxBenMwxPFp+pX+t5T3Mr8r6bCK7dWKSAHjBmJBwsRzijDpOHsjGU3Zo8I3RThMctsv7Gn8yL99V/gf0m+Yl7lz8h/dybNy6alAcm6SxrtAp4tO2SRJDAK5Jido9izGvDrJ7VyEd8mL3Ad0NWdljWHgsl8umYfZhFQiwskQYzW3gRgijbMMkYHsWI14bne1bG24kjwgHuu1q47b4vYeVVzwEeJK45tStvbnKCc8uVEETW3YnaBG6WE4jsWrD1htNCsHIRnadwndeALlgk4QGT/E27EmdgjLQIALGYJ2tC7UmKauTtntUW294FG6xhu0/bP4JoP5UPQl0bOh9IsPJ5eQS8ev2+29LHGo3DOK/lOKSABeWu1ZQ52RuvTpvYg+TyBm7tXHZCF8Mpq1lStYerTr5U8LA697J5N6E67KnbyBzO5SZW0YxRUaeUKSy9S1mSXHu1Weqez2ZTttlb1qUno47JxuTmE3o1/reU9zK/K+k34srJiDI0YXPzIodggo7WsSeYBSf2DfsVu42+Tv1RDxKGMZhr9yLznxwkZ+yEXcKw3euEbuxT1+1tlEwWHEc4gCxGJKXpVPusi/fVf4H9JvmZe5c6dh/dyEXkarGNoFsQnm9WA4VmcWN/Ik6yX3MCTWG63arSfo7P1xv3QOzYx2azdd1k/llCGR2R6VqrAeU5TtFY0hgt8roJzWqvIBF2excA3/AHEhC5X6M0r9WLxcR4WgN0iRw1bIVGL0rFSSpdZmOdY/752ZPlGZqbOr3V6gd34D+7Gs3bK6ye3YF3/xM+tW5umeMS1Z7Is23dEZYXR9sjzYQt06UXmaZW9s/gmgflQ9CXRs6H0qUmDm7op3qx7A4MGrK4HKxqz623zFzj2g5u2CYS5Mcw4KIiVYxhVDFsO7tdyrNjXyXcuPVuVczdiOJM0zPbxTO7tGPV4ZK9cnPgwa352nys385i1mXk2MsPGhLMNUCwK1TIyyQ7Vj0a/1vKe5k4s1Rot6Lfiyufmsrn5kfYX5YPtn/wA6Q3Zdufb7iOzNSqM1Z3Y4+kGZsg/Q7u5idY/fQn1h/wBtWcFP/qqxj6dP5kX76r/A/pN8zL3Ln5L+7aEQ565GkzSi8XqgPW7kJMCzWnOdaFc5ixLYs1nswaUJeSnHtyAGIBtBuxZCchAK1XnYBJoQbpCLewgLASyMEte5Y2TnaCQkIOOcchZG8JSrvxmBB/KO2x6tbjj2I9PfciWOQqOQEWjVG4AQG5KlgZZGA4LFmUO/brkOCPbb8GZOCwAs51z0bhosQn+JMCB4bZRlaoO8VOdQr7nm4ovFwjMO8HtFhCI4tGPtn8MkD8mHoS+uh9LJYx7ew4eupOnRYzGcN5lLfx9h7ELlOYs3caIi5OmSxj3AIMHgEcHx1I1a1eIQ+Pu1LRLNGFfK3DinZyVM9ixRmOUWlF4uKrlcc8xVgY+8+SBbPlqJbYwzBXhasVSDujr5jH9RV6FW8x5Wbfo1/reU9zK/K+k34srQiTIzxYljo3UpXHCDuU0i9Gf2AP2urP8A+F+KOfuM0ICNDtuEsZ1gdZDqO8rUHcj1yEk5DnjKERDpu7GZlbm0zOzelT+ZF++q/wAEvSb5mXuXOnYf9mvWA/4tXDH8LtF4/wDcAW/tx3+2fwTQPyYehL66H/C1/reU9zK/K+mxzM3RuQZSnOb9ZegEnaJGanLdOUvZCxEcOkPTp/Mi/fVf4Jek3zEvcttFgdP2pP4JoH5MPQf6/X/woPreU9zK/K/4Wn91kX76r/A/pN8xL3LEGcLs0JNKLP8AtMz9BTdCboKLP7+UaQD1rsRzgSDTh/gilgEciTxEJlezcn7bo3LXmzf4SiN5F3/vqt8D+k3zMvd6zqSTEhL8N0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0Vuit0VuinMOP4/8AI8md/QnCBYShPh3aD9aTZK834+SuryV1eTuLyd1eSuryd1eTuLyV1eTuryd1eTuryd1eUuLyV1eSuryV1eSuryV1eSuryV1eSuryd1eSuryV1eSurydxeSuryV1eSuryd1eSuryV1eSvuztFqV29JpXoxaEWjH3LeN3dZiepZXEsriWVxLK4llcSyuJZXEsriWVxLK4llcSyuJZXEsriWVxLK4llcSyuHYXEsrh2VxLK4llcSyuJZXEsriWVxLK4llcSyuJZXEsriWVxLKDjjFl/yGOAYtGP75r/AAS9JvmZe84BP+PHCuwFccK44VxwrsBXHCuOFccK44V2ArjhXHCuOFccK44VxwrjhXHCuOFccK44V2ArjhXHCuOFdgK44VxwrjhXHCuOFccK44V2ArjhXHCuOFdgK44VxwrjhXHCuOFccK44VxwrjhXHCuOFccK44VxwrjhXHCuOFccK44VxwrjhXYCuOFccK7AVxwrjhXHCuwFccK44VxwrsBXYCuOFccK44VxwrjhXHCuOFMETfh/tGv8AA/pN+e/8Ka/wP6TfMS/hTX+B/SPF/um0ZNJurfwmLP8A+kYR2xZvTkBvxhtOtp1tOtp1tOtp1tOtp1tOtp1tOnYzMtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOnYzMtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOnYzMtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOnYzMtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOuh1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOnYzMtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOuh1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOnYzMtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOnYzMtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp10Otp1tOtp1tOtp1tOnYzMtp1tOtp1tOuh2b79p1tOtp10Otp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOtp1tOuh1tOtp1tOtp1tOtp1tOuh2b79p1tOtp10Otp1tOtp1tOtp1tOtp1tOngd26PEbQ+/8A/wC0spxj+PJGuSNcka5A1yRrkjXJGuSNcga5I1yRrkjXIGuSNcka5I1yRrkDXJGuSNcka5I1yRrkjXJGuSNcka5I1yRrkjXJGuSNcga5I1yRrkjXIGuSNcka5I1yRrkDXJGuSNcka5A1yRrkjXJGuSNcka5I1yRrkjXJGuSNcka5I1yRrkjXJGuSNcka5I1yRrkDXJGuSNcka5A1yRrkjXJGmsDdcga5I1yRrkDXIGuSNcka5I1yRrkjXJGuSNcga5I1yRrkjXIGuSNcka5I1yRrkjXJGuSNcka5I1yRrkjXJGuSNcka5I01gbrkjXJGuSNcga5I1yRrkjXJGuSNcka5I1yRrkjXJGuSNcka5A1yRrkjXJGuSNcka5I1yRrkjXJGuSNcka5I1yRrkjXJGmsDdcka5I1yRrkDXJGuSNcka5A1yRrkjXJGuSNcka5I1yRrkjXIGuSNcka5A1yRrkjXJGuSNcka5I1yRrkDXJGuSNcka5I01gbrkjXJGuSNcga5I1yRrkjXIGuSNcka5I1yRrkjXIGuSNcka5A1yRrkjXIGmsDdcka5I1yRrkDXJGuSNcga5I1yBrkjXJGmsDdcga5I1yRrkDXJGuSNcka5A1yRrkjXJGuSNcga5A1yRrkjXJGuSNcka5A1yRrkjXJGuSNcga5I1yRrkDXJGuQNcka5I01gbrkDXIGuSNcga5I1yRrkjXJGuSNcka5I1yBrkDXJGuSNcka5I1yRrkjXJGuSNcka5I1yRrkDXJGuSNcka5I1yRrkjXJGmsDdcka5A1yRrkjXIGuSNcka5A1yRLkjXJGuQNcka5I1yRrkjXJGuSNcka5I1yRqJxpnZ/TJLa33DC0fvl/CV2Z1Mbh/5wZ+rM/pfif+FIH6w9JvmZfwpr/A/pN8zL+FNf4Jek3zEv4U1/gl6TfMS95stQ5b0/ct5CnQ7XJZ+rM/st2wUgyMYVkRwDKH045mi196D/3XkqMbkac7l6rjxd0/2twSq5bH3n21/bZy1CgYYrDOzt1b+wPlKOOmKB/Z5miPINRf/a4H6Dl6TfMy922eNWsY0mBZhXhm1TNGxWCWMpNBnlKWrMRWI8VqO3XujxRq8PggrOo8XRL2yZnK1clg7Uq2E+k0fYUgwwkQn2vw4p7VQuUrommG1k6mMKEBrOpsTUNIRKmRq5Gv3KuqcrXYBKDYLOY44q1JFytOrcFVNX1HjD3IVBd7EPlnE8yQFCRCfa/EClsVC7Svi7gDFDWDI521Zid3RAsAthYoVqHJVqlQlYmm87Sr1AUSlKMMJTJ9r8SF9jY67Rvi3gVg0awSFfHZGjkAOYNy8KoEpZ0L4bddjBtahxdArjLQz2NyH/UA5YVwEOTH5AF2t3wX8hWxoe6edoIqj2nnqTFQqiO+PygshVjYGjfrQC1kzNhxLHYjGTx1UhdQYUFEEL9LHXXu42uaXsuAJmrWTtwwFuF3GQI6t6hxlAjwLQzuNyDPAH4I+p8TUI8JUctRyUH405QHF5TfV+JA+xVMhTvi3VCmiEciTbWGIE+xqNytbGxg3clTx0GlZrauxJXYbM7OzOy1T9QxPtIbEyzURTMYVcciGfVmJZ0CzUsVuSPI5qmbM0rI8Xk6eShMgf8AaVf4H9JvmZe7q6y0KwaQp0BPg3otpSy5KhKs9W2ChrgrDo4inRrQC2pMeKlfpzBnbXBxEyx07iqoMeOwXVmLCGnG4HCfSaPs1NY7tyljpAnpkAIgjUJWx+o4ipati5LeNgwMZSrhYUcWKOP1UesLV7QfGkKsQETUKk21CHmahpBcWOo0oxiL8NbutVHd7VPHuCemq1ZgwrzrUNSjhR1c02ehOdc2n7lVhCxuMDixEEFZyMSY65OWlQhfDwnLVEyHu0sVAGNpVwxDAgmwOogNXUotKLxfAP47L38bPVZpSHVojyXTDafftadxdYVCFk2qMVAFdr9ctnmaWJZWkXZsIy1kzNigK1+mCrS2LrFpcs0BwHHbBH+7WldazZmw4lj9R4kNOqMmYzDZuA6GPx4uHSBW9met8HFmKtN1IVsXFp6cJ4/M3cdPO250MWc8NPYmsKgO0bKadFZmCxW1NcLVxsBwxWIq0Kg2exp6DXwXqmq7E4NVoql9m6deIW7lXHaiqTo6vYr0BPGhZwFinAQcfigYqZ3rXpgBqffkDU8HmqrwhTFxa8AxWqfqOI9pf1iBavYrgquqljT1qrEQaGJr4tiwBmBCjqLFRYYxjb/h/tKv8EvSb5mXu2AfaHP2YR+ysVSC+Az4gS1mOUeHaHUtisAgYOrLEHvUK0NTgkfATaGnbYLWKE7auswFjmAsH9KoezVIAwzdE9ltN4Z2Z2r0dOgyTBFqb6hivZHo2tZrWf0h1hOk8bTdZL9UY32fhrWS1PAflaFmw2nMA42mMFDACyQwDt28fAg6lq7pLFzaUoaVt2CcuqZZn6XdWjXZ8PBai6Us3jrqhOJIRnDLka/qKhWB7NTwehkqOTHS25fUhbK1RW72Fk0NPWg2sWJ21XbEHGOFCDIOlOOtHWBviZhWsLImrCrq1+lyrRn0hvab9agWsmZsOJY7G46WOpO4q1auzsH2al6X8nTxQm0nBmZlksYTT1qnchqMLWtPlmHTtkNnEidruVqY3tPY1aKRKADwoWRXKgjwtZapUshqk1iGPNo2Cx07gCiYooY7TQMkEDXrtKqwx27WlcOdt0dNHPC/coFMTC5MpqJMtgKlGsa5T0/bJdxUDnWqfqOJ9p/1oFW7uOrvCvct6TxJG6tpG1Ze3cpFzbtHUWJl/tSv8D+l1ZrEnf2kjKQ5NDC4aOJGdnWZw8MtAJInAO1VetabTV6s7xpG0iHs1+m1nhsc2mCgPItB9MsWudrFGrGnUBXdZHFV8iDsl+zGUA2wGJwtbFvIqyuIbJnqmZeI6Zp8o12gK1WmE1DAHxltnHaxHIylW+yfEs2afKNextbIhcE20vk67PCvisFXxkpmfLYivmAtunpvMbNhMZja2LB2gIg4EHOBBaaLUtMWpfx9S5UcFj7L5QDbQYvC18S0p+3UnY8RZ72lK3CxbOnZpM7OfS8xFkXHVNNNCxGzedmdujn0q7WJGoz0qGdVxIlBiYwlZ8TQ8bSjW9psF1zAsmszjWytNqzVBPWqAr+5VwvYyli+RZTHwydMgJY+tOlSHTJZ0s4izPjamm+3aazfMMRhTFOemLNWcuBjMAKkZ7Vi9QBbBIJvsvkqzO1TF4EGOI9ieTxlXKVu0X7O5aMe02LxFbFClEWUwArxubXfTd21JmvArirBgESz2AfMzqkh9msgsbhbVCz3imwrPlh5NZfB18sGA5vpvMdthvjMVWxYXGLM4Wrlwwm2IpW8dAsTf7SB8D+k3zMv8Q/4OpabOezvvNFoszN/v6t8D+k3zMv4U1/gf0m+Zl/Cmu/Qcnf0fwPJ/wCFIHZoSf0m+Zl/Cmv8D+lN5tZlt3HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW466nW463HW463HXU63HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HXU63HW463HW466nW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463HXU63HW466nW463HW6wtx1uOtx1uOup1uOtx1uOtx1usLcdbjrcdbrC3HW463HW463HW463HW463HW463HW463HW463HW463HW463HW463WFuOtx1uOtx1uOtx1uOt1hbjrcdbjrdYW463HW463WFuOtx1uOtx1uOtx1uOtx1uOtx1uOtx11Otx1uOtx1uOtx1uOtx1uOtx1uOtx1usLcdbjrcdbrC3HW463HW6wtx1uOtx1uOtx1uOtx1uOtx1uOtx1uOup1uOtx1uOtx11Otx1uOtx11Otx1uOt1hbjrcdbjrdYW463HW463WFuOtx1uOtx1uOtx1uOtx1uOtx1uOtx1usLcdbjrcdbrC6nW463HW463HW463HW6wtx1uOtx1usLcdbjrcdbrC3HW463HW466nZvu6nW463HXU7Mup1uOtx1usLcdbjrcdbjrqdbjrcdbrC3HW463HW463HW463HW463HW463HW463HW463HW463HXU63HW466nZl1Otx1W+B/Sb5mX8Ka/wP6TfMS/hTX+B/Sb5mX8Ka/wAD+k3zMv4U1/gl6TfMy/hTX+CXpN8xL3buRvXbs6GMfG6hAzlBiMs2VA7FYov+aCcR26w1PYNUDQ7HdGzwhNDIMnXYtP3LFg2TY7PlL+XyFcNqGosOJ7KoWoXqQTQUzVwdO8ztJmdpThCLylmbbwxRz1ceSZaVac+9XFJoTJZNDVA6rOQbS2KBQG3bJHAJ2YjOzszsScBtumIwyR3Qd2izu4DhNF3GpEHDo00xIEeTNIoAMzmjKMmZ4uQbSYalKMGeUmdpMzspmr1/zWdpMztKUYReUsra2Yu0etjCyNjaMyd0EJtCc7J4alDUdZnIvjaUjDrXIWgD2lMIMepIThOLSgpEFDbGSgQc3dozOATsxWdnZnbePfsU5RG26ciDjFpS5ABvGElljmrZnFAHqOzbA1GFZ8Vnhs7i09kjXXtVbaKWIhTI+Fy92eThykQoxR6zhOM4tKDuzfe4jgL17ahYrxntkjHEFmckZRnHdGc4QbrMRBEbdBaksnp16EgRfrFldBOVMzNjREBTEMnfrwk0SSlGDPKWMzcbFq8EieY4vFpyk0Wd5RlGcd0e4ObThHGVrFapY7mBs2rOHsnLpu0Wzi5EOsJeMXKZuBwnCWLuNciu0mHL2TlAcXlMRwlhuHqK0eoGi4JWK8P+DrA2TWJ5TvEJAcdxBkGSO6Cyt1qFMp2xlqF+sGaISA23TEYRYbh+xzgHJoTRSjE3Ug5jJHdB5wi8YyieuxO2ndmZ3fv14j7k4yjOO6LuzM7uOwArP213q45tEjyaLPJ6WaifIXq01fsxpVDnWIvwyFMZpzlAcXlMJAmbcNEnAbdZpiDeexGMILdSQnCbNKLkHGUYyd2Znd2IN4b2exVHCLy9kTV3l23yRJhoWiQwp3Li65zxlGbNKLTg8ng05wHHdMRRlhuhlsv46dSEBkGRusJErh6d1nZ2Z27g+rxQzBKzuP8Af9f4H9JvmJe7oza9K5YkqTMDVN+EQUefm8uEuRoBwlijdo6rZuNi1qYLtWqXRX70A4V70NO1Xp4uEZLS3zeUQL1OhqHMcjK56paqEqUsTWlTx9avKbSlCTRbDYoLknktNnEPK3aYXB53NWgm1DhBY+gSxQPb4Omg2I47TtM9SJ7tGsSnqiNaeVDK5qQIYmr08BQtnqUcfibAWPkcMaFPKmx4rXFsZwwcrUw1Wrchax+oXG+WqV7Y8HjpSDaxq1NTc+LaYwXxEwrZF8Lup4i3kzYrDByYOfkRCfT+cr1w2P1XSWofpF1Yj6XSU2k8JNFsLiAtOeT04YI8rcpgKLzubNULnsEHH0DWKD3moaXAeGN07TPThYu0apKmqI15rV9Co1HlPQo0MZS74cXjoZti38icDacylWdVaqrSfHgtiJdG2EfJNQeWKwBrssdgKtqs1q/Q7mJzL41N0+2jrV/0cqz36VAqGDqRhWuHWd/UOIWqpxG+NnImp8Q7dVp6BTXb+UKtTWHFRiCGYo8XFU5gDcGekI8KFJtQFPkL4cGTG34zpZRiZPLjxTW9L1oiaeNzt+xTxleDQw2mXr9C6ctkI1mpOpChdv3vL43FQx5jSq3OLYzZg5Wphala1C3jlqxmavjFH4IrMv1x1tY+VgekdwMTRwd+rF7FTFtXpEo2cNh8dYv5KBVq8UnpV7MM7b7+LBAWnSyrVrNU2mYbw3709LfTLi099DsrR30n2VKL381lwzy+IDigxv47NZXtYmtMI8NpnjMxtNXiShepTV/FBt2u/dtvjcbfpGxmrnm1OnshpnHzButadOYJb+MPpmcIvm0Dh5u3Zs5KfEwt6obG/iy1TTqWMfM74HG061QFkVSu2obtuzbyVFtPFDeoDIxIQm2fuEx2OmYdTTFF6/W5i6trGhsBPRqNnynvX7df7N3adupq1ivkcKwrmmse1Qsx420S5pqZC4HDgyGPEa/jg+G1AWgLLGhdyvjj36OIqAe1jr2WceAHdhS01RnXadzHYuxUDZqW8dh8aTNZUBYxaLNFs/UqWseSZNN4ylCjWuMGv5+/bnZy2NFg2HfxoZwOIZWzdVr2NMFsFeifEwsEwb7/ACOXJi8dDNsTI5AoG05lqr1c59YwKyXyNlUf0oRYXC1rtCtZuszMzM2oLJ4tUpV5aTxXHeDTFZDpy4K7V/R7rT7/APw9VA6Nq24rnEPqEoMrVwlSvaHbx2paFV7VMrkHWwePskr4+lirYGtZLDGHj81wa9inztTXQPSx9XHDcVf9/wBb4H9JvmZe7UsPp2/ZrWbGcxtcTkfAAMY9vKHwX1/LrWXytBam/IxyOCFqmUUwTLcanhJszMzMy018xlljawbGfzDEjUqgf/pWSkaFGzIGIq4GdFj2sGQBc/enXqmjic7eFY1NkgSoTAC5Xnb0zAA8RmaljHCYtK3G5qtjQL0bV4FnKz3MWcI8SPTtmkzHxj4ad4sKVi7hblk9G4w69LO0h4q/foNahQvZSvRoGqlxcHd4x6zi04Si5JFFE+BiegxcUeqHAZOtCi1U9g0MtnKUK+VLChqDH3C6gylUmNOGvh/plJZKRYUbMg4etgSUYntYMoC6ivTAM0cRqG5yNR5MHjigCWuS5paAB4fL1LOPBEtS5C7quJRrV8XliCKnZqXsWwBaeuhqgnQtZM8MtkaNKsiAGYUxzhI84i0/LM1ORiTVxYbL0zUBwMEkMxqWFgVywPH6mBaPqbLBs0ZgBnv0tWVaTcCtH2Z79Q4hapZpSxkXajSZ+sWZmboyuAhnc7MDz0jjdrstMfk28WXT1wdNjY61HNAnkBVA3Jti8/C2a9m6VMHcbUISW6Fay4BaUMBjPgSY2ULJqLWMHmHNGzh+lfNnBRPcw1y2ejeiIFLOUh4pardohou7ZXHLJHr2MTcmHEWoU9N1jykPTOUrcmWlyGnXsRli7AamXywzq5WgeocL4Pu3r9CpPPklRyFvt0KzVKFYC0t9MuLT30OwtHfSfZjL46Ofy3e1DkAFqPSBncbOOIpMhh0qevE0NPzxpY2S0FGFS3mbzZPMSxkDUAUdX72qU3gDNUS1IkfTzzsZG/klp38zLLGVsWCzcpZIkdNht164m+5lnoSniLbRwFuqfHAhDC2B4q1ex9nP3B3Ygx1UTbBjgtU05WMX0FTzlK1SgV6GQhlh2dmnrYaLnx1vNHHlLNPHVs79Uwqv/JWVgv0sZaR+jhRmjDWVZlka9StnnLetQ0vUB3HzNBi4CPGx+ao2qUCTxWYhf5TiqWA1tRZZjs7OzO2Tg5MdchHS9ytLGCE2KOPFZG7Ss6hvCtAhQq1RxDXAJldKXGGyWOGDHRFieOtNZANME6FnJWIZTKUKdbUr8W1iLj5PM0nx54V6P6UKtOfRa3s1LGYSYy/CWZxrVu+pXefp28dY4Xe0n21gcvRHjRBPij8vUdwzWL2FtWjULjir0c1WhitTvs8cZ77CyeKswrYIWnj1Ghcx74F8m4qdb9V3lkcpVxsIOWMmlFpR/f1f4Jek3zMvdsgBZG4zQwODBN5wZmZujNRqVzFKK7Vq2hxjYt1KNiEGJev18aGBrGCBKzdu5Unsr1K1VySE1OoKwU4vbPA4whO7NsfUAeRg36dG9DaceGxwRzHCEIChEcC4TGHL3SNjqUDRKKdOo9qNmKtYPDWJby1KdWoLoK/jcbd6OanjKOPbcK/TpXobT18Tjqc94FduBohcpcZ1yWaNlnVrEY67LeerTq0R7Q261e1BxEHiMaEcxQEMYRwGNTwWLIXuzjQpBO5g36lO9DYaGFw4BzGMQhgFAQzYTGHJ3S+PojLEo1KMZxeMq+NpUJzlWv4vGXvvNTx9LGQfsq9eBQD3j4WEreUs5aasYXG2idw1akGtBhivVat1tho4egMMwNZp0ygarOMYwjGMUepUMURpXKdKyw5F9oqNSsQxAqFCnCyWwO1jKN7o9mtj6OOboCxWr2BOM1fC42sRiCdmdnZyYDESm83EIYYNAdjDY65NyGq06tAe0N/GY290c1bHUce3Ssr1KjchGBnwOFUMXQrV5hiGqAAYihLAYmZO44hDDBoDt43GW3jMv4eweNp1yGMO5To2nhMiqUalQU2EChUrjcQq1SvTEwgJ8bRfvs9bEY6lN5gdmdnZ54HEznvkMQwQaA1bxdG999iOGx8IQg1mnVswgxy4TGHL3SDEMUIwGGnVquWQMhQoX2bv1MVjsa24KdmdnZx4zHVDzNWyFChfZmPVxtCl8v7C4LFFI5JiEMMGGO/jMbe6OapjKOOi/ZPj6p5iJOUYzjKMoUqVYPZGEFeoGIQEoVOQ1pj169sWwwMHjK094kXB4sxXLMAh1xRGO/jcbd6OaMWhFosh4ugGw9kd+jRvxZj1cVQou711ZsiqAIWY3jns2K3BW8XQuuz2KlCpSi8a5hCsQcZQ4ahX3uMdCtCq9aAQCrigIKlGM4vGXgcRAm9pgEQUhTBXFXFEIiYfGlL3Z8GoKw5xX8bjbzMx6uLoY7rIJwBsDcZalKrQg8AWMPjrc+4arTrUobK7U6grMrTXadG5CMCszMzM37+r/A/pN+e/pzhCbdJ/3Eoxm3STMzN0b+0nGE26SZmb0pkgKLvPymOXlccvKY5eVxy8rjl5XHLyuNXlccvK45eVxybKY9eVxq8pjl5XHLyuNXlccvK41eVxy8rjV5XHLyuOXlccmyuNXlccvK45eVxy8pjl5XHLyuOXlccvK45eVxy8rjl5XHLyuOXlcavK45eVxy8rjl5XGryuOXlccvK45NlMcvK45eVxy8rjl5THLyuOXlKC8rjl5XHLyuOXlccvK45eUxy8rjl5XHLyuOXlMcvK45eVxy8rjl5XHLyuOXlccvK45eVxy8rjl5XHLyuOXlccvK41eVxy8rjl5XHJ8pjHb72yeMj9zeVxy8rjl5XHLyuOXlccvK45eVxq8rjl5XHLyuOTZXGryuOXlccoZGgR2jFnZ/8AQ9f4H9JvmJfsS5ZjUrFNK5es5AzzLwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwri4VxcK4uFcXCuLhXFwrinVsji8p4PLmrHGAn+hgfBL0m+Yl+xNTMzYtpNp6MZ5QW7oy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoy6MujLoyMOBBTjJndnZ2H8Ef8AQ1f4Jek3zMv2Jqf6WtN/VB/tyXwv7B/BH/Q1f4H9JvmJe7+CnepifaTyePXk6C8nj15PHryePXk6C8nQXk8evJ49eToLydBeTx68nj15OgvJ0F5OgvJ49eToLyePXk6C8nj15OgvJ49eToLyePXksevJ49eToLyePXk6C8nQXk6C8nj15PHrydBeTx68nj15PHrydBeToLyePXk8evJ0F5OgvJ0F5PHrydBeToLydBeTx68nQXk8evJ0F5PHrydBeTx68nQXk8evJY9eTx68nQWobVaxjX7Om/qg/eKYQY7ieUx68njl5THrymPXlMevJ45eUx68nj15PHryePXk8evKY9eUx68pj15THrymPXlMevJ45eUx68pj15THrymPXlMevKY9eUx68pj15PHrymPXlMevJ49eTxy8nj15PHrymPXk8cvKY9eUx68pj15PHLymPXlMevJ49eTx68pj15PHrymPXlMevKY9eUx68pj15PHLymPXlMevKY9eUx68pj15THrymPXlMevJ49eUx68pj15PHryeOXk8eg3qZX6D/s5fC/sH+XD/AENX+CXpN8xL3Hfoy3WMsSbDHiceJujeNorxtFeNoLxtFeNorxtFeNorxtFeNoLxtFeNorxtFeNorxtFeNorxtFeNorxtFeNorxtFeNorxtFeNorxtFeNorxtBeNorxtFeNorxtBPjaC8bRXjaK8ZQXjaK8bQXjaK8bQXjaK8bRXjaK8bQXjaK8bRXjaK8bRXjaK8bRXjaK8bRXjaK8bRXjaK8bRXjaK8bRXjaK8bRXjaC8bRXjaC1DRqAoOQWnpxhkxvL3MjelVYYghwwpuxb74zHrxeOXjccvG45eLxy8Xjl4zHp8Zj14zHrxeNT4zHrxuOXjMevG45eMx68bjl4zHrxeOT43HLxeOXjMevG45eNxy8Zj14zHrxmPT4zHLxePXjccnxmPXi8cnxuOT4zHJ8Zj14vHLxuOXjccvF45eLxy8Xjk+Mx68Zj143HLxmPXjccvGY9eNxy8Zj143HLxmPXi8cnxmOXi8cvGY9eNx68Xjl4zHrxmPXjMevF45eLx68bjl4zHrxeOXi8ci4bHFj0QjWcVYhXs/2U/gl7B/lw/0NX+B/Sb5iXuZczix1ibVARrVxCj/AJ7U/wBLVKLyLJ2rE71cRPcrN381cJL9h5cUTULDPjiyPSrkl/Yy+B/YP4I/6Gr/AAS9JvmZe5n/AKVYTfg3+f1P9LWP/MOsf8jV9yh9Uyv7EyP0+0sP9Nq/2U/gl7B/BH/Q1f4H9JvmJe5nvpR034N74rdY05jH7DWA19vd9grADSJAanbrDNEM0UowweZAHDYF3Be5PK0By2SEcJ47xeq9kLGYL2LtWq7MaOUoS+5mfqzOyd+iDbr2N7C9hbIAyHAnsDYBYaTi9TU/0tY/8w6x/wAjV9zH/VMr757IK0WkaE4kjGcS2AgcbFLZAGQ4l9DmVu/2PWd2izu4cxRPYjXF6J7Aa0N5hkgWDTh6B7IKzRcvq5D5GysP9Nq/2U/gl7B/BH/Q1f4Jek3zMvcz30o6b8G97IWeLUMVUhkx1ijYmrOZEErgDk747jVYtYsCqhcpY51n/wCT4SUZ28lKKzjEfKNIVKxG1UGVZ885sKmLDFcOIcjUrcLteBoTyEIXx02WZsGlMFEAMNjwjaL+OPQvhLSPfgC3XrPYM1cBDPDIQlQe43nIk2di1lgVLDBJSyErXceU87B5yjXoZGvehLYbMABYMCYc2ORoCPm7Uw1ZjjhrpChECSL+ohLLwgXK46EyYfHEG7LAkmzW6sllrPFolk2PHPGXqsZo2aHA0ggu3R3bmOeNu4GkFymhnYv95NPOzhtuz54DdyDNfhCm1mx59mbfIRxHFEo55yDzlGvSyAb0JOOzmBBK4Q0soK5KQn9zU/0tY/8AMOsf8jV9yh9Uyvv5aL5LJCoxwB+5UcE89+film/nMSrVwNILlL59mbfMuQFClK2KvZHYrQsNjssHIzLCBMjCF4VNkUkRDnOTMdmjmEOcSQjON7LApziJq2bCUkQGt3AUhOUzagi3/MkDiIFjRln4SlLj0r9e+N5it5gNY3YhTzArJexO9kwUXhCQM5CRohsLN2pgqEHHAWG7QQtPO14EOJByEXpvZsef6tvaraBcB3h+eD1LFhZ+rJisaOfgxIQO3szPW7dq4+GAM7CNUnkL8MfAU5kz8IO8oNfFOnK0GlbHdrxPCeRhG/Ck1LJRsntBazlg1bgqkr90dCu5554vdp0SOTPwg+6FW0K4CJhWs0GuZwCpZgFsrglcyoKNgYS0clO7MjOXOjiWYq9DKVr2+EfdyHyNlYf6ZU/spfC/sH+XD/QwPgf0m+Zl7mf+lHUfhb3tQWYvOvWfJ5Krcp9qFO29jHROtPChxZmWfDFi0itnehD40Eu3Bx7FgIRGfIQiixaWfhF8RN6Z7lCdXrdPfvPgWZ8YzPiZPSuXqcsPHlWrt+Svf9ecozkiGEJ4RnkPrONWT+n2UH9OyWFhCGOA7W4Rnn6jSzc3FjjbaNy2CsKAqMLT5V7EgwjPUVyT6ii3ChNZH78QeT4np4+t7C/qISzbljkaDhcmfM2xsZQ4IHjJZy1CV2sCWUyQLgYdt7bGxc7I8AKEcfCbZYUI5THkjlWY+SxwJkFAgpQlppulaysGOL2L83zs5cmgNSv3pQeD4WseFI4T1p38RGQZ4+1StuWYO1fxdu0UVK/Ttnf3dT/S1j/zDrH/ACNX3KH1TK+8UkQjnOWNyoQHt2TY++NszNx578/ErN/N4lZZmPlcdXnMUJiccsHBpgyNSVa1KthrwJVAeLv0Vime1kr9z2aitdil2ovlKL47hLT1rv0O1LDQY2QyZ52adW24nLlGY+XoVpkCIgpDnlxQo4YgQUr10FYIxYwdryhzyOC9j8ie2Cpk6dyzGJcnTuQvBv1YZatYKMN5ll/ptpYX6XVWHHCWWyk31C++dADxFCI2HHDMwL+UBDBDjyslNXYQfP01qKMXxsnVV+tcLqUmhF5PSyoR37dw1e+HzTGHqdt1SuyYQ2Cw203/AMqdkb4ib0bV+jLCR5Vm7kJEbh6ghNHC92OTvtfN5KWLrx1LFnrVYphQiJhtgZPCle6Ym3aCMsxWHv3LlM7ZIcS5rGQnlJuLHWZQxVq1WqDiEMbhssC0/u5D6faWH+m1f7KfwS9g/gj/AKGr/A/pN8zL3M/9KsJvwb3qNYxsjat2HHB2dnxAD1C3K0xQvYY5oQuiyV8gDyy1GdyuOQY5HJzH2WwtSxVLcYynXN5uBlnKdiRRWKteo9bG9hsIEoKMYFz4phKGyLH1+JTEH2ZPH84DbYX8tXZhmrU7l22O3dzFOySVezWsGyuQrkChVzNgpBfFwIKgAZDANLN1jNbqtarEE9ezkcbBqxaD5ExiGshrmhnLZXzgCnp7BHrSPj5hWILcE0ah0Sud84MyyFY5MnjyQ9jv0Z1i6hpWbduyQAiDnCWHrmGC1TsVnv4d5gRwZG3dqWiZeiWxEJq738lYH2YYGsasA8C4cBgzuuTLUSWYCKHymS2ONVB3Wp7TivZSqzis4upZazYuHe1k6RzMSrXtW8lG6b3NT/S1j/zDrH/I1fcofVMr72baxOk4gUqo61YQmzlMhQhNWyYrFp8WSOXrnNaxsh5jHlsMGxXnk8mUXZhiaD0auyd3GHJmGeGdqFsVRuDD1pVKQ4zRqx7maHInbggVT08wV4HrXMZeLaqsfJ5E4dmXoGP2bFaeSyphdkb0CGxnFsAt5PHjjWNjPIkmY1s9jKUrhXeALeTv17JMhPJ1bUTgstdzEwDUW2xZlkxzLQswhihzDj64yYkBwZDJElmMdK7Xj22ymUiNgvh6JagzEPhgHCW+5bVY883UM2dCU+PmMVZnjXDF8w5+AWAMdUarTCKWcpSsVGkHLCtW6FLp/wDTosBXNXDYYuohTEYFgOOrtTqBCs9UMeuIgMbS7WMiAmDx1kNspD5+uY4azC/9LB1ihFbY0B38KYsRVz5W7bFNXK555mgWJwxOEgp1y5LEM9edEmUsWnMf3cj9OtLD/Tav9lP4ZewfwR/0NX+CXpN8xL3M99KOm/BvU6e9LrtfaOhftWRGve3p6HT+y6e509nT0NT/AEtY/wDMOsf8jV9yh9Uy3qdPU6ezp6fT059dr7RY+/atiPf/ALLI/IWlh/ptX+yl8L+wfwR/0NX+CXpN8xL3M99LOm/Bv8/qf6Wsf+YdY/5Gr7lD6plf2JkPkbKxH0yp/ZS+F/YP8uH+hgP0HL0m+Zl7mXD3seeEaViNqqIrf57U/wBLVKUmK8Y1x9kAh+4D/ozdkcv2HmDRBQMqAnBTAJ/7GXwv7B/lw/0NX+B/Sb5mXuOpBs4s0y1x5rHzb/l5fHLy+OXl8cvL45eXxy8vjl5fHLy+OXl8cvL45eXxy8vjl5fHLy+OXl8cvL45eXxy8vjl5fHLy+OXl8cvL45eXxy8vjl5fHLy+OXl8cvL45eXxy8vjl5fHLy+OXl8cvL45eXxy8vjl5fHLy+OXl8cvL45eXxy8vjl5fHLy+OXl8cvL45eXxy8vjl5fHLy+OXl8cvL45eXxy8vjl5fHLy+OXl8cvL45eXxy8vjV5fHLP5GnZouIWnWaWTF19zJUWuQhIQ8w9d2Ff8AM4xeZxq8zjV5nGrzONXmcavM41eZxq8zjV5nGrzONXmcavM41eZxq8zjV5nGrzONXmcavM41eZxq8zjV5nGrzONXmcavM41eZxq8zjV5nGrzONXmcavM41eZxq8zjV5nGrzONXmcavM41eZxq8zjV5nGrzONXmcavM41eZxq8zjV5nGLzONXmcavM41eZxi8zjV5nGrzONXmcavM41eZxq8zjV5nGrzONXmcavM41eZxq8zjV5nGrzONRM5QgzdsFazfPC1c/sp/DL2D+CP+hq/wS9JvmZe9OuCf3z4VRcKouFUUqNFvw4VRcGouDUXCqLhVFwai4VRcKouFUXDqLhVFwai4VRcKouDUXCqLhVFw6i4VRcKouFUXCqLg1Fwai4VRcKouFUXBqLhVFwqi4NRcKouFUXCqLg1Fwai4VRcKouFUXCqLhVFwqi4dRcKouDUXCqLhVFwai4VRcKouFUXCqLg1Fwqi4NRcGouDUWowiFjHaGm/qg/elCE26T4NNcGmuBTXBprg01waa4NNcCmuDTXBprg01wKa4NNcGmuDTXAprx9JcGmuDTXAprgU1waa4NNcCmuBTXBprg01waa4NNcGmuDTXAprgU1waa4NNcGmuDTXBprg01waa4FNcGmuDTXBprgU1waa4NNcGmuBSXApLg01waa4FNcCmuDTXBprgU1wKa4NNcGmuBTXAprg01waa4FNQrgF8H9nL4X9g/y4f6Gr/A/pN0axN3/Yep/pa039UH+3J/BL2D/Lh/oYHwP6TfMy/Ymp/pa039UH+3J/BL2D/Lh/oat8D+k3zMv2Jqf6WtN/dlB/tyfwy9g/gj/oav8AA/pN8zL9iao+lMtOOzZQX7cI7NCTuzdXQvgj/oav8EvSb5iXu6dzk8layoJn1NmI5e/jqD6vyeNlDzATiOEZQrUGUfB46dt9NZkeYoNbf2VM7M2ormIdZi34nFWryxN7n4ynbl/ksjVa7UKBThZoWHi7Z7LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LLz2WXnssvPZZeeyy89ll57LI2YyR4SGTDY0t21CSZujM3+hgOzQk/pN8zL3dFv0v6oWH/WWpFquzRr4C7A+loFFgcdAyzH/AMxqbGYtsR/8PqfKYtZzKQw2ONclUx+sb9aN8um7Nqzq3JTt5TJ5a9mGwuI1CDP4jE2msVbBqehQnBh46oztGpZWZyuUPlB4XEXvtLp6ELps5qFqOCfI1R47WFilG/Bspfq6eNfv0AarzlNsi2APlnoShlP8XZo1LjN3/s7i0+nMUvs5il9nMWvs5i19nMUvs5il9nMUvs5il9nMUvs5il9nMUvs7il9nMWvs5il9nMUvs5i19ncUvs5il9nMUvs5i19nMWvs5il9nMUvs5il9nMWvs5ilqbE0sdhbVqtjcHjbFCqUmq8TXx2GLbp5QWGx2FDfQdI5wwGM+nauOyYbA7dHFmz92/xMVp60G7aq5K5SfI5guLxNjA5HDW6zm1TUBijYnh4zSdkZXLktL0KuTjlHtYalezhLFcNmgbT9+q2S1bh3xdR79LEaXaAnJe0rRqZKndNcqY0+oblqdBqM8LlqtPK6nqixd/Eip3tLZenSndjh8bh8rjKtxfZ3FL7OYpfZzFL7OYtfZzFL7OYpfZzFL7OYpfZzFL7O4tfZzFL7OYpfZ3FL7OYpfZzFL7OYpfZzFp9OYpfZzFL7OYpfZzFr7OYpfZzFL7OYpPp3Fr7OYpfZzFL7OYpfZzFL7O4pfZzFL7OYpfZzFL7O4pfZzFL7OYpfZzFL7OYtfZzFL7OYpfZzFKOnsVB2dhBECDQF/oav8AA/pN8zL3dD/U9TKGGrZzVuegcWh8NULAs2ZmZmYxYBEQs8HT1Lk7V7NUM3T1HjT0M1e110s4CtZDVshsVAmBhjis63zJBYuca2sc+EutDiDgLbE//wBfLSUmbTVBEx5rOs8qBr2liNXm12dPFA06GlenpnL4cEz4fCZsObwBD5OOlj1RvYwOlM5byda+G9+x9a/p64sD9Gx611bEesHDVdd0mq4jEMw9M3iDgSGnMTWxxcicR9N0L1uxfw2EyuZpZ+eDy2lJtWzuo6pbN+ljGFO5rT6vpj2aGdmhnV/TuwHplq6/qIUcqFKo2uf0uVUps+MrLRcN+EzcG/p4WDYgtVa8lApcJRDqT9QabWY+l3Voj9OVv9ZV/gl6TfMy93D4SGItZMg6WAhSzV/KN7MvjvIY2xTjj6YcXQBSBlMcLK0T0y1sMCrhY4yzDR1mozgqUdLU8PkJ26+Z0qDKzFYhPRz2xG8o2Fi2A8O2Hoti8eClHP6aq5ftnZ9GnNKDZPJ4qjksdwLH2PusN60oYLG18W+PaGj7lRpBq4nFU8LV49b9j5zFRzWONSlDQ1iEO22L0tQxJJGHkKlW7VJVsfY+3WhINXEYSlhKjhEbSMq9o9nFYfTwMUctueX0oHJGhcCDSLwsitZLN4MeUs42zJYbEQwrXmHpjBNk6t+yKjpaILzXr+QxtfJUjUzYXTtjDuaD4LCiwALI2v6UHK4S9jcZpkVG69+1lcDDJ5HG3XthaxWMF8Njo4TGjoj/ANY1/gl6TfMy/d2OxdLDQMKn/rqv8D+k3zMv4U1/gf0m+Zl/Cmv8EvSb5mXuyJKUngNqsf8A7cQa4o1xBriDXEGuINcQa4g1xBriDXEGuINcQa4o1xBriDXEGuINcQa4g1xBriDXFGuINcQa4g1xBriDXEGuINcQa4g1xBriDXFGuINcQa4g1xBriDXEGuINcQa4g1xBriDXFGuINcQa4g1xBriDXDGuINcQaesKP3vxBriDXEGuINcUa4g1xBriDXEGuINcQa4g1xBriDXEGuINcQa4g1xBriDXEGuINcQa4g1xXb4RWHZ9hfQd2izu/Q1luqesJvx4o1xBriDXEGuINcQa4g1xBriDXEGuINcQa4g1xBriDXEGuINcQa4g1xBriDXEGuINcQa4g1wxriDXEGuINcQa4g1xBriDXFGuINcQa4g1xBriDXEGuINcQa4g1xBriDXEGuINcQa4g1xBriDXEGuINcQa4g1xBriDXEEuINcQa4g09Ybfe/EGuINcQa4o1xRriDXEGuINcQa4g1xBriDXGGuINcQa4g1xBrjDUgkE26AyMRvRMZhszN2ik+8vGguNBcaC40FxoLjQXGguNBcaC40FxoLjQXGguNBcaC40FxoLjQXGguNBcaC40FxoLjQXGguNBcaC40FxoLjQXGguNBcaC40Fxmb4R2DAdol/Ytf4Jek3zMvcLLaOToUWHBv2nYj1G7tCW6EX9Cw/WQxftQjMGzB4+hB++YpX/wAJOLTi8Xpz/wCh937Er/A/pN8zL3Lf5Mv2pP4JoH5UPQl82P8Aalj4wejW+En+Gx/5Zv2KDo0Jek3zMvcts0QP+1J/DJA/Jh6Evmx/tSx8YPRrfCT+26t73Vn9LH/lm/Ytf4Jek3zEvcudOxL3bpTDIEYumSVdywh0J1b2Ts9o4R+zq3t6s6I+2EnVGwQlZnlXlesxnKEj2q04tY9l6wQXbgKsXvBiT2dWVM0y99pq6WYAs42f/izv1b3Orezq3+Qn8E0D8mHoS+bH7sC3TFLETNkWduq6t7K9hj91nXVvZ1b29W9vVmVw0w9nYurezqze679Wdo1u+OEmJ1b2W7jVoxaD/fF9tXvDhJidWb29W/xlj4wejW+EnvlnORJEjF2kzO0pxg3WUzDmObRrfeGCkcUH6PKTStBdlZN24dGEWJGZmmUY/igUZPhVr7gyU3fiIVaMxxk8d4DRh7Ljuw4dFNmeEmcLQYbbJWAxfo++O3eh2WchN3vUZRgIzv8AsSv8D+k3zMvcts3Z6e7fk0D1ZLyMExGJQmSNOu9gHUtffXtvXezXhywsiO1Os7xHUYsGmakaYTGrT2TNdsjYAoghsib8qax3yUFjjCEIjTuHgeMQii22MW9guli4Yqx7bLJa0rpSN2xD4Oxt0Ma7ux+sWneMR5XQzBCO3Iv0x8XYVX/lExW63SkeU2ekQch3yTGw4twm2NMFwpR1dyhTGUbSgPusKESf46fwTQfyoehL5sfuhssA9nrC9Cc4wWRebdjZOj1g8lV6WqUokpVRzkd3tknMw68CU2FB5hcrmoSKq1XkBgQqtCNOMYwPVgEbzhWm5QQm7uzM7uIcr8pmLaFMJAQV004RgMb0Ys33UzyMB2nVGO28pHqQNX7kXZp3TlZ2qlASMgXO49qvEYKsAb5KhJ3oE60xTsB/7HaWPsj6ZQEOgysEEARdmx5JRoklIAHts5jR30rcBvb7nNDCJ6nZHIogT7goT/xdj4wejW+EnvGn2xykguFgPGVSe4bxTt3rTxlYFBxSdoSeFRpNXFBhs7xgwrwfZbZuzJ0NoxHF2rQYm8s7EWFIZYK1+RNT+UQrLRHCKixDliSSu/lw9hfy5oLO9XpEJAQjsIIY4wdoAjFzH6+9j/yzfsWv8D+k3zMvcu9Ow/u5JowtU1uiidHCV2x/ysE/RspBZHbC5TV6Hcoy2V6lA4mkqo6cSz7NX5u57C/lzWN+ViqFcJYElMYBC+BWisEE5qvRm4YyRAzpnEZXv+s1cykYcR73xj7ondY6URGIEmTLDbAbZL5FmTfCyrV60iFGeVbFikNpWCBi0BlsVhVhuUL2YNTHIxadVhuUVCwQlVnl/jp/BNA/Jh6Evmh+7QeLWbPWMxrIfFWUvgdY38mSx7xYtiErgx8wcjFpYscHm7xE1Ejip/Kh9l2UnIESsVqYBTk1L5USlHdCUVjJxaDhnfLBzAHHIwZ3DOXBxO1naq1aI3mBw464zzjTlOJyhejKArBQkJZEOcIPbdvKVVL4ZLH/ACRliSjcbjlckxTADDJfcIaaTSH1jSg86E4tjywkKMHsSY1sA4F6eQArfyxlTk/CE3+LP8YPRrfCT3rP/YQYVxwpmYFlmYnUB+6jHiQcojHDfVaKFYiOOwrTclsUvZZi8gyZgFgSLRYM+K8hHnPkzhGCtfkTU/lFX/JH7bv5cVzAJyRKGbxBv4u6PJDOPQlSLs03UZsA5d/vY/8ALN+xa/wP6TfMS9y50YD+7arAO8evj6qhVgIWyIAQBBoxKGHI7yOEJm2yEKAR7ISoV5O7oQRhjth2BQLOcE7NJnZxBgCDQgMIgboi9hQjPBoERgjLDZNxjcXbeFCvCTO0BDC8+2eqCx98o48LweLFCM0Nk1Zq1zSbqGmAP3xlXHYhtJDH14OzpxwnB4SbG1lGLRZmj/jp/BNA/Jh6Evmx+7OhXnJ5P40HXqrAQz2M7t1bohBgGLtAlQJJ9yRQiIPZOOOrxfqpChKDwcY4ig0IowRGjtnGgCKGOAYNCKs0a03Z5cKpGMYtKMZxeMvHVlCERxaMZ0ATlvQgjDHaO1WAd49Q1ABfrEtYfchOTt1UYDCGcB0qwT047w1Qg+CY4EG8ZhriBF2gEIwQ2QLjgGfqwq4gM7DIATliREhEkJQkMcRQaEf8VY+MHo1vhJ6TMzfgnZn/AB9vRk7M/wCLMzfh7/Rvbtb8fY7M/v478o37Fr/BL0m+Zl7lzp2JftSfwyQPyYehL5sf+AjCMG6R/wAnY+MHo1vhJ/hsf+Wb9i1/gf0m+Yl7lvp2JftSf3Qkgfkw9CXzQ/2pZ+Ov6IP+MzQf/CO7Mzu9IjQBOX7FB8EvSb5mXuTi0oyiq8+sdj/tKzNujBizdGZvQsQ6NGcYyaTdW/aXViF3N6FwD9WMONgb/dLuiXdEu6Jd0S7ol3RLuiXdEu6Jd0S7ol3RLuiXdEu6Jd0S7ol3RLuiXdEu6Jd0S7ol3RLuiXdEu6Jd0S7ol3RLuiXdEu6Jd0Scwm/F3lZ/4xZmizM37EB8EvSb5iXukBGf3t/5UVutLdaW60t1pbrS3WlutLdaW60t9pbrXRbrS3WlutLdaTPYaK3Wkz2Yx+7daW6yt1pbrSeVpbrS3WlutLdZW60t1pbrS3WlutLdaW60t1pbrS3WlutLdaW60t1pbrS32lutLdaW60t1pbrS3WlutLdaW60mezFlutLdaTytLdaW60t1pbrK3WlutLdaW60t1pbrS3WlutLdaW60t1pbrS3WlutLdaW+2t1pbrS3WlutLpbnFoPAbD9J6zxd3HusrdaW60t1pbrS3WlutLdaW60t1pbrS3WlutLdaW60t1pb7a3WlutLdaW60t1pbrS3WlutLdaW60t1pbrSeVpbrS3WlutLdZW60t1pbrS3WlutLdaW60t1pbrS3WlutLfbW60t1pbrS321utLdaW60t1pbrS3WlutLdaTPYaK3WlutLdaW60t1pbrS3Wkz2Wit1pbrS3WlutLdaW60t1pbrS3WlvtLdaW+2t1lbrSYBjN1m0Wi3RvRlCEvi4tdcWuuLXXFrri1lxa64tdcWuuLXXFrri11xa64tdcWuuLXXFrri11xa64tdPWrri11xa64tdcWuuLXXFrri11xa6lWrR+9cWuuLXXFrp61dmXFrpq4G/D9jV/gl6TfMP8Awpr/AAP6TfMS/hTX+CXpN8zL+FIPgf0m+Zl7kpNFur8gK5AVyArkBXICuQFcgK74VyArkBXICmOCLdFyArkBXJAuQFcgK5AV3wpjgb8OQFd8K74VyArkBXICu+FcgK5AVyArvhXICuQFcgK5AVyArkBXICuQFcgK5AVyArkBXICu+FcgK5AVyArkAXICuQFd8K5AVyArvhXfCuQFcgK5AV3wrkBXICuQFd8K5AV3wrkBXICuQFcgK5AVyArkBXfCuQFcgK5AV3wrkBXICuQFcgK5AVyArvhXfCuQFd8K74V3wrvhXICu+FcgK5AVyArvhXICuQFcgK5AVyArkBXICuQFcgK74VyArkBXICu+FcgK5AVyArkBXICu+Fd8K74VyArvhXfCu+Fd8K5AV3wrkBXICuQFcgK5AVyArkgXICuQFcgK5AVyArkBXfCuQFcgK5AVyArkBXICuQFcgK5AV3wrvhXICuQFd8K74VyQLvhXICu+FckC5AUxgsuQFckC5AVyQLkBXJAuQFcgK5AVyArkBXICuQFcgK5AVyArkBXICuQFMcDfhyArvhXICuQFcgKY4VyQLvhXfCu+FckC5AFyArkBXJAuSBckC5AVyQLkBXICuQFcgK5AVyArkBXICuQFd8K5AVyArkBXICuQFcgK5AVyArvhXICuSBd8K5AV3wrkgXIAuQFcgK5IFyQLkgXICuSBcgK5AVyArkBXICuQFcgK74VyArvhXICuQFcgK5AVyArkBXICuQFd8K5AVyQLkgXICu+FckC5IFyArkBXJAuSBckC5AVyQLkgXICuQFcgK5IFyArkBXfCuSBd8K5AVyArkgXICuQFcgK5AVyQLvhXICuQH3a/wP6TfMy93bBbYLay2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2stsFtgtsFtZbYLbBbYLay2wW2C2wW1ltgtsFtgtsFtgtsFtgtsFtgtsFtgtsFtgtsFtgtsFtgtsFtgtsFtZbYLbBbYLay2wW2C2wW1ltgtsFtgtrLbBbYLbBbYLbBbYLbBbYLbBbYLbBbYLbBbYLbBbYLbBbYLbBbYLay2wW2C2wW1ltgtsFtgtrLbBbYLbBbWW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW1ltgtsFtgtrLbBbYLbBbWW2C2wW2C2stsFtgtsFtgtsFtgtsFtgtsFtgtsFtgtsFtgtsFtgtsFtgtsFtgtrLbBbYLbBbWW2C2wW2C2stsFtgtsFtZbYLbBbYLbBbYLbBbYLbBbYLbBbYLbBbYLbBbYLbBbYLbBdIrbBbWW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2C2wW2Hu1/gl6TfMS/hTX+CXpN+e/8KQP0HL0m+Zl/Cmv8D+k3zMv4U1/gl6TfMy93E5elki3Ags6zw+OtHrGq65wVorBj+Psv3a2Jpku2sZk6uVpwt1/Vq5mkXKHxUFlM9Vwxqdc3sIUQISKYZIFhGY/2DksjWw9Odk0f6g4F1ic5ispB3p+jgs7UzLWGB7LBwVhuQ2+LQcjYTPVs4OxMP+tq/wAEvS+5jy93Rbs2R1QsBCE9Y59n1Xi8ebAXDz0wch8DjiFWqnbJ5LEYMekZ+KyuVwiuWwUKprVgOf1HkYcvHYDUFfOVCTRtS37l01PB4fUhD33xWVzmfhh5grB+0uZxUxTzeRzNXHYyWRm2c1Q4ubCjmqlvDwyqFn9Q5OD2MVgM7DMVJkmsV/8A0TN+zN5Z8bbxAGzecjh2AOE8/n8W8DZfWpr7YcjV9HW8u9SsO1+wdafp64sKIL4mmz6sxQ8LGrn8bay1PHYryM4Z/U54NcBhc5WzlF7A31HlL1w9bB4TOmydkuNv5bUk6V1sbjQanyFC2CtndQ599OmxzPhsnnrdt52dOZrygsm8qWsb15jVqdLU9ytfDj85r6eTag4m09bzRajxyOmcy+WqXZzPqW/bvFpYPEamK9+GKzOez74S3jhufUOoqUXunpZAGQpBuA/1iD4H9JvmZe7ot2bI6oQwZg2r882JNpzPZZ9mYBXhWCIAndmZ3elmL5s/ksyC/l7487jcuf8AqLPrggoMBjAGI7lavWpZItT+nm3wbo1aoQ4izsPmJ66yPAyINb5OoamfwBrmlA4q5G3q/CAaB6dujqfCzlCrU1ZgAcerpzP18uMzusV//RM37NZ/V9OrUen7OVehboz1Hn8G7PnNTTGbS2UKPAfQ8X+wte/p6ysNJmwdFl/UE4xYNqra2EWthNPVVGxr3ZBoabxOXxlvKGvvg9SYG1bPhsHqXmXiY3IaV6EzWpSFPVqHjGNjWkIvlNMwdaGdmhnVoOAmBl5N/UaMGxdQja6/TM1Tfpja7LRb7cJnHh/T3Y2EKVa8ZhxwJh6qhF89pdnzMW8Tei2gv07W/wBZVvgf0m+Zl7uncTexdrNlsYrDXaepcxkTezN17hMTcFS07j3w+IBUWpsX5fEnrMLDmu6ZHjcpUDrjFhagDTuEPiwWS3mwuodPXLE8LQxWasZOOUzOdwNu5cDlsSYOtsuLi2MrhbFvDwpAb7eODgvi9MtQwZMbCmPXeNHwoacwxsO1qxbVrC6irZ+/lcZv/qCs/hr1+3hDiz+Iy1h6d7F3cfqvN12p37mHCfBzxcdPA1FjGahf/YOq8dZymGsVa1UOv6lcFcdDTdw1+GSzmfxFfMY0lUoG17Shw4YLFXcbUNzoUdX4Oxa4OMwmSfLzzOXyuDyVXLSzOEli9SZk9V8tn8Ncv3MGcC03iruJbKQtaXp5mDX7+JjhM3mb9a3m9SYyOYxZ6awDairQLVymmMPcwtO8G2+Ez+At2zYWrhMtksoDI5zUOJt3cxhbYsgGdijZDDTFKzicOGof/WNf4H9JvmZfu7TuJ8EC2Nv9dV/gl6TfMS/hSD4H9JvmZfwpr/A/pN8zL+FNf4Jek3zMv4U1/gl6TfMy/hTX+CXpN8zL+FIPgl6TfMy92nmLNXLXGOztJmdtP2zzu5ZpnsgrQ6lDfp2G6hWXnMeMuThjyknpoBZ4bH3MnShZlMmSwVqsx/ZqK3de84KWPuQv0BGac4Di8ptepHgSIdKnKbDymYl2lVdnsZu4/cxL1iGADYxQ5Cn3nCi3Klbb3wWQWY7gyIMMXmQGVoWH2glKMWeUoZjHEJ24fwJr/BL0m+Zl7uIphvkz1cuBuTrlNirunH6Xs2qQI5zI3rVvN0BYthZKiEndEMizf0m8sX+lQLTmfoY7HQCazZnqG1UDWZujMyKWARTJLT3asRvXbGmCsGdzGzzUJ3sxVxavYKlGpOdTS89mCeaweODkoEyN7LY8dHI47saqjKb42Ma2MoY0cjixxsTdma5leRjqOVqExmqOjZCpyYY7BX3AShqtixoVHjCnpnI1u1WDBwhHB/4EV/gl6TfMy93S/wBQzK1DRlZjC5U0iR7Vm+SdKw2DyV6pczmQDk4ixtKvBhhGNs59KvLEfpeC0mEEsN1nGMYt0ZaoO4qAwDjpHFDDHuHphwGXomBmHljcvUySvZ2jKrOFTS8YTwDjfA5EFAc8fdy+QhfyOPYGpmZi4dEGxBzg+JfG0pFoZMV3CTv161PJZetUsDq2c02JdwTxGSyEsUKsWxmZacLS7lfFMfx9XkfwIr/BL0m+Zl7o69cDkkFNVq15TcBqVWwJmt16VSp1YKMOBIShNghCPswCMIY7BeyyCueY5zR64DxaJZDHODjkHHUa7vMIQhBHYG7ToXXi5noUmiODHDWNt3q3WpXBs9gFWtVi7AKEFgbxOOhQqP1rzgMkXhOOOxYX3h/gSDo0Jek3zMv4U1/gl6TfMy/hTX+B/Sb5mX8Ka/wS9JvmJfwpr/A/pN8xL+FNf4Jek3Tvdf4U1/gf0jR/CbRk0m6t/CYpP/pGLbYs3plHHo8m7xF3yrvlXfKu+Vd8q5BV3yrvlXfKu+Vd8q75V3yrkFXfKu+Vd8q75V3yrvlXfKu+Vd8q75V3yrvlXfKu+Vd8q75VyCrvlXfKu+Vd8q75V3yrvlXIKu+Vd8q75V3yrvlXfKu+Vcgq75V3yrvlXfKu+Vd8q75V3yrvlXfKu+Vd8q75V3yrvlXfKuQVd8q75V3yrvlXfKu+Vd8q5BV3yrvlXfKu+Vd8q75V3yrkFXfKu+Vd8q75V3yrvlXfKu+Vd8q75V3yrvlXfKu+Vd8q75VyCrvlXfKu+Vd8q75V3yrvlXIKu+Vd8q75V3yrvlXfKu+Vcgq75V3yrvlXfKu+Vd8q75V3yrvlXfKu+Vcgq75V3yrkFXIKuQVd8q75V3yrvlXfKu+Vd8q5BV3yrvlXfKu+Vd8q75V3yrkFXfKu+Vd8q5BV3yrvlXfKu+Vd8q75V3yrvlXfKu+Vcgq5BVyCrvlXfKu+Vd8q75V3yrvlXIKu+Vd8q75V3yrvlXfKu+Vcgq75V3yrvlXIKu+Vd8q75V3yrvlXfKu+Vd8q75V3yrvlXIKu+Vcgq75V3yrvlXfKu+Vd8q75V3yrvlXfKuQVd8q75V3yrkFXfKu+Vd8q75V3yrvlXfKu+Vd8q75V3yrvlXfKu+Vd8q75V3yrkFXfKu+Vd8q75V3yrvlXfKu+Vd8q75V3yrvlXfKu+Vd8q75V3yrvlXfKu+Vd8q75V3yrvlXfKmsFXfKu+Vd8q75V3yrvlXIKu+Vd8q75V3yrvlXfKu+Vd8q75V3yrvlXfKu+Vcgq75V3yrvlXfKu+Vd8q75V3yrvlXfKu+VNYKu+Vd8q75V3Jy/GMIw/D3f/xABHEAACAgAEAgcECAYBAgUCBwABAgADBBESkVKSEBMhMDEyUQUgQVMiM0JgYXFygBQjQFBUcLIGNENiY4Kgc9IVRFWBkKOx/9oACAEBAAk/AP8A4crA/kf6dgSPEZ/sIIAAzJ9AIF0/OM9pXQC6nj+3GDpYoZSPiD3+rrTw8MS1MPoYXa5WXWrTmv6iBMJajvx++2SDdjHGGrHDPaNp/VKwlz+Q+6OxVJ2mAvGufESl7BZMO9QoUE6p/wBwaTold6ZP/OLAgfsI83VQgUNaA8wtGgjwCgxqaquCWK1iuxBHDDqtIzSpZgqgkoFFp6M7LyMxUswVQWYbqbX5IM+rrZthMBUBZcidGBrcVSvSbKtWmezq/PMOlmjxdpQj2vozr/VMBUiFHaYYaP5X09nlauU+y0wdQtwvlQE5NMBUlZcB2B6MIthTzNKFqLsQgHR5TrgQ2KgNKvMJUVI4ZfT55kXCgGVfxF45FmCq0ReqvUfVygWdfW8GZRw2X5HOYWukJTqmHFYoseUJb1vFKBrFBs6uYCpBa+mUiwl9MwyVVU09ZMAgrHHKNGIhyAGZMoF5TxeYZEHGkwFbpVbo1RQhsqDtKOvsTkmCqidVfwf78XVXYhVh6gyk30eqTFXoOFpUHTjSOGrZNSsPSPl1tv0fRVmIwlzlfpu7y9LKHGoBDnphJbD60c/ohJsvt7B6eglC33FBreYc24f7Eq0YlMLYhE/yqf8AmOjjnyDPHVAbb4CErasLPlXf8Jhk/ieOVC2og5oZhUprfzKs8BaWSN2CgM/5rPPicQd3MGS1oB0NosTyNKLfwtSYtnHDbKeqvfyQ5WMNCS9a6O17C5l2Dr4XDy3M03+cHsYAzCpaVo11k/DUIM1a1AR6gmYSuqwrMMlZtc65hku0INOqVBK0wxCgT5wnzo5WogtblwrKtF6IShhyZb0nnuyQQ5J2l/0rKEpbjnnRyrfmJ5/4OBjULAbQPErn2w4VCy/pcT2n9W4bIL/vzDm4awpWYQ0tpJBaYSm0DjUGeQoG0cE4yqRtALhWOXl7Z7X+HyZ7Z3pl/XG1bGD+XzCeCXI2xhzBUES4rcBwQ60eizSTDkq4mpifwDCeBEOa9bPkGcc4RPmVz5V3/Celf/MdP/0nnnFgSv8AK+eTDDX04I26qtYfXKurZDkUmDQPxoAGj5iu1grfpM/9B3mJ6klCVOjXPa//APVPbScsfX1WH0avXSMp85Jwicc+Uk/x2nzhPnT5Dz5Lz56/8p9m2f8Ai1ughAAGZJ+Anlsvdh+RM+QsxZouflnVXoOCXOaC4V6mM8CAf99nSjiVWDQc0tSe0bJVbdY5zawwgv5rX4nMqL0WnW44IUcJxrBZbY5A1AZKojamHnb1Yyk2Ya0lyF+xMSNA4lDFYrubGzsuaeRKtErPU6s6rPVZir3HkCrG1WKql/1GfIacc4RONJwXf8IR1tiQPRiKW+IjA3VvPPoLrCcs+0fiJ5726f8AuavLDfhbh2cMxtmgjIgSp6sKDm7mD+W1WjYZCBxofOq9JbWDxhItiYfXqeydi5ZRGGh/oWcQl2sCnNINVlDa5YanI0uCoO4M874Rtc+cJ8+f4zz5T/8A+T56/wDKeR6hKLCFbNLawSJbiTU/nZgVGmMWFNzpn66TPC3ChYLEep867h5TDSmsEF0SVMmGRw5czwA/34gYehGcwNPLEVB6KMunBVMf0ytUX8Bl04Kkn9MRUX8Bl0Vqw9CJSifkJTWx/FQYoAHgAMhMPVyDoUMPQjOU1q3qFA6MPW/5iVqg/CYZ3urJUN9iDN7rO0+gPiZ5K0Cj3KUcfiJg6lPqBAAOhQw9CM5gadXrpgAHoOipH/UM5QifkOjCVE/piLo4cpTUPxCiIrD0IzlSKfUKB0YerkEoFtyLmEmFqGGhLHgCRM7cTcTkPVjPCqsLEVh+IzmCpB/TAAPQfsUvuBd9coyPGfOf6gvTZxJPadvJKc7eNvN/8vq+zeYizeX2by+zeX2bzEWby+zeX2by6zeYizeX2by+zeYizeYizeX2by+zeX2by+zeX2by+zeYizeYizeYizeX2bzEWby+zeX2by+zeX2bzEWbzEWby+zeX2by+zeYizeX2by+zeX2bzEWby+zeX2by+zeYizeYizeX2bzEWbzEWby+zeX2by+zeX2by+zeX2bzEWby+zeYizeX2by+zeX2by+zeX2bzEWbzEWbzEWby+zeX2by6zeYizeX2by+zeX2bzEWby+zeX2by+zeYizeYizeX2bzEWby+zeX2by+zeX2by+zeX2by+zeYizeX2bzEWby+zeX2by+zeX2by+zeYizeYizeYizeX2by+zeXWbzEWby+zeX2by+zeYizeX2by+zeYizeYizeYizeX2by+zeX2by+zeX2by+zeX2by+zeX2by+zeX2bzEWby+zeYizeX2by+zeX2by+zeYizeYizeX2by+zeX2by+zeX2by+zeYizeX2by+zeX2bzEWby+zeYizeX2by+zeX2by+zeX2by+zeX2by+zeX2by+zeX2bzEWE+mcvs3mIs3l9m8xFm8vs3l9m8xFm8vs3l9m8vs3l9m8vs3l9m8vs3l9m8vs3l9m8vs3l9m8vs3mIs3l9m8xFm8vs3l9m8vs3l9m8vs3l9m8vs3l9m8xFm8vs3l9m8xFm8vs3mIs3l9m8xFm8vs3l9m8vs3l9m8vs3l9m8vs3l9m8vs3l9m8vs3l9m8vs3l9m8xFm8vs3mIs3l9m8vs3mIs3mIs3l9m8vs3l9m8vs3mIs3l9m8vs3mIs3l9m8xFm8vs3l9m8vs3l9m8vs3mIs3mIs3mIs3l9m8vs3l9m8xFm8vs3l9m8ut5piLN5iLN5iLN5iLN5fZvL7N5iLN5iLN5fZvL7N5fZvL7N5fZvL7N5fZvMRZvL7N5iLN5fZvL7N5fZvMRZvL7N5iLN5fZvMRZvL7N5fZvL7N5iLN5fZvL7N5fZvMRZvL7N5iLN5fZvL7N5fZvMRZvMRZvL7N5fZvL7N5fZvL7N5fZvL7N5iLN5iLN5iLN5fZvL7N5fZvL7N5fZvMRZvL7N5iLN5fZvL7N5iLN5iLN5fZvL7N5fZvL7N//mcMFB9THEtUk+Az/s7AD1P9AQB/YWAHqe5YHLx9xgBGA7tgSPEdDAE+AjgfmZYpP5++wJHj0EAf0GJre3hVgZYrlGyYKc8j/qKslVOa5GUkJfaQc2iHrEOYOcszfgUZmWAkeZT5uhgqKMyTLm/VoOUYMrDMEdoMfW/Cgl4ZR4jwMs02Gsv+GkRzqbiEJFaDNiBnLvMgYBRmQDLQyHxloX0HiTLf5nCwyJjBVUZlj4CXN+rQdMt118SDOW2ObcRlV0khWbSCB0EnqW02fqjl34UGcuDEeYHsaHK11LAQ5IgzYw5rYupZ4AZx/P5eIxwxXzCWBEj5MeMZA9FhcrwDMSwOvqJZkT4IBmTLM2VkJUjI+YT4UpCdP2mAlru9l2SSwIkYh24+ly7r4hBnHDgS0Bj4IBmxln0+FhkZc6YnsIKRn11opbUuXRfcMIqZFMvtTyPoIjEuKK9QQeojhwZcF9F8SY+VnC4yPQSETxyEJd38ABLAvoJbps4XGkmEBQMyT4CXEjiCHTLA1eWeYl1n8J1WQGmeV1DD8jCRWgzYgR9RI1ZKM44aWgeixiLTxiMFRRmSZeQOIodMddGnVqz7MvWXM3qyoSI+uvLPUIc0YZiPqak5PCTbaMxLtOfgB2sY2Vp4xCQmYGYEsLWnhGYj9p8FAzMsJ0215rPig6LDYRwDMCOHWXaeoID5iWkWcLDIxgqKMyx8BLzyHTPjb0WhAdzGK2NxifOWf5VUYKoUEk+AlzZcWg6YweoKW1L2zyt4GPndV5hCS9pyQAS4IDLsn4XGmMFUDMky8/qCHTDmGQdHA08r3ZGawfwMsNpw3kc9FoUnwXxJlmVnCwyJjhUUZsx8BHb9RUhZ8bZZkeBRmZZmVsTNSIxFYRBGLWPwiWhSfBR4mWfT4WGRMsCVL4sYzegLKQscBAMyx8Mpef1BDpjBlYZgjwMs1OPEKJcrr9qXac/BQM2Msys4XGRPQxF1qfRlQGIFWbsUmrX1uVoPHLPCwpG01ldWt+yOf1FSFjfWKRWUGcud8SS3aR/p75k+yCRMmuvckmDTruFVg4gegZLfb9OVJ1enLTLGIRXdA0Aa24a3eAAXkrYIM00Sr6pNSmeYYRDEB6ysMxgyTzASi20o2ioKuoLMHiK8TW4atuqIn0RibAHlCdXp0x2evMsA0UZ/xb9I+kF1L+Yh/mU1lD+sTstdGtgBtu+kzQaVvfq7J/jPOCfJWcJiDW9jQAC6ohpW9lVNQIRR4kzA4jVp/lnqYCLnIrmBxHlGo9TKba8PYutQ6kQamrt0JEAKOJ8KUgB7J/kGHOrC169ErGa1syn0YCeYDRDkSpAMwjg9YT1wXMGMCbPPMPdba1pCaU1BVmFxCX12glurInGDABnUnQB/23Qi/TqRm/EkQaaw4YCUW2sH01hU1BQJhMQmJqcFH6oidhZAdx0INTVgkwZmlurrBmS3VHWjCdhxbVq5lKaAuWWU+pGTqIoy6jo4IgL2oGcmfRrxSHWszOIC5IBm0wd1ZS5crWXTBqddDleKYVsPmmhgyzLSasqpg3pITQ+aZqZpbDujfHMZmefCM4h/72hg/wCrzTyYYCpIoPUHRWJkttX01YTzWdWTEUZKIAzi4okUB0cT5YmQUgg/lMNdd+hJUahwGL9WRpigOmKQAidi32AvKE6rTlllPsWqOjtvUfQAmCtQrYpFjJpnxtrn+VVCQMRaFeUJ1enLKdtNZzUTwwljuv6Yf+7Liw/qOYnamDSU23JSNNSouqYPELiEINbdURPonEtWrSterC5ZTyoOlC5F3lE9h26//PCP4nEtqYcI6M3xRXygayJg7ail31rLpnYmIxAV5h06vTpn2bYAzi3QkUK4cThqiDyAkzD3WuH016V1BRMLiExNVgKv1ZEr16bBYa+ISg0+A0WLPEKpWYRqMk0MGSMDWlRNZz4jMFiHscZ2MK/EmUW103VkWBkIGcwzX0GvT2DMpNQxNXkBGg9IGfVCf5higuMQyiV62qfWycUwrUZjTk6QBq1w7aPj2ZT/AM3+nlJysM8GUiVWGoOTVYozlLJg8K2vNxlqboGd1D60mDv/AIkDy6IQlmIVs14VMocdUT1diDMESk1YXDD6GoZFjPKKJ8ozx/g1nyV6KDbhMR2kgZ6TMIzMXGt7EyULADfQ4sWYO0YnhCwKLHOYXhWUWZX4nOtwOKVGw32aB0/UX4hb/wD2w5K1ZQSmzJCerdVzBEpavC4byah5jKjYtQKOBMLYECg2OwnyVnCYNLdY08oqlfWpo0WoJgrGxDeAKRVGKKB8hxzDWpdWNJySYYU0H6rMZMRKWfB4nhGeRlNgorKtY7KQSScgBPAVJ0U2AW3ko4ErL16dNyCYa5r7RpJKkBZ9kdsy1ZHLP1mDYXD0TMGUmnD2gCtJhjZhbnLo6jOYQldWdj2LkAIM7PNK7UspRc9Q6KnenqtDaYhycqQIe0Ur0Yc24TEHVmoz0GYNiSc3d1yAHRwwZfyllJtwmI4RmVMpsysI6yxlyCifWYbQa/x0TB2jEKMtAWJotxHgvoJS7UPToJWZgWIGAPYRnOCU2ZBB1ToCQyykpTUhSkGYc2UXBdDAZ6ZhrOoW5dTkSoWOgXWPwmDse510hDXKw+JrBIUzB2G7Tka9ETR1lmpU4RAepxpr1wZ2UWixJ9be5saVGzCYoZvp+yZTZk5HWOy5ACAtpKicIlJbC3HUGUSmwUVupZipE4BPO6ELMHatyLpKBJVoZxFIBKxPDFpBnfhyHEwVxxXgK9P2oMnLjooe2mysBWAz0zDWdStil3KwEnra4CT/ABVU+vw7CxZhLRieDTF03Yh9WnhE8mORan5hFzfDaHX81n1t9hMw/XYe9f5mQzKzBu9xIzZ0yAEI6+oixZhrhiANPVhImh2QE9HwLT5vuYZ3rvINdgXVMPYKEvH0iPEztvosFizC3fxPgKtB80B1myUs+GvbWGQSiwYetwWZlILGD5U+FYmGazC3nVmozyMwhYlgbHtTIASkWjX/ADR8dEwlll7jIA1xBZdWBqUzBWvcV+r0RtBtDnLh1TDWh6vohwuYImG6vCAdhYZEmYUtg3X6LIJhXQU2B3uK6OjhnwqE8TjIMicUZSLBq+mPjpmDtsvcZBdEyNpqKGYZ1vDv+XFMKSrLn1n+nwD7gGfQAe4AHQBn0gdRQhP4l+jD9a/BFCXOuitOBegA9yAOgA5dI90AdIB6QB3IA90AdCjP3E1O6ZKIuTLWAwPQMx0gZ9AGfvADpHuAZ+6Bn7gyHuKM/X3gB0AZ9wAR0gZ+vQPdAHQB0AZ9AB6BBl74A/8Ai2GI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtK32iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaVvtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaK+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtEfaI+0R9pW+0R9oj7RH2iPtEfaI+0R9oj7RH2iPtAw/MQ934nsE7T6/sn27v4L+yniPdcH7KeI91wfsp4j3XB+yniPdcHuhzYh4Zr5ZYHX1HvsFHqTlCCD4Ef1IJuAJ/DpdSw8QP6l1bLxyOc8iCAaXGY7o52UZh/fsXXw5/7S4j3XB7qKwybzCYKnafU2LHCIJh7ynHHDCOErUZkmYS9xxARwQspfwDapQ+sJo19AJWtSxA9BEcF/LV9uUvS3rZ0A228CSp6C3H0I97jgiGu7heOFRRmSZg77F44/YPMvxlbLdTxQEqIpCkkaTKney3ygRwiCYO4rxywFIrE2+BEoe0mwJrHllmkTCXKvGYQUapiOhGZVIBCym0i52IAgZS65hT4iK9zjgiGq3hfpfL0EwV4TjMcOjDMGP+lfiZg7gvGY4ZGHmEJLHwRZhrKhxPGBUjMETN7PlpKrKP1w5gxHus4UlTU2njjBUUZkmYW64cYh/UD0VuwZtICyix+seeBQGBncoSK0/VKmptPH0I178KRDVZ6P02aRMHdo44wZD8ZxvKnL3eU/DorcPT5iY/6U+JmDuVeIz6dQrNhI+IEpt0aNOn7UrZQSQNUDW28CSs0ueODMIpYgeglb5schX9qVlPgA3+zOI91we7Q1uokaVmAsX9UGT2A6FhIorXUwlKBMsguQyn1GIHljEI5BaVqEAyyn1WJ8yfqPR8odHyXiAuXIQxQLKl1CHN0zrl6pZZ8xwXj67kYaXCmeZ8PAacR1hzLLCHuq8GqaDyvqeOlJAAKPMx13mybMTyYn6Bn/AItupp/4b5rO1MMMkhPUoup5SoTLLTkMp9TiB5Z8WMqQBR4gT6jDJ5YoNZGWieQK4Xp42ni2SCKDbaNbGdl9JDFoe1qwT0WZdVxuAkvSzMEaQpM8gsn1OF8EMQFD4iXtYrNqAIy0wa9L6awYoyI7DlmRLmsVAWzIlyjFPYfPMShuC5pG7a80BiGu5n+sKy1LHqOYNbAGAkhlYgcIjLS6p5XGQnmt8SG+j0/OnDEBssBOqKBYtoGqecoJiE/iX88vT+JrYeXMEzxepSejtowo8srU1kZacuyXs4sOoIR5Jxv0+rSrXhlEuRg48pyBmfVr6nOcE8UrJEvX+LsY5lpehxSZFCJ9Z/DuGMQFzYRB/sziPdcHvAa8iwM+uw8JFNq6SYwK5Z559kOdOGEBNakBowZGGYInkw+RY/pPQfBOj5LxwHRyQI2TuulRAdba7Z/OvNpB1Smmu2xxkEABn2aQzGU1P+YAaEhrHGqsGEhX9BMOn0x51l2vDKCZ56HDieSmhOYieS2lgZ58Q+qfUWAq0YFcs88+yHOnDCfM6PqcQscBAM9XwynkIcDp+1cYM28889S6CIc7ryAFnmWsZ9DnqE8qcUwVKBBnrZRPjbDkmI8rRwEUZkk9kpdUU6c2+1CFPWEpGyVREetXUqNUw9RxNdh84mBpRBK9FBBby6ZTXYUOkhlyMtKWl/IGmpVbIEKM+0zDVnWM9aS3XhyG6fEWThnDPnCDOzQGmFpNwGVgIGqYKldRyACCJpTSCq5ZZA9HZVivBjHAQDMsfDKUvprbTrbwM4zPJL0CZZ5zy2KxHNBm7jyso0xBSVUkOI3lfSsIAIyHLPM1cw9RuQkPqAmBpX0AWVgVNh3KDy+InzT/ALN4j3XB7t7s9vinwHRiXV+AAZGICs9oXirhi/qaAMp8Zj7kr4YCWbzOfExQyMMiDMRZoQ5ivo+S8c1Wo76bFmJe8iDsyyymLegt4hJinNreNkOYVNMxVlGrhlzX38Txc0M9o3rXwwEs3mc+Jng6kGWO+tsyWljIVOYInlrUARQUntC8VcMX9T/Eyx0NJzAXoX9LCY656eCWnPRo0ADSB03NVd6pLDYFGRZpiXocnM6JY99o8C/STVavg6zGWX6fBTLXfrGzOqL5fK0x9z18MQKi+Amddq+DrMbbcnCYoVFGQAlhqv4kmNsvHBFARRkFmKegnxCS58RYDmC8UMjeIntC9K+GAs7eZ28T03vS7ebRLWAZAuoeMYstYyBMtdND6s1naAMpiHoc8EufEMOPpXw8rT2he1XDECVrLGLXkkiL2CYmy1B4IZaxNw8uQAEzSxRkGE9oXunDECovlWE12qMg6y9rBqzzaXvTb6pMVZiCPANPoq1ZTs+AIyljMpctmf8AZvEe64P6FtOtSssLgEn9i/Ee64P2U8R7rg/ZTxHuuD9lPEe64P2U8R7rLyfGMsZIyRkjLGSMkZYyRkjJGSMkZIyRkjJGSMkZIyRkjJGSMkZIyRkjLGSMkZIyRljJGSMkZYyRkjLGSMkZIyRkjJGSMkZIyRkjJGSMkZIyRkjJGSMkZYyRkjJGSMsZIyRkjLGSMkZYyRkjJGSMkZIyRljJGSMkZIyRkjJGSMkZIyRkjLGSMkZIyRljJGSMkZYyRkjLGSMkZIyRkjJGSMsZIyRkjJGSMkZIyRkjJGSMkZYyRkjJGSMsZIyRkjLGSMkZYyRkjJGSMkZIyRljJGSMkZIyRkjJGSMkZIyRkjJGSMkZIyRljJGSMkZYyRkjJGSMkZIyRkjJGSMsZIyRkjJGSMkZIyRkjJGSMkZIyRkjJGSMsZIyRkjLGSMkZIyRkjJGSMkZIyRljJGSMkZIyRkjJGSMkZIyRkjJHSMkZIyRkjJGSMkZIyRkjJGSMkZIyRljJGSMkZIyRkjJGSMkZIyRkjJGSMkZI6RljJGSOkZIyRkjJGSMkZIyxkjJGSMsZIyRkjJGSMkZIyRkjJGSMkZIyRkjJHSMsZJxHuuD9lPEe64P2U8R7rg/ZTxHuuD9lPEe64PdtQH9QhBH4dzYoPoSBGU/kc/6FhqHiP2EeOo91we6bD1OMcLk8xdydURrreZDXUHmJBMsV0YZhlmJAfhEvBfhl5rxjgGqX6sUlCNZ0FzSMLxTF4inEVKSn059b5WmIDMOGXrZXMSEZvBR2tL9Tr5gRkZeKUO5mIyt4WlpGItXNFI7DLNClgscGt0Dg/gY5ZEbSZis3/AS4WVn4iWgPQgLywlEOTu40zGR80NCfsI4j3XB7vsp8YHxj6tMwqYEAg2hodIvuSiYeplCafKMzCTQn064gezrylQMAqvpvQRASUpfmlSBzhKvADh6MJbfnQBornsPEVu4067Jb9husf8AVKWxPEVr6yUmmh0DhDMA9+Fv+ptC6wkcDFVgjT5JgjisCtOkjgMIpxNRzQfVNPHC3rqjfyTU+JfbJJ9dhdeFh03X5b2Smt3esFyQCSWhyw2Ko60JwmDWtdKNog0HFXis6ZhqmBrGrNZ5EoAr/YRxHuuD3f8AMn12GIFv6IQbUIurEw1yYtPPTolJquxhGivhSUWHAYi02VXSlzhFsD33lYM7Ai6R+mLYmJw9CB9Y4ej/ABOj62ygqswVteMqAR06rzPKTU9lQOiYLRhj9Taq6gZgzTRRmb7tGiYIv7OKfWqMyHmBc3paHe8V6ABPt0zxroqwifkk/wC2xmLrxRhAYAMn5pMPbTi6AEI0eaUmqo19VhUPDPkJPr6211zDXJjUGg1aCSWlfV3W4XWV5f2EcR7rg7hFLDwOXb0AEehigD0HQBmfdUavXLoUEehGcGQgzEUAegGXuopI8CRn7ijV65fsJ4j3XB+yniPdcH7KeI91we9Tv2SleaVLvKl3lK80pXmlS7yleaUrzSleaVLvKl3lK80pXmlS7yleaUrzSleaVLvKV5pSvNKV5pUu8pXmlS7yleaUrzSleaUrzSleaUrzSleaVLvKV5pSvNKl3lS7yleaUrzSpd5SvNKV5pSvNKV5pUu8pXmlK80qXeUrzSleaUrzSpd5SvNKV5pSvNKl3lK80pXmlK80pXmlK80pXmlK80qXeUrzSleaUrzSleaVLvKl3lK80pXmlS7yleaUrzSleaUrzSpd5SvNKV5pUu8pyH4Q90/0z4KO0z2dij+OiezMVyT2ZiuSezcVyT2ZiuSezMVyT2ZiuSezcVyT2biuSezcVyT2ZiuSezcVyT2ZiuSezcVyT2biuSezcVyT2biuSezMVyT2biuSezcVyT2ZiuSezcVyT2ZiuSezcVyT2biuSezMVyT2ZiuSezcVyT2biuSezMVyT2biuSezMVyT2biuSez8WB+iP9Ie92mVCUrvKV3lK7ypd5Su8pEqXeVLvKV3lK7ypd5Uu8pXeUiVLvKl3lK7yld5Su8qXeUrvKl3lS7ypd5Uu8pXeVLvKl3lSbyld5Uu8qXeVLvEKffviPdcHufCedvD8B90wB2/SHcjO61tKCfTubz2HxP9j+hiFH0XEGVqMVcfiPc+EOZP9kPYfv1xHuuD7sehnp3HglTMP7L4fQO49zjH9l9fv1xHuuD3PUfdThM9O4+Qf7L6V+5xj+y+v364j3XB7nqPd+PhC20YEe8wJHj0eELbQ5/0HgTl/amJZPhCA4lewzlfZ+WU7Dwn3eEz07j5B7pwqjxJmvL10ywMD6d44UschmfHvfSv3OMd2jbSpT2SodpynlbpGZmX5ZwEH+i9fv1xHuuD3PUe76mIDPIrdkq1lfFicgJXoY+WDMk5Aesww0fn2zJiR2CVAkv9Pt6OIQAkqMzPKVzIlZezYCVaQ3gwgJJOQHqZhgE+ORnlyzmGGn8+2L/+xnYCuZigERR4ZsYnWP8AH0Eq0avAiAEaspTlUftHxlesr5jK9D6wfXMf2rzHxjfTHiIxVx4EHKfSQ+s7PiROxl9zhM9O4+Qe6+ow4z0+pMrTRllpyGUdf4Z+0JmcwZUbbn8EEweitjlrU5xQ0wOuniLZFvyEQo6nJhMObrB5jnkFmG6prPKwOYMQMr56vUATCiugjNSTML1nVnJnY5CUdVdpzHbmDKQAtn8rt88qWoj/AM2YmC11qctbNlnKzVcnmTuvSv3OMd22Q0zhgGU8SenzucgYxzgJsB7G/CPorHiZbmw+BGUOWctyPwAGcbUjeDQ5qwjZKoz7v1+/XEe64Pc9R7rZHOYlmA8QJ4Fc854lznPMLBlPDI9HlFhynH0eomJAGXpGL2N4sZWCyjtYzQBrGQWPpZWzWVBlHiyw5BgCDAtiDfKDLxBE8ytpH5NPtJoM+JyWebXkejx1rnOGeOszj/tQzGcOfZGydTOxx4GHsUbz6I9zhM9O4+Qe67DYAyzEmkg5kgZzGNcoQlotbWAfbleG0t6EzLUgRTPAVgDafMiUkm06y+ecSgaHDAqTPmzwAmGTQrZdY5hTXpbLT8BkZ86cMrw/V6fo55xalAQq2g916V+5xr3fDOH3fssc+gfRzyzngSZ6zw1T1nwfsnihzE8XOZ7v1+/XEe64O5yyRu2eBEA0HyzIqe0rMgF8qCHS6doMUDsyLdGkq57ejLMT4AdABD+IM0DS3lzj6XU5iBFWMVIHYYo/VDmSczMurORI/EQjWCJ4/GAEkfSSAKinPSPjMsw4Jz6NLBvFTMhkfKD/AGsZrAyH8IzZiHJxBkB7nCZ6dx8g91ZovTytFTP5kfXe/maOBcBkwPgRClKZjUyntna3ZlnPEKBMtNr5rGUizzIY4prQ6tKHtMA01vm0+IyipZSWJXV2ERgeL8OwjIRgLan1LEUE5jJZptpzOnV2ES76RGQrXyjuvSv3OMd2pI0ynOIMz4iAADpGanxEL/lBpRfAQZrnmD6Qlm+Gc9YSrAkHL4wZIIMw3YZ4L2Du/X79cR7rg9z1H3NrErAnYR4iebLt9zhM9O4+Qf7L6V+5xju3Mcxs+5GeU+J6KwHyyLd56/friPdcHueo+6nCZ6dx8k/2X0r9zjH9l9fv1xHuuD3fiPun6T07gZis5WfpMYFSMwf7G2SqMyYCDe+ag8I9zxHaP7L4L9+uI91we6uaE7RhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhGEYRhBkg8B3IBBGRjCyr5LGey7dxPZls9mWz2ZdPZlu89mWz2ZbvPZl289mWz2XdvPZlu89l3T2ZbvPZl09mWz2ZbPZls9mWz2ZbPZls9mWz2ZbPZl09mWz2ZbPZls9mXT2ZbPZls9mWz2ZbvPZls9mWz2ZZn+JjBagcxSkGQHu7SppS0paUtKWlLSlpS0paUtKWlLSlpS0paUtKWlLSppS0qaUtKWlLSlpS0paUtKWlLSlpS0paUtBpHxzg+/XEe64PeQRBEErEQRBEErEQRBEEQRBEErErEQRBEErEQRBEEQRBEEQRBEEQRBKxEEQRBEErEQRBEErEQRBEErEQRBKxKxEEQRBKxEEQRBKxEEQRBKxEEQRBEErEQRBEErEQRBEEQRBEEQSsRBEErEQf7S4j3XB+yniPdcH7KeI914r+yjxPeHSY6x1jrHWOsdY6x1jrHWOsZY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jLHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsZY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jLHWOsdY6x1jrHWOsdY6x1jrGWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsZY6x1jrHWOsdY6x1jrHWOsdYyx1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jLHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsZY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6xljrHWOsdY6x1jLHWOsdY6x1jrHWOsZY6x1jrHWOsdY6x1jrHWOsdY6x1jrHWOsdY6x1jLHWOsdY6x1jrHWOsdY6x1jLHWOsdY6x1jrHWOscTtP/APNKZntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntA20z2me0DbTPaZ7TPaBtpntM9pntA20DbTPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaBtpntM9pntA20z2me0z2gbaBtpntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9oG2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0DbTPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7TPaZ7QNtM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntM9pntA20z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z2me0z273zHwnaf2T+HxHd/Bf2U/Akd1wfsp4j3XB+yniPdcH7KeI91we9avX8HuWaNbZL+J6X01r4mNqR1DKfUHvCTf+X0f6u3+cfBAI+hMwsxJ5Zejt7lgDv4D+ityst8g6frzt/tjx1HuuD3fCtC0J1HFZzwdc4QABmSYWc8SiWa0N84RGLv6IJZ5Ss+SnQ4VVGZJ8BDY54gscOISbbfDIR3dgcjoGYEtDKPMISLgUaF+uFYhPWOMxwzN7H2lA/jwoOrTGCqBmSYzseICWBxHCVjxJjWckcMh8COhnFt1J0ZCM5ckxgqqMyTDY/qwEsD9AJCDM5QkgHI5iHJKxmxgbJvKSI5d+FBGys4XGRnkrUscvQTMhiQCRCQk8gq6wn0XLON2Wcxg7G8On5yzA0EtSn2BB1T1sPLPNYgJ6SdNAySedV0dDl34EEf6fo4yPQxsYcAlmZHiD2NGAUDMkxnf1YCXBx8fWOErUZljBafxCywOp8CJbp9B4kwupPFDmOjjPTSP4ogEHTHCIozJMa3klwevI+Eaw1U+eHNU/wBp8R7rg936zEPNH1Xr9qeeh55rnlSNxMRmWM+qts8vw1RsrNKgRA99w1EtAEJIV9I8wM+SnRb1dVhDWtLMPpAlobDYgQ5EkylCMsiSMyZ9S6RBq6xJWgbqgcwISBYqAyhAR6CcAluiuw52GW4Y5LLA2GxAyIB4oCcMLD1kFJzXLQcgZa7K7Z9vQoLCoxF1iMQLSNcoTSBl2ifUYn7H6ujwIynlbN6558RbPFEFaRBZfeNRLQCu6ojMrP8AxMKWnGZ8yf4sQO5cgRQq+g6fjcsubMVKCAspZgzAs5E+wgHR2FhoSadVoLPGGknWk8+WlIge+4ayxli03I3mE89zaJUjWsgLsRnmTLkpK+dR9qPoFxzdpfh5ajU3kK6g8U8gtHWAQVeUDQZYxFuwik4c/VSxD6OviIzMEGQLdHEen5aQE0izOyClgRloMtZkc55HwErUAgZgCIq/kMv9p8R7rg92wpTQNIYDhntLEywvTeuWowfU2w5q4zBjAlHBaeI0PCNSIEhGu1xPkL0JnhnAR5hllajEV9qicR6OCcYnh1Sz/wAvRwCLnh2yV5hwQRmMjFVcSh1KI6Z3eCMJnU54JZrFLZK3R8ppxT6oEAxgVYAgj0MyPUEFukeVwjz6nCIFSDtBDxgXrQIZ57TGAsGEeMNaMYw1l5/izjPTwz5yzB1FjSpMpSseqgDpb8XntHES57R1g1ap+FohzatBXPtnIACAkVWB4wIZBCTbb5VAik0g6HlGoMMxkYAt3mQRwBcdIBGYM/lN61y42VVeUwJZZWMyDLXpesT6zo4j08Cx0zu7FVhmJnU//kl3WrR5X/2rxHuuD3G0sVIVss8jLute1s2OnT0XdTZU3YQuqAWKVAM9rWVU8ExZFyWmyyxl1GydoyyMxz4bV4pMY9uJsAHXMPLH1dUgXV0Lms9sW10cMZrb281ry3q+oPlC559F/wAPq9M7UYds9pO9A8KjL8upHk09F/igHVwArPa1qU8Mdrr28bGmaOnlYT2zb1UB9WY+JPQuYYEEfgZj7K6deo1QZqZ7Ytrp4YTZe/ndun0+j+qDJ7zrMGYIyImPNGrzIJi3xVi+XV0Y6zD6vsiYpzczBmucajLNINHVBpbr0knVp09OK8q5BJZ1WTBtcbMVoFz9cvcxHWtZ5V0AaehtGfg0tFoQEasssxPaDYYt5kWYpsTaPLqiBkYEEGe0rKFb7EvbE4njaDUreInta2qnhlr34k+NrzMEHMET23Z1UzZ387t4mYh8NieJZ7Vstr4IoVFGSjoxXUdTPbmIntO29dBGhhMT9gLo0wlSnlIntmzqZmWbzufExijp5WExz36yCC3+0+I91wf2n2g99QcsK4MgBkB/v/iPdcH7KeI91wfsp4j3XB+yniPdcH7KeI91wxFiLEWIsRYixFiLEWIsRYixF3iLEWIsRYixFiLEWIsRYixViLvEWIsRYqxFiLEWIsRYixFiLvEWIsRYixFiLEWIu8RYixFiLEWIsRYixFiLEWKsRd4ixFiLFWIsRYixFiLEWIsRd4ixFiLEWIsRYixF3iLEWIsRYixFiLEWIsRYixFiLvEWIsRYqxFiLFWIsRYi7xFiLvEWIsVYixFiLEWIu8RYixFiLvEWIsRYixFiLEWIsRYixFiLEWIsRYixFiLEWIsRd4ixFiLEWIsRYiytd4ixFiLEXeIsRYixF3iLEWIsRYixFiLEWIsRYixFirEWIsRYixFiLEWIsRYixFla7xFiLEWIu8RYixFiLvEWIsRYixFiLEWIsRYixFiLFWIsRYixFirEWIsRYqxFiLK13iLEWIsRd4ixFiLEXeIsRYixFiLEWIsRYixFiLEWVrvEWIsRZWu8VYixFiLEWIsRZWu8RYixFiLvEWIsRYi7xFiLEWIsRYqxFiLEWKsRYiytd4ixFiLEWKsRYixF3iLEWIsRYixFiLEWIsRYixFiLEWIsRYixViLEWIsVYiziPdcH7KeI91wfsp4j3XB+yniPdcH7KeI91wfsp4j3XB7ulTX9dfwz2mb24XSDRfUcnSOua+b8JYjfpOcfTneBHAJHYM+09Dq2RyORz6LNYS3TXPaLUpTkQNIMxoxNQ86ETyugPRciegYgQ5gxgAPiTkJeNSjsZSDDmzVgky5Fc+AJAMsHU9Rq0Rxqyzyz7ZarlfEKQcpciE8RAhjBR6k5R1ZfVTnCAJaj5eOk59Dqufhmcs+h1JXxAPaJaiZ8RAhBB8CI41H4Z9sIAHxMIIPgR0XImfEQIQQfAwgAeJMuXUqEhlIMfNjVm0tRSfAEgEx8qjRqKdGg2BhkrGOhzRSQD6x1QficowYeoOfQ6gt4AnLPodSR4gHOXImfhqIEOYMddWWeWfbGAHqTlHUL6k9ktQO3lUsMz0WkV2fWS81G23SSJ7czb8UiacRScn6DkqqSTCf4fFa+pHQ6qPVjlGDD1Bz6LUfLxCkHovQPwlh0WKi+rHKMCD4ERgo9Sco6sPUHPofQbLwD0XdU7Kcm9JiOvYDtslyKfgCwEIAHxM6oLU2SHPocAnwBMIAHiTGBB+Ilg1AdoB7RMd/EHU7B+GOHcdZk0s12FyOi0GuqxQglivl46Tn0XoH4dQz6WCj1JyEsV/0nOW6C+JRGliKT8CQCei0v1V+msGOqj1JyEcMPwOfQU1quahjHQWPWGKgxgq+pOQlisPVTn03IHPgpYA9Dqi+pOUcMPUHMRwC3gCfGWp1nDmM4Zcmj1LDKMCD8RDkJaj5cJB6LUVj4KSAYQB6mdV1dRGhs+hl1pWzIGPiQI6CxlzKAxgo9SchLFceoOfQ4UepOXQ66uHOWKi+pOUYMp8CDnHAJ8AT4w5COpXiz7JdWqt4EsAD03pr4dQzhyZamIMs1NozJMIIPgRHGoeIz7YwUepOUdWHqpzmgmy3S+Z8ojqw/A5y1Ez8AxAhzBjrmBmRnLFfI5HSQf8AQHEe64Pd873sW6PIwBIjt1GoGxAfOZqqBuFdiAkghp8cSpmYswrAz7Vea/m089v81/zbo/yJcKwXAE1X3XDQAonmRADDkSOwzFLdexJLO+mXa8KB1lcc/wAJhDp0ephKI2QsTiE84pASarr7lDuxPFLS6JTlWTwRzWHw+T6eGV6fozFpbiLhqOq3yzEddhmr11fS1aZYRSoBoUkhZisqiuVlanUrS0rgiu7zFLU9bAtoJcMOjz1HrBPk6jO224vcZnbZaSVGZAAhzweKOQThM/xnnBPkpDkxHYZi0uvZsyzvpl2vChdaRyMJhfMnEZqrXTpsUEkMs+sFI0fmxmd19w1kk8UtLolX8sng6KwbwVXVK9LNSr2HMnwGczs1uRVXmQABP+0vfQ9fR9bhnFonyNcX+dbnZzQG+60aiSxjlsNdWXqHCRPH+D6P/She3F5I+ssek5Bb5iw36QZWUW/IVqej6zEuKxMuswRRhBkj16zCXTWVqqzIAAlwGGb62okmWlKETXd+MPVYpfI4McJfaRXr4fUzGobyO2w2y7rf4d8ks4lj/wA9bSER2KqFmLzwtnhV6GWEUIAaVJIWYoCojKytTqDdHxxInoJ8lp9aKWIlmvFP9brsIbVLv4iphkM+GUjTTYNA1Ho8+GuFiwHViyiiebCuQZ58TceWfPtnFdPmt0Owwwu1WoD5yCdM/lPWRmIdFmJIAbhmNRrz42db9qW9b/DuFW3iXoxf8gL9CknQJcAxsC2orEgrPN/FLC1uIbzuWOZMfWaPI0+OKmKUVJYUppL6RMUDVbYEtpD6gehM3prOgykLdZQA7Ri2GpsNdVQJAh0JrAsTxBngyg7z6xyESBrb3GbuWMuFlKkmr1CiEvXrKVV5kAQlcPbYEtqj6HLtA4vVCws1HPMRtTaGUmZuikrWkfPD2VdYqTEinDVKDZ26dZmLSq+oZjTb5ocrrEAWa7b7Bqdyxji6huyoEnMLKc6qD9ATwAyErzeql2UyoC4p5oxOFofQlYJEJrCOBYnEDPBwGEHaF1L+oQ/TrTS0+2zBCeFZnZrcipMyAoEY/wAJiH0OnmE4p8oz/Guha06cqwSQEAPQ+izE2adUYi4L5xxR1suWlxr4hPkT5c+UI5GGVAaASQkxQWrLKytTqVpUNV9+Vkq0qql9OZOZmKrsxF2ZINvlmIFuFtXUg1atBjslJrGscUQohOZGZP8AoDiPdcHujLDXuXqeYlG9ApiFWvOSKeHo/wAtJ/kpO0MuUz/k4lus/QnT/kRFMrUdH1grOmFLL/Gw2GKBT/D5JPo14o663McWO/Dwzz6EYS4I9SBGVuITMVdUQh9QJ8mecpKKRiEGlw8wi6qh9aolAD1eDWRyyO2VyA5qBKfOuYLj6Mt04hrQOrRswRPHIZzwIIni+LUJ+hp8nQghFd1H0CrzJq8KddjzMUmt6y0tFjvwz5CT6wVnTCll/i5tMQLV/D5JDpqxY1K5jh3sHgsGbdUGH5rL0Rqk0OGn1QqyQ9Nivqw+l8vEahHFV1Lnzw6xVZrsceA6BmrqVP5GZ6kxhDfoEGZCdg/THSp6xlYDPqMLWV1zJaradOqWCwkjPRP/AEZ49WvSAQcRMLWD0kijDVzE2zz0O6f+1o4rtrsOWqJ12YzdlPYsGVF9egvLFtc+REOZaU/VOHeuVVCYTqwDp1ZZapSq21sVIsyVpc9mE0Zt6K0o0tX5TZLdSu38+sHNQvR/kiYureWq6dQ4zEQ2aEY6R+qaUYjMkEKRHZ6UtIqZpYEZ3BXV0eFiaTOz+BD6p/8AnqQJ/wCHWJ8+2cV0+a3R9Xbdlq+GoRxbdeQqhImsYYqWlVA2BEwgqAbRqyyBy6LfI2VSMclyldYIvUuUE8wxKy5EGnNwxi5V2HSk/wAmU19aLS1bOPFTMGllz8AB09AzJrMtUulIzWNozuL1M3gQYwssttBOn4AT7KgbCfWVEWS5E4wTkQZWVqBKK/FHFbV2ExtZ60PaeET5zz5Zn/qdHwwkrDYa+vzHwDTD0WeipkSZTo0hXSqXojKgDKewgiUkVI2SWcccILSChPR4mhwNpaOuRSNMOgW2a62MYW23Ovln2K1XYdA/7plNX/unh1OmZV30sRk0OtabOstYT6upvpSwW221kKqz/HunB0LqWh/py9NGnOUGrVSZ9qgy8VmnsYNAQpo+hMOA9Y7DbLiQ5/moDmAJ5ExALSwWF0IGUoqGIrGVgeYQE1DPrQJwCPpBOWcOYIzB+/8AxHuuD3alsHCZhU1dFSrZZ5iJWHCtqUH1EqD6Dms1BCchkJUU646ageHpqCazm34mUqrWedh4t7mDrLSoB2UKzfgJUtkwlYV/MIoCqMgPQTCVl5UAyJo1+glQNwXLX0YdXaVKieglCOwlKpKlslCK3F0EhQQCQM5TlUiaKsxkT0UIz8UqVB6CVLYvoZhqwj+YRQqIMlHoB0YSvVKgHKBC/qBK1slCaX7CIoVFGQAmErLSpQ6JoDeg6ACpGRBlKoz+YiYdHaVqmfQSFzAlWSkaKQejCoz+srVKx8BKlsX8ZhKxW/mWUq1fCYMgAAB+A6KVNlfkY+MqVyhzT3KRWHObkfaPRWBYwyd/iZSjmUop9BKldT8DMKgfigzBmFrJiBVHgBKELcUqVPwEoV2EoRPUjopWzScwDMBVKglTfYErC1qCAvwAMwdeqIFUeAEw4d+I9NQRrPOR4tKVdq/L0UrXWSSQJSqpwiVhE9B0Yes9cc7OzzShFbigzBmCr1RAqjwA6KVciYWsKh1AehlQZa2DLn8CJhULxQqL4ASsJ1jamy+JlK2EShVboGYlCrYwILCUrYZQqHpwdZYxAiDwAlCuwlSrKVZ6jmhPwgzBBBH4GUqtfAJWErHgolQ60LpDytXT0MwqBujCVl4gRR4KJQjsIMgBkOjDotvEJUtkw6IeLoOSIM2MqIwuHXsYjLUeihXPFKVQHxyiK6HxBmFrGqUqKSpXR8MjECIvlUeA6ACD4gzC1looNZXSV+GUQLWoyCiYSsvKgLCukvKEcylVJiB1PwMrCAnPITDIzcUqVF/CVKLmGRb4mVLZkcwIMgPv/wAR7rg7tQ35jP8AqVBHoRBkP6VQw9CM+7cKPUmYynnExtPOJjKOcTGU84mNp5xMbTziY2jnExtHOJjaecTG084mNp5xMbRziYyjnExtPOJjaOcTG0c4mNo5xMbTziY2jnExtHOJjaecTGU84mNo5xMbRziY2nnExtPOJjaOcTG0c4mNp5xMbTziY2nnExtHOJjaecTGU84mMp5xMbRziY2nnExlPOJjaecTG0c4mNp5xMbTziY2nnExtPOJjaOcTG084mNp5xMbRziY2jnExlPOJjaecTG0c4mNo5xMbTziYynnExlPOJjaOcTG084mNp5xMbRziY2jnExtPOJjaecTG084mMo5xMZRziY2nnExtPOJjaecTG0c4mMp5xMbTziY2jnExtPOJjaecTG084mMo5xMXQPycTG084mNp5xMbTziY2jnExlPOJjaecTGUc4mMo5xMZRziYynnExtHOJjaOcTGU84mKqJ9A4/0RxHuuD7ieCCMTwoPATC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chmFu5DMLdyGYW7kMwt3IZhbuQzC3chlFqj1KECOWpcgAcJ/0PxHuuD7iccA8Ggg9we4P6Ee4PcHuD3B7g70e4PcHuD3B7g7xQQQQZ4ien+huI91wfcT5gnA/3c9D0eg/0NxHuuD3sRWp9CwExdPMJi6ecTF08wmLp5hMXTzCYunnExdPOJi6eYTF08wmLp5xMXTziYunmExdPMJi6ecTF084mLp5xMXTzCYunnExdPMJi6ecTF08wmLp5xMXTzCYunnExdPMJi6eYTF08wmLp5xMXTzCYunnExdPOJi6ecTF08wmLp5hMXTziYunmExdPMJi6eYTF084mLp5xMXTzCYunmExdPOJi6ecTF084mLp5hMXTziYunnExdPOJi6eYTF084mLp5hMXTziYunmExdPOJi6eYTF084mLp5hMXTzCYunmExdPOJcj5WL5WBnA/vOqj1JymMp5xMZTziYynnExlPOJjKecTGU84mMp5xMZTziYynnExlPOJjKecTGU84mMp5xMZTziYynnExlPOJjKecTGU84mMp5xMZTziYynnExlPOJjKecTGU84mMp5xMZTziYynnExlPOJjKecTGU84mMp5xMZTziYynnExlPOJjKecTGU84mMp5xMZTziYynnExlPOJjKecTGU84mMp5xMZTziYynnExlPOJjKecTGU84mMp5xMZTziYynnExlPOJjKecTGU84mMp5xMZTziYynnExlPOJjKecTGU84mMp5xMZTziYynnExlPOJjKecS+tz8AGB/pPQ9HoP9DcR7rg92xqsIpy1L5rDMMh/UNUwlPIJhKeQTCU8gmEp5BMJTyCYSnkEwlPIJhKeQTCU8gmEp5BMJTyCYSnkEwlXIJhKeQTCU8gmEp5BMJTyCYSnkEwlPIJhKeQTCU8gmEp5BMJTyCYSnkEwlPIJhKeQTCU8gmEp5BMJTyCYSnkEwlPIJhKeQTCU8gmDp5BMJTyCYSnkEwlPIJhKeQTCU8gmEp5BMJTyCYSnkEwlPIJhKeQTCU8gmEq5BMJTyCYSnkEwlPIJhKeQTCU8gmEp5BMJTyCYSnkEwlPIJhKeQTCU8gmEp5BMJTyCYSnkEwlPIJQiNrAzAhAzU+6uu+06UWOcRb+PlH5CYOnkEwtHIJhKOQTCUcgmFo5BMLRyCYOnkEwdPIJg6eQTC0cgmDp5BMJRyCYOnkEwlHIJg6eQTCUcgmDp5BMLRyCYSnkEwtHIJg6eQTCUcgmEo5BMHTyCYOnkEwdPIJhKOQTCUcgmEo5BMHTyCYWjkEwlPIJhKOQTB08gmFo5BMJRyCYSjkEwtHIJhaOQTC0cgmDp5BMHTyCYSjkEwdPIJhKOQTB08gmEo5BMHTyCYSjkEwdPIJhaOQTCUcgmFo5BMHTyCYSnkEwtHIJg6eQTB08gmDp5BMLRyCYSjkEwlHIJg6eQTC0cgmFo5BMOiH1QaZYbcPZ2V2HxB9D/S+g/0NxHuuD3OGeCqB/f/AJghyKIzg/p7Z9pAfc/8BERR+r7ifBCw/NZ4sgJ/pPQf6G4j3XB7nqn/ACH3A+YJ/j2T5Se5xVfcTgacH9F6Ho9B/obiPdcHueqf8h3FqsyeYDpsC6jkM+lwWQ5MB0WqLG8F6GCqPEmOGX4Ee7iU1SwMPUHvrB1hGYWXKhPqZiq+Ye5ar6fNl02BS5yUH49LhtJyOXe/ME/x7J8pPc4q/fsVFJ8TDmpGYMcKXOlc/iZYqmw5ICfHubl63g77sEcsx9Ae6sCL6mMCp8CO5sCBjkCe++W84P6L0PR6D/Q3Ee64Pc9U/wCQ9/xC9kJIxAIfope+weIQRGrsS4aq3GRhyUCYO9afmFYcwbcwejzJSH5STPtCdrPmzD8FiltJc5CAgGISzLqJ+A6G0vce0+iyhXPqwzMUmmw5Wpn2CISbc8jBmEUmIQAhbKYWy3sBbSPLFP1euYaypVGYLjLOYW28L4somYZfMG8REbUgGWX2iZhraCxyUuJU5DoRrXwWYewAJ9YR2Ho+VFBUzDoP0jIw6hRYVU9ByYjSv5meXEVjm6KLb2XzFB2CKyst2TowyIjZAbkzCXV1HwsKz57Sl2sDlQgGZMQ1eqnxEwd4p4yIwKEZgzC23hTkWUQEMnmVhkRKbL7B4hBK2qtXxRxkfd+YJ/j2T5Se5xV++30a0LvPPQxQz/IE+bGyWYHELVxkQdagGf0Z5WXOIV0RCXYZk/AdByCgkmZ5nE+H/lngREe25vCtBmZRZQ7eAcRshMFelR8H0xwUIzzmDuuRfF1WE/iCMiJU913AglL028DiBntfy1oM2MwttBfsXWOiixxZW4LqOxZg7B2Em2VObK30AAeaVtQATmGmAxBq49MbMGUWPYthQIozJyiPVYnijCYS2lWOSs46T6u889DkRSQ7hJhL3p+aB9GAuoUnITwMrJYrqJ+AiFHpOkkxSS8UsAQMhBp1XIcjMHe9I8bQsPYZTZfaPFUEreq3gcQEB1J1TCW1IACruMtUw1uIK+YoIGSxPGthkR73y3nB/Reh6PQf6G4j3XB7nqn/ACHv5katTgSi1WQgoSuQGU7W0HP8xO13c6jANRsAnke0aoo05ZZTwWzIdAzBoIh8hLoTwmeUK1dc4mhyHnSfbfQn6R0eBBA6HCljkufxM9GnBPkvB4gmAEdVPjkJ7NZl0+YNMG1KOmTQeCDLYT7Niz5U4Oj5UUNYM9IMqqr/ABj6nc6nb1PQCyVnW4WV2pZUwZWKzx6lmH5gQDU5JYwAMzjOeTMkiAFSMjPmmDt60iJrXVno4jPZDFSMiNQiMmbNpB9DMJ11WokOkQJafOMsjMP19Vraj6iUGvEAeDLk3u/ME/x7J8pPc4qveOQUEmV2u9rfZGYAgdKsRxDL6U/yBPmztr8SIoKkZZTM1i1lH5GfWUu1fMZ2C+nS/wCqeAOhOjzXHTKL8ur056J5qSRMi62aRKwTWc1PoZ21eaAEMMtMGlcwNzPZLFAgyIYCYRqa7UlIvrt8w+ImHNWIXy6xEFpRNJQzBmp9X0dYzGfR8ozggzKv2H8yZ5Ht+lFAUDICfVhgQJ49eRPisAzDLPiiw5ACV2OXOSaVzyEV0S8BWDDKfG9Yo06csp5RYwE8FJdJ9t9CfpE8mJqO6zxrtHVn8EnhZlY8+N6iKNOnLKfYseYFr2dyWs1T2c9TVWDNswcxBmMiZ4hDlPZjOD2lw2WqYF6RpIf3uBpwf0Xoej0H+huI91we56p/yHv1FR5aw0USsirXqrb4EGYdrsO7Zrp8RMOyItgC1+J/Mw5W1EMhmAcW+BY+WKe1xk3F0VnqxVlqisXyKPp9DB9LQc/xJiFW1GHJ2Br3nwXt6DpsQ5qZgGsbjWKEFf1dcGdlRmENS6fpE+LfgJWRZ1TDRFKsq9oMrJrWogtPBhMG1qr2I6QdXWRklUT+WUChohdtanITsZk0zCsoQeforPVivLVKyUQ/SPuVFWdslDekUZMMjKzoViFJ8GVph3upzJQrKCqiwZJwgHxM+upOazBPXYewu3lWIVJsMrK6riRPrqjmk9mv13r9mWg3EHt9Jg3ubM5OsQVm0ZBJh3xFTNmhXxAlHUoi5Kp8T7vzBP8AHsnyk9ziq96ss1hCmKOxREzupcMspbNbVZxwyssEszciEdfUcxPZ1i3EZaj5RDnY5LOfxMrPUWWI7+maxSbK3BAEXJ2zZ+ioiihc1J8GMUSonD3DtPwBlJtpt7bEEpfDUK2bk+LT6+k5iez7EtIyLnwEtL2Fe1z6zAPboGlXSfQRvJVwylr8O3lC+KzDGiqk5jiaIbqdORrEwbUVq2pnfoBLGsgCKVcJkQYhVXfNTDlbW2pJ7OsN3F9mHO+5tTysqGvJWVk1qhBaIXYsvYJ4hFB2lZd3+jkIo1BfpRCba2DLKCX61C69FZQm0w5M4NRn2VgJtrcEAQZF0OsH1aIwFa6K85WXIuUkDorK67iQDMOb8M7agB4rKTh8OnmDeLSsmtFbU08HUgzCNfUCdDJE6mgDIVHxJ97gacH9F6Ho9B/obiPdcHueqf8AIf0jppqOaon9s+YJ/j2T5Se5xVf3DxyjppqOaIn9H8p5wf0Xoej0H+huI91we56p/wAh9wPmCf49k+UnucVf3E+W8+WP6L0PR6D/AEN46j3XB7nm05w+ZR/f/mCDMuhQD9XZPsoBsPcIAxCqyn9P3E8XGhR6lp4qg/ovQ9HoP9DcR7rg91DZh3ObVjxUy3QRxTFJMUkxSTEpMSkxKTEpMSkxKTEpMUkxKTEpMSkxKTEpMSkxKTFJMSkxKTEpMUkxKTEpMSkxKTEpMUkxSTFJMSkxKTEpMUkxKTEpMSkxKTEpMSkxKTEpMUkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTFJLA7a1M4T7raLq/I0qap+PLNTMVXMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTFVzEpMSkxKTFVzEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKTEpMSkxKSw2n0QZmJorTtqp/o/Q9HoP9DcR7rg96pGP4gGYevlEw9fKJh6+USis/8AtEw9fKJh6+UTD18omHr5RMPXyiYevlEw9fKJh6+UTD18olFfKJh6+UTD18omHr5RMPXyiYevlEw9fKJh6+USivlEw9fKJh6+UTD18omHr5RMPXyiYevlEw9fKJh6+UTD18omHr5RMPXyiYevlEw9fKJh6+UTD18omHr5RMPXyiYevlEw9fKJh6+USivlEw9fKJh6+UTD18olFfKJh6+UTD18omHr5RMPXyiYevlEw9fKJh6+UTD18omHr5RMPXyiYevlEw9fKJh6+UTD18olar/MWcD+8oImHq5RMNVyCYarkEw1XIJhquQTDVcgmHq5BMNVyCYarkEw1XIJhquQTDVcgmGq5BMNVyCYarkEw1XIJh6eQTDVcgmGq5BMNVyCYarkEw1XIJhquQTDV8omGq5BMNVyCYarkEw1XIJhquQTDVcgmGq5BMNVyCYarkEw1XIJhquQTD1cgmGq5BMNVyCYarkEw1XIJhquQTDVcgmGq5BMPVyCYarkEw1XIJhquQTDVcgmGq5BMNVyCYarkEw1XIJhquQTDVcgmGq5BMNVyCYarkEw1XIJhquQTDVcgmGq5BMPVyCYarkEw1XIJhquQSpF/IAf0noej0H+huI91wfcT5gnA/3c9D0eg/0NxHuuD7ifME4H+7noej0H+huI91wfcT5gnA33c9D0eg/0NxHuuD7icc4G+7noej0H+huI91we7SKxhbtE9jpijhzxz/p58PT81Trjhq3UMrDwIPQms5hUSIEfh6aQEpqD6+ioO1SEhIApuqD5f3P7Qmqu1DMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZizyrMWeVZiiVPiMgIpFVbAu3+h+I91we7/mP0OmuyohBPOKuj6rDDr7p2U4oC+mLqKjJF4mntsYd3GtaAmaiVCu8YfRYBxSwUsgzvv4Zjv4/CWjQ/rXDosrwIZTPaZwlA3tjqLggN9/BPaf8A+IYZCBcjiDXqRCn/AL57f/mFNQpCDQZhwmIqqLOgnt4UJYCUqrSUhL0bSH4xxf2ylXmGPO0w55zMOeczDnnMw55zMMedphzzmYc85mHPOZhzzmYc85mHPOZQeczDnnMw55zMOeczDnnMw55zMOeczDnnMw55zMOeczDnnMw55zMOecyg85mGPO0TQ6SnN3qUsdZg0WVukweuy5K+qTWfPYJja0tI+q6uYTRjMM+i4B5iK8Hh8NaatP1jmVLfSAGqxKnTESlcN9fiCS8y9o4Ww6X0IUZOWUA9ddkycUvSxGT6lFI0n9UTX1OLZE/TCtIpubXiSNX/ALQIUxWCvITrANBQzEaETQpqmJ/iRYilQF0aIuo1Yh0H6YyYPBUEoHILmwzTiMNizoqv8hV5QHF5YGvVMfU5QazTomFKmxOMyg85mHPOZhzzmYc85mHPOZhzzmYc85mHPOZhjztMMedphzzmYc85lB5zMMedphzzmYc85mHPOZhzzmYc85mHPOZhzzmYY87TDnnMw55zMMedphzzmYc85mHPOZQeczDnnMw55zMOeczDnnMw55zMOeczDnnMw55zMOeczDnnMw55zMOeczD7sYgVR8AP9D8R7rg93/MMutQI2YKQ23uPmtBkBDkiKWY/gBL6qv4i0+eX02jD2j6uHNOtqthBWxAwIhBXqp57yHrh84Cif/p0+Nc9oW4N7RqQp9uf9T4nqD59RmIDYXqgmt57ccUohIqs8sCIBml/DPbr1VEZonikA/iMLcam+5PyFhD4rE3AaY5CUX1o5n/U+MKkcU9qHFvZ9b+oT2s+Gu15WdVxS4Xk1a0un1xuly1a20pnP8zo/wAx55/4otPrrcSuicVM+NQn+RbOy2m9w4n17Y1DPmPPktOI/wCsuI91we7iC5xdxs/TL2JxPinTeaesGRcQkpUoGrinktXKWHEIKyn057fxFeE+TLWyekV6JiHoxVfktSe2LcU+gqh4ZadP8P1WuWl1qXIuZY1GIq8liT2xfiqk8KYgWvIZKv2Z/wBR4g4T5cqzp05ZT/qG+vC/LgJBOp3bzO33ItNYf7Qn/UWO0Qvdf8b7TKw9bjIrP+oL68NwTMlvMx8TPa9uD63zqkusxOMsGRusmJfDYxBkLq57StxtlXkDy8ocJcLAvF0XtZ/EXG05jTMXZhcVTi3AtrmPsxuITyF59XYMjPaVt9BAFaP9iXF+ttNk9o2YK9/Oa5i7MZi/mvLihwhJAhy1oVlxsCZnWf8AWXEe64PvcCOsfW+Z/wBd8R7rg/ZTxHuuD9lPEe64Pd+HiY7GM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8d94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z95awgyPdHQkZi35xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvGfeM+8Z94z7xn3jPvC28Z94z7xn3jPvC28cn1E7CPEdyM2PgJYfyEZ+aM/NGfmjPzRn5oz80Z+aM/NGfmjPzRn5oz80Z+aM/NGfmjPzRn5oz80Z+aM/NGfmjPzRn5oz80Z+aM/NGfmjPzRn5oz80Z+aM/NGfmjPzR3B/OfSQ+DfcbiPdcH3Y8V7RPiO48PE/dT7XYe5+B0j+y+Bh8jFdvuLxHuuD7scJnp3HAfupx9z8xv7N85/uLxHuuD3PUfdThM9O44D91OPufmN/ZvnP9xeI91wdyRmxl6xhqh6FB19B9z4AwgEk9ssUBTAChPiOnztnsJ8R0GeK2ZDoIz1Ae8eg/3DhM9O44D7tgAUy9cvcGko2XQeg9J9w+Z8j0H3yM44ZvXpAJJyhjhmz8f7hx9z8xvfP0UIE+MOQjduU9IwzhzBB6Dk0OZy7YwEYHp4BGbMj1jEq3r0cXR4ZQ5iOIez1j/Rz+j7/wA9/uLxHuuD3PUe6CciZTbtFIzU+MsbSCQoBjlkK6hC38zMntnw9ZcxZhn2Hwh1gDUpMsKoD25QnKcJnEY6g6/iYdRLfCfAAdHkQaRPAHUIcmsOWcufXl5iZxxyKkOQAjkozDx+BE7CXEdi/j+AlpWpTkAD4ywlCcmUxtKu2TN6CXNq+BJzBhzIyEvcuR5tUYEr8R/b+Ez07jgPuozZt8BK7BmcgSJ465a/WZZ55w9oJByjEaH7ADG06hmzfhLmDr6nMGdhKGWN+AB6LAmfm9cpcwcfEmeJXodgmeSqDHLVl8xn4iHJ7DkDL36zizna9ZKy5gc/KDlLNSHy+scrVWcsh8THYp9tSY2RIjsSVyJMJJ1mWMEUkAA+Jj66nOWRM1DU2ULHP1OczIDEx2yJ+ioMcvU3rG0krLX1L2nM+M+I/tfH3PzG9+xc28e2HynKeVB4QAEek4YASe0kzwIJ6BAB9GDMkwZHVkengErc5CIVVfAHo4+j0niQcouTfHMQ5qTn6xR2H3/nP9xeI91we56j3fAGOu8yI09HBPATtOoGeOXEYDqUZE5kjp4TPUxASHyiKvR6ZD8zMQ6FhmQpylrOM8iTPKp7YRpyznxeHI6s4czqBM+JnpPOD2dpEDaj6EmeDS0ofzieYDOWaG8RkZ5szmf7fwmenccB91h5467z5onpOMw5HrIPoMuWc1cxg+hpJnp0PoRvMRBnmPo9s9J8QRDkyMc58HGcGaBsmhJH6jAcvjnAwbYxy6AZgw5EtmJ2sxyAHR6GcRhyIhzIbNpxCEHsnidUIBQ5EQ+Q5tOEzhnoP7Xx9z8xve+JzMrEGSuIM1YZGdpI7fwE+KzNWWDIEHLpzzA7RAQueamA6AcyengE9OnihO09DBmQPCDt+IIgIUtmoMzAY5g+/wDOf7i8R7rg9z1HugnT4EGIdzBksGQEH0ssooKzwikfkYABB9JvE9HgYMhBkCcz0rmAc+gZiDNcssjF3MHmOZixewwZjoXPKDM+pi5rAT+ZgBWKd4MgP7fwmenccB91TmfxinmMXtQ5joGQi/S9QYARFJ/MwdhGU8B0LmIs8B0A6/UGDPTBmDFO8GQEUg/gYABB5fAxe31hzZehTkQTFzIJyMHafiYMwYDkYMhAf1QTzqMs54EZGeA/tfH3PzG7odAHuCCDuAOkDufnP9xeI91wfdjhM9O44D/YFAH904+5+Y39m+c/3F4j3XB92PQz07jgP3U4+58Q2e/9mHazswH3F4j3XB7nxE8y/dPxbx/Adz9n7qD6KeH4nufOJ9BvQyxdxLF3EtXeWrvLF3EsXcSxdxLV3lq7yxdxLF3EtXeWrvLF3EtXeWrvLV3li7iWLuJau8tXeWLuJau8tXeWrvLV3li7iWrvLV3li7iWLuJau8tXeWLuJYu8BFfxJ+M8B9xeI91we72N6zQ0RIiREiJESIkRIiRElabxEErTeIkRIiREiJK0ErTeIkRIiRE3iJvESIkRIiREiJK03iJESIkRIiREiJESIkRIiStN4iStN4iREiJESIkRJWm8rSIkRIibxE3iJESIkRIiRElabxEiJESIkRIiREiJESIkRJWm8RIiREiJCF9SJ2sfE90+URIiREiJK03iJESIkRIiREiJESIkRIiStN4iREiJESIkrTeIkRJWm8RN4iREiJvETeIkRIiREiJESVpvESIkRIiREiJESVpvESIkRJWm8RIiREiJESVpvESIkRIibxEiJESIm8RIiREiJESIkrTeIkRIiStN4iStN4iStN4iStN42kQd0oMqWVLKllSyldpUsqWVLKllSypZUsqWVLKllSypZUsqWVLKllSypZUsqWVLKllSypc5UsqWVLKl2lSytfuPxHuuD9lPEe64P2U8R7rg/ZTxHuuD3DkI4lglglglglgjiOJYJYJYI4lgjiWCWCWCWCOI4lgjiOJYJYJYI4lglglgjiWCWCOJYJYJYJYJYI4jiWCWCWCOJYI4lglglglgjiWCWCOI4lglglgjiWCWCWCOJYI4jiWCWCWCWCWCWCOJYJYJYI4lgjiWCWCWCWCOI4lgjiOI4jiWCOJYJYJYI4lglgjiWCWCOJYJYJYI4lgjiWCOJYJYJYJYJYI4jiOJYI4jiOI4lgjiWCWCWCWCWCWCWCWCWCOJYI4lgjiOI4lglglgjiWCOJYI4jiOJYI4jiWCOJYI4lglgjiWCWCWCWCWCWCWCWCOJYI4jiOJYI4lgjiWCOI4lgjiOJYJYI4lgjiOI4lglglgjiWCWCWCWCWCWCWCOJYI4lgjiWCOI4jiWCOJYJYI4jiWCOJYJYI4lgjiWCWCWCWCWCWCWCWCWCWCWCOJYI4lgjiOI4jiOJYI4lglgjiOJYI4lglglglgjiWCWCWCWCWCWCWCWCWCWCWCOJYJYJYI4jiWCOI4lglglglgjiOJYI4lgjj3eI91we6BAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAIBAPd4j3XB+yniPdcH7KfHUe64P2U8R7rg/ZTxHuuD3frMNYUsgtN1XmySXFHPGNPSSESNnW3fEnEVIGforsd8QcgV6XCVr4sxyEYMrDMEeBH3CBFaeYzruSYlXIGbd1TYhofQdfTataAjNmOQhzGWcrdBS+ltf8ArfiPdcHu/wCY0EpQPXQSjzz9T0fbfrr/ANM8oY20xtFVS6mM9gB8J9jXYA7yo1WVHTZUZ7OGKNJystdgEns8YTF6c0HmDiYc4rHX+SiexBVhnORtQhtEOqoAET2AP4Tz5G0ays+hT1Zdhwz2EGwv2XtsALyg0W1PosqPR/i//Z0YZLP4i3LU0w734rEHTRSv2p7DCYVnAL1PrKTDo+GspJttLeSYSuvCCgaLQ/3DqrOdCeKgxRVbS6i1eMGfU6Fbmn/T4OF/GwayIDWwJV6z5lM9ljEihtFl1jhF1TAnC4uoa+JWEwf8XjeHwVJ7JXDi45JajhhKg1d9mmxp7KGGwTIdDM4Nkw9dRpxBrGj7U9lLfjBaRp+wqDinswYV7vqrQc0MwyHBHQXu+IaYKuisVIKSrhtUorpKXlAqT2cMWaTlbazAJMAMJinGdXC8qD1X6tc9gAYL/wCoNcOaWrqX/WXEe64Pd/zHmMShw30y6B57b10fKqQLFC11qFUegEOQE9l24wfVVeiCeyLcGqEVWEnUHnle9IBoCKFA4QOyU1rc9NhYoACSBPMbTqlVZtTyOQNS/lEoa4YcAC7gmF9n6H/XH0WonmHEJg6cfhaR508+mApS6NWwldGOwdZJTjlJqvpOl06P8X/7Oj58vFOLwpzraeyq2o1AG+mHUr4ZCOaf4yfcP40Cee6wIgn2balIPEizC+zfAccroT+KOoCox6b6L7Nb0WTAfwuMQc0+v/iJUlgHhrAMA0nF9H+Y885xk+tGJTRPWqfGoT/Itn2r3Lz68Y1NE458lv8AWfEe64Pd0AYrEF0mkUX+TpCnEWV6VJOkCZdYBnYRPOfLNIt0aSVgw2IrQZVXtLzbfiDqthquwt51Gl/smXqHqGVNFflEsFWNqGll41howdJ89qeaYopdWBpslGF8unryZijrcPqsHE0qw16r5L3MuFmLxTarOivDGq+sAdY8owHNNGWHsBumKK30D6o+R4mHwuGLDrShzZp2J1OgRKThKqtNbr9wtGtuKVYEJWsxgxF9f1VQ8iT6PA3CYmGu4b2mLN19xJfhENWLw1tmsC1u1ZYn8Sa9Fddfggl9fW2fX0v4PDVhsLS/1VR80CdXhrg9nQEHX4o2rpjobBiXSymydXVRQc0oSPpLRKNFaAVWJAmdt5cZHUNM6q7DXvqNFkatRh/qcPXNPVYZmNk8z1kCaetXzEf6y4j3XB97resN1xs/13xHuuD9lPEe64P2U8R7rg/ZTxHuuD9lPEe64P2U8R7rg/ZTxHuuD3XdsKcS9ZPAYcwZYzKlmSgy1a14mMvV/wAuhiGFLEGPm5w5Jae1MQhJIyBmLOJwtzac26bGH8LSbbdM8GQZxgqjxJmMrdwp7AZZrcuZcifmZfmj3ZMUMsVS5yXP4mWg2j7IlyoD6mWq4/CMEX1Jloc+ghAA8SZiEL+mY/YVxHuuD3fB8S0+tq+qbiWfMgD00v1dVUUVGtxrRfAgz7Sgz5LT/FML+fglLiip9buw6DkqqST+AjoDinYZHgj/AFFh0fpjkUhOssAiJTei51lZ9g2vtEFz2udIbwAma02XfV/ANDkTiJhxrRCTae1jMRWbXc6KnPlExCFLn0W1KZmcH9sTEV021MCDRA3VdcOu08McV3cQIDgxi5VQNR8T+wniPdcHu/5Rn/dUfSWDzvPoVW2GypzHFz2uNbJ2hQJ9lQJ8lp8hpWhOtoAB+HR9biXCARCz6QIMqLfoWCdtJTq7DLVuusGlETtJJnFcscUvU5yLz6VNdwHWDylp/kLPtAjeV1LYjnS7r5hMBVe3i9iKMklH8mwfWsM1hBxhtGgUTDa0c5WHxCR6v4n7Bqn1orGr9hPEe64PdpRC5zYqACx6KK62c5uVUAmUo/4MAZRXXnwqAT0IrKwyKsMwRK0CZZaAAFAlSVpwooUbDppR3Q5o7AEj8uimuwDjUMBvEBQjIqRmJhaUJ+KoBKkrXPPSgCjYSiu1h6gGUV5VnNfojJT+EoRyhzUsoOR9Rn0YZX9NYEoSsf8AkUCVK6cLAETDVI3EqgRQynxBGYMwNKNxaR+wriPdcH7KeI91wfsp4j3XB+yniPdcH7KeI91wfsp4j3XxT9lPEe68V/ZR4nvMwfwjGNHMcxo0aNGjRzHMaNGjmNGjRzGjRo0aNHMcxzGjmNGjRo5jmNGjRo0aOY5jRo0cxzHMaOY0aNGjmOY5jmOY5jmNGjRo5jmNGjRzHMaOY5jmOY0cxzHMaOY5jmOY0cxzHMcxzHMcxo0aNHMcxo0aOY5jRzHMcxzGjmOY5jRzHMcxzGjmOY0cxzGjRo0aNHMcxzGjRo0aOY5jmOY0cxzHMaOY0aOY0cxzHMcxzGjRo0aNHMcxzGjRo0aOY5jmOY0cxzHMaOY0aOY0cxzHMcxzGjRzGjmOY5jmOY0cxzGjRo5jmOY0cxzHMcxzHMcxzGjmOY5jmOY0cxzGjmNHMcxzHMcxzGjRo5jmOY5jmOY5jmOY5jmOY0cxo5jmOY0cxzGjmOY5jmOY5jmOY0cxzHMcxo5jmOY5jmOY5jmOY0cxo5jmNGPv/wD/xAAyEQACAQMDAwMDBAEEAwEAAAAAAQIQERIDE0EgITEyM3AiMEAEUGCAI1FhsMBCUpCg/9oACAECAQEIAf8A9A9vy7fYt/RO3Ratuixbqt0qt62+1Yt0WLVt/QC34L/EX9Ebl/uvoX27l+hD6H0P+l7/APjXf/oTKZct0WpatqWralqNdVuq1bUtVVapatq2FW3YXktW1bUtS1Ldq2ralqW6rUtRVtSxalh1sIVLdVqWOKcCLjralh0VLUtS1LUtW1EOlqWo18PKjqqs4OBHIx+Kqrqjk5GcHAh0ZwcU5o6KrOKPqVeBUfQvIzgVWcUQziiF18HAheaMVe4zgQqOqEIVGOnBxRUdOBFh070VGMQvI6ro5pwcHeioxiq/h5U8niiLU8ng4Ec04PJYRb7Hk8Co6M4palqeRdNq2GeaIdOBeacHmqp5PFLDOKItTgVeSw6IZ5PAqeRUVGcCEWHTgQhCLDELyOlhFh04F0cURYZ5ELzTz02OepUY+h/Mq/5O6xb+mXFOKWraiGq2LUsWpYtS3XatqWOK2pYt0Wrali1LFqWLUt0NC8li1LFui1OOm1LdqWLUt8ucFhnFOBU8oQ6IYhncRyOiLOnAhnBxTgVOKPxXgQxdFmM4q6sVxjohroYhiOR3FXivBxR+BHenFGI5H8ucCGcU4FTgXkdWIfkYjk8HmlxiH4EM4pwKnFOK8CGIdODuM4q6s708C71firEPyIXkZ3rxXgXijpcZxRnej+XL0vS9bl6XohsvS5ely9L1uXrfpuXrely9bly9L0uXpcvS5ely9b9Ny/RfouXLl6Xpcv8A9WejpykbZto20baNtG2jbRto20baNtG2jbRto20baNtG2jbRto20baNtG2jbRto20baNtG2jbRto20bZKEo+f6HacbvvObl+xwnbs9SOL/odHtpS/ZZ+1D+hy9p/ssvah8b2+1b+Gr2n9pJydlsTGnHs/t2b+7L2ofGyoyxalqOipxS1O1O1WqWLVsL98XtP7S/x6V1k73J6mcVeGnl3e1CXo04ZyxNuC7S1IYMjpq2Unpq14aenuZE4wS7LTilectNKOUdFQwkSUb/TtQj656ePdfZl7UPjZVYqsdEdi9FXwduhjEKjF++L2n9qffRgyDS8ywelkfTsxvF6UXc02nrXUvUzV9GmauFo3hLShc0fTqUenGK+v6diWOj6dQ0fcRqbebvKUNtxX2Ze1D487dC6O1O1O1L9hdCrf98XtP7WnqY9n/g8mpqZdlCaxxlfRj3WnJRndvu2TmpRghThKKjNvTinjpzUYzVHLT1O8nqQ23FaU1G924qV4OWlPu5yha0fsy9qH9Dl7T/ZZe1D+hy9p/ssvah/Q7T7qUB9v2NK7NXtaP8AQ7wZQn6tuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwNuBtwMow9P/RrrP8AoLYVLCHW1bUQ1S1bVtVDVLddi3TavAi4n0eB0sWpatq2raq6bUt2+Y0cjEXELyMQ6I7nAqcCGLxTgR3OBHeqGKnlVYjkYjkYjkdeBFjxVDGIbLipcR3OBHenB3qq8CO9F4+Y/CLnlCoheRiHRFzuKnAhnFOBF2d6XHRDF4pxVHhCGI5GI5HXgXSi55F5HRUsIuzvS46Jjoq8UuMXj5jbpely9Ll+i5et6Xrely9L9F6XL1Q6XL0uX6b9V+i5ely9Ll6XpcvVOxcuXpeif9B7/wDKHduhdLp2+zx+TxRfgI7Dq195dVutj+0qv8i34j6bfCjpwcHkQqOjOC3QzwhHJc4ODzRnFEXoqOnk4F0M8ITOS5xRU8HmipyXpwIXkvReBFh0VfBelxHcdeKqiOTvR0QhjGPyOnFEdx9CLjGeDzRDPB5oi9PB5EXORnFeS9L9qIZ4PNEMZ4PNeR0Rc5ORnBxRHmvHwh5LDH4EKn+9WcdL8HAjkscHAjvXjp/3ozg4FXuPwcCOacUVH0f70ZwcCF5qvAujwi4xljgsI7jO9eKKvJ3H0qjH5HTgQjuOnFEWGOneiGMVEWoxCpyM4ODuLyWpxV0XS6IscjoixycjOKqjvTj4Rv1P7F63pfov+Bet6X+3et/v3pel+q9b/fv0Xpf7l/tXpf7Xjpv03rfqv8p3RdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdF0XRdfKzdkNtlmYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWYsxZizFmLMWWZCXHyrqekh6v47z8q6npNP1fx3n4XxkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSNSLUTT9XVZswkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRhIwkYSMJGEjCRjL8Xn4W7aSHqTZlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRnI1JScbGn6umEb929V+I5SMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIWpNFlqK6/D5+FdJXmiTu2/wB/1PSfpvcQ+zfRLtpR/gmm7TRNWk/w+fhXR9yP8A1PSfp/dRP1Pon6NP8AgkfUjU9b/D5+FdH3I/YcWvNEm/FGmqYtq9Er+GmvPQtObGmvP3cXa4oyl4259Di4+aKLdWmvP3NT0n6f3UT9T6J+jT61Fy8CTYk3f7OLtf770pJXf2Um/DVvspN+Pux9SNT1v8Pn4V0fcj1wjlJIm1qRkkR0m1d6UHHIUXJ2WyaytGFNC227yWMmj9Oldyesr6ticXB2MPoypopd5uWrNs3FODU1C8XISu7GH14mzbzHSc1dShjY2f8AWcHDytJuKkPRdrrRinI1oJSbovYZpdtOYtWaZrpfTKmlHKaNR7kJUWk7XcIOMJkYubstn/TX8o2X2MLyxWyNNOxs/wDtODgR0m1dz03Hv06npP0/uon6n0T9Gn16X+ODkay+q5peJml6dQjFydlsig8sW42diem4WMPoyou7O3tD7MjpuXcek0rqMXJ2Wz/pZ3sbP+soOHmOm2rktPFXIQch6Xa6NGKcjWXdi0XZMw+rFbJKLi7PZfZj0ZcbPbtTS+iLmay7qRCGbsLRMHli5RxdjD6MiUMUmR0nKLkQjm7GivqkjZJRcXZx0m1dy03FXI6bkm1KGItF2u56bh1R9SNT1v8AD5+FdH3I9ehHtKRp6cozu5Rx1LGu/qSNBu0kaPZTZfua7vGFF7LNX6lGZL6FCBr+4an1xhI1fpjGFIezOlmyHtTNP1ofvms/8jIdtGZoq+oicIuTvPHbsN/4Imh6iHumr65UXsM0bbc720Uak83TRj9EmaWm4t3xtqWNd/XY0m9qaNPtpzYm0z9R6kaz+mBoJYzZhC5rSWaalhq9ycZRte8NSMU5wlGPTqek/T+6ifqfRP0afUldmpptqKU4PaNL0zNL06hp9tObLu5q+YSHHLUiyT3ISNT6YRjTQjeVzbluZGvG0zV7QghScfGn205su07ml9eqm5Qi5M1HHbSE4TgouWnKMe2nKOLhJ6bSvE0/WjV9yRqP/FA0P/Jl+5q94QZrP6YEH/hkaHrJeXSem8IxUoPas9D1Mu7mv6kzU+uMZGr9KjAX1aInhhAgsNxmh6mXdzW9UTVjF2usIxkjTdtKZp95o1Ixcu7xWk49UfUjU9b/AA+fhXR9yPXOSWnGKuzVkpYyHhqpXg9OCaNKeL74ad7mtOMlGxktlo0ZxSalKWWpc1mpT7aDTTi5yyk3TTngzDSfdSlCMcYaUoq8ZRWlCVxyW9c1HebaUltSRGWMkyUdOf1E8EkouS2Yo0Woy7qVp3NXF/UhSWy0QklpzXRqSWMYxTaZqyTakpYavcUtOMJRWlNK6eGnF3NaSlJW1ZJqNtKajdPb0/JJxy7OGnLutSUbRisdOSVpSjHTwXRqek/T+6ifqfRP0afVpWUruUnKTZpTs2nBqOZpySU76c0rp4aadzUnnIjqR2zRkoy76sspuiko6TtdjkpaaE4zgoytpwTNKaV08NNO5mlPJOOnPup4KyilpyihuMIOKgtOSs446V6Q7SRqO82yck4QRpTwZt6d7mrNSaS1ZJqNoyW1JGk0p95eWadslec8pNmlO0u+m4xnKmtJSatoNNNOcspNmjJRfec76lzW1IuKS0Wk3c1ZJuNrw1UrtacYkZLbmhOzuPDU7k8FGy6Y+pGp63+Hz8K6PuR/EzjGLUP2vU9J+n91E/U+ifo0/wBxc4xjaP4UfUjU9b/D5+FdH3I/wDU9J+n91E/U+ifo0/4JH1I1PW/w+fhXSdpomsZNfv8Aqek/Te4h92+h/VpR/gmmryRN3k/w+fha8dRWb0pm1M25m3M2pm1M25m3M25m1M25m3M25m1M25m1M25m1M25m1M2pm1M25m1M2pm1M25m1M25m1M25m3M2pm1M2pm1M2pm1M2pm1M2pm1M25m3M25m1M25m1M2pm1M2pm1M2pm1M2pm1M2pm1M25m1M25m3M1YSUO+n6umE8R6d+8NqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqZtTNqYtKY3GCxj+Hz8L5MykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykajeJp+rrykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRlIykZSMpGUjKRd/i8/Kup6TT9X8d5+VdT0mn6v47z8q6npNP1fx3n5V1PSafq/jvPyq1dHdMzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyM5GcjORnIzkZyIRu/lZpMwiYRMImETCJhEwiYRMImETCJhEwiYRMImETCJhEwiYRMImETCJhEwiYRMImETCJhEwiYRMImETCJhEwiYRMImETbiYRMImETCJhE24m3EwiYRMImETCJhEwiYRMImETCJhEwiYRMImETCJhEwiYRMImETCJhEwiYRMImETCJhEwiYRMImETCJhEwiYRMImETCJhEwiYR/42ZHb7bO3Quh0X8H7ffXyMi1HXzRnFWMVeBiLjPAheaeDyIuL+AWEXoxU56/FEXHXxRUYzyKjPB5OC/xmqs4EI704qx9FzyKjGIVe/S/310VGMXg7nI6I7joxCLuncY6cUYxCo/I6cCH8Z36r/dvW/Rel6X/frl+i9L9N63L0vS9b9D6b/wDderUfTzTzVdDov6RMZ4FRHI0eKsdLj8jouhHcR3/oyxDPIqI5HVFzzSw/I6Lr7/8AHtf/xAAuEQEBAAAEBQMDAwQDAAAAAAABAAIQETEDICFBUTBwgEBQYUKQkWCBoMBx0OD/2gAIAQIBCT8B/wDEx+IsZYyxljLGWMsZYyxljLGWMsZYyxljLGWMsZYyxljLGWMsZYyxljLGWMsZYyxHwQ2Lb7Htbfsg+fsv5+B3n7L+fgd59Ldm3nQsWrlj65OhYtbtYtWxaTqT/wAzrYus6npfn4HefTNbDpkuXnJlvGWLrl4yX0/z8DvPpdTLa2uufa/m6t3y6OWzl0Y9L8/A7z9l/PwO8/Zfz8Du/wBl7fA/o+biXEuJcS4lxLiXEuJcS4lxLiXEuJcS4lxLiXEuJcS4lxLiXEuJcS4lxLiXEuJcS38/60mf6ZOzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMz/gYkRERERERERERERERERERERERERERERERERERERERER6BEREREREREREREREREREREREREREREREREREREREREREe1fXFMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzzbF0JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmbf3v7/sUHoHoH0BHIfbDp7xfpydC2yxGvjl7cvabftds3TJ1nTN1meRu+f6cnTPEL4vE9LrYjXxk6ZOhdT6PveM8Rr45/GXQnUyxGuWLTLoTqZOuTYv7TdbEa+MnpdSdeTvli6+OTvyYuvjJ0LqZM6e3CXnPfk85f39biWLXTLxeeTadbbPZvPL4ydLi5YtG2sWiTqfRdrcvHL3u2faS783EsWqTppOpdLFr625eOXtn4zx6WPXXPHYtfbZy37zozbNj6Z75eeXax6X82zYtbbLfLFpdfzb5ebFlvyueLRm2bHqZ7Nj6R0sWl10nRnX6PZnLZsfTLcts93J6k6JOrbNj6ZYtMnRnXW6Ni15drH0tjn2npy7PM6M6ueLRur7h9/6j7/sa9ve7oxERERERERERERERERERERERERERERERERERERERERHPtMRERERERERERERERERERERERERERERERERERERERERERl/PtWzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMz6LMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM/43DMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzkzMzMzMzMzMzMzMzMzMzMzMzMzMz7skRERERERERERERERERERERERERGREREREREREREREREREREREREREREREREREf92of//EADMRAAIDAAEDAQUIAgICAwAAAAACAQMTFAQREjEgITIzcBAiMDRAQlBgBYAjUUHQJLDg/9oACAEDAQEIAf8A8THPciG8m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m0m8iXI/p/odc8rHaK6oX3z/BW1Q3vil/Nff8A6Gt770/ha/dfbH+h0/mF/hU/MW/UHv8A02fzC/hM6pHeeUncVoeO8fhy0R6/iJ+Yt+oE/wBOn8wv4Uxtf4z4J27FdU1vPa27wnxjexO2ltngnlG1re9arYsge+fLwRbmhoWy+7LxK3tae8zc7NMV13TLeD9TNmiiM3j3fex/l1XefeJ/BT8xb9Np+2Cftj7Z/Gn7YJ/nJ/ML+FXPj1DxNis0fdSbIu8Z+/yG8XW917TbEx0/aa47VwdP8dpTp5P4ulz9u/U/FUekEWu8znHnyV8up+Oo6jvjJVt4R4olm0O34KfmLf8AQ6fzC/hXU+f3l/8AlehTT4e9raW8vNO3UP7ptrlq/GFjssQU1MjvMtVYjy1cLc7RLXVM7J27d47EJdVMwiVW6w7X1TZESqw7JMWQl9XuWpLPLyf8FPzFv+h0/mF/hU/MW/6HT+YX+FT59v8Aodf3VkcWYaO8fwTTCx3miO8s/wDodMQ0dpzsq+Da02sNrDaw2sNrDaw2sNrDaw2sNbDaw1sNrDaw2sNrDaw2sNrDaw1sNrDaw2sNbDaw2sNrDWw2sNrDa3/xnZbP/JEdv/U3ERM+mVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhlYZWGVhm/1WrSXaFiupKlNajao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqjao2qNqiLK290dV06ssvH1U6P50HWT2pn+uLMxMH7CfWfqp0fzoOt+TP9cj1P2E+s/RbRINUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUNUOhdWujt1vyZ9qWiPXVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVDVCLEn9JHqfsJ9Z+iv372ntFNamaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaGaHQ1rF8THXT2pn2bbPH3KtHf3vmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmhmg1KSQzUtCt+ij1P2E+s/RS+e1cla+KRH8/0fzoP8p+VYSe6xPsJ9695/ol6965Kp7pH6KPU/YT6z9FOp+VJHp/P9H86D/J/lXKvgX2Kvm2/wBEt+CSn5a/oo9T9hPrP0U6n5Ukentw6tPaPsZ1X1+yHVu/YmxYnx+yZhY7yrQ0d49ib64FZW9PxfNfLxGsVPXas9ftV1f0+yXVZiJ+xWhvT8To/nQf5P8AKuVfAvsVfNt9tnVfWJ7ktC+suq9u/wCBovl4/jehF6M3jH4LNCx74mJjvH4DNC+v4tvwSU/LX9FHqfsJ9Z+inU/Kkj09q5/BJkrianRpH6iIbxi62H8YGeEXvPJOlnu9n2dT3291beaRJ1bz2hI6dvGjuV2RYvcm3/khPs6hp7rWq9PXEGTV2RKNb4uqjt4LLEWxNfmcrv8AC98I3aa7fPv3nqff92u1bPRuoVWlSOpjv2bqXlU93TWSyxH2T+agvjvdXEzRXMHSzP3l+y9/GuSmJpsXuN1ERPZbbIsesexa17zyf++ln3McqPfBrEJ5NyhWhl8onqY7/drtiwfqIie0V3Q89vZ6P50H+T/KuVfAvsVfNt9u/vbZ4R0zd08Z6j4qzqfirHda47zySbY8POFeGXyKrosmYJtjSE+xp7RMn3vmiz3iJLLoSewvURM9pexUjvPJ/wC4aJXyOT7/ALtdq2R7nvhZ8YS+HntNlsV+4XqO89pOoeVXsdM3uiCepiJmCLfueTckR4eO8cmPfBHUr7+/J9/v+zqP+R4SOlb3Sk2WRXBPU/8AWsSnnFbxYveJt/5PASzzZoGvhX8SyyEXudQ3dFk5H/SPDx3h74We0JfDz4ll0VtETXb59yeojv2Wu5bPat+CSn5cfoo9T9hPrP0U6n5Ukeke11b+9VLbVdIiEfyp8jpVjtMnVLHkknUe9qlPGPHsdJHZ7PsaO/U9iic2dJX/AJGssOm99RTObuk0R5u9n2W+7qEmSWiPW359Zf8AKYX8qdNERVBZHfqUOpnxqntVY6pHaqH28iI79Ux1cR4RJb8go+Uv2T+ag6nvFiePfqW9xTVmv2dS/exVLroeI7eflT5R0sRn3OoiItSS771tazKxK9jpPhY6aI8nk6uZ8kg0smOx06t4NErNlHeJqdH79u1lLtMV2o7ez0fzoP8AJ/lXKvgX2Kvm2+009omSq2IZmmqz/nOo+Ks6n4qi771tazKx49iiO62KK/jU6iRi6lP37Xf7OqfxTsarl4HTP3r7FEeVlky1at273feuRZlVmOxdEV09lrseFiIph9ZkZbK7JeEtR299yP5w6xcrTEOXfLko+VBTETdYdV6pBCx49ij3PZB08ffceI5KnUxGYnwwTPaBLYixnlbY37nVfCp4x49jpferwUzmzpNEeTO8t9y/uMvn52Dtrkp1UfdU8Y8ex0/uRyl2Xv2nR3WS2O91cTd92pu1Lsq+5PObob2rfgkp+Wv6KPU/YT6z9FOp+VJHp7VdctaztKqUoyS6zGnTtPZ4utaGL65dY7a3THidNWyS/keDciGOpraZhkSvxq8Tp1la+09WvZlaKk8EiPsvq0j3RbenulEssfzs6itp8WR2vtXxFRuP4lMStcRLI09QjFieaSor20/dmrVpmWVG5DMdSssnuZJarxKJePuMSjciGLUaba5+2SmufNndkWYmChGhWRl1omYGW2x1ab62bsy63PHjHTIyK3fp0ZZfvfVLxErtb28RIsz+9Flye5qK28mefO6tp7ojvbpPsdH86D/J/lXKvgX2Kvm2+1f5SnZa64VIgvr7xErarNnJejM1fa+uW7Ms22zHiU1+C+96Wm73dRXLJHahPCuO4yM98d/GBUZLp7Mr1PLL5W2tBfWzdmXW5o8YzlqvFle2uPGatZmZZmureSFe2yGm2bUbvDed8qR7oLY7pMFMTFcRNKMtlkzfXovu2tiPEorlImWoWVl+7o26sXrLJ2hPcsF/l4TC1VwqRE31+S91tV3RD/wdMjL5d+qXsyzFS+CRB1NcusTFVfarxnp6WV5luoWWhe3/AIKEmIbvEWUNPZWtd4kdZm5JGXyWYFm2j7pXNrP3n2bfgkp+Wv6KPU/YT6z9FOp+VJHp+ikiqx3ibP4vo/nQf5P8q5V8C+xV823+Qkip3fys/RW/BJT8tf0Uep+wn1n6KdT8qSPT+f6P50H+T/KuVfAvsVfNt/olvwSU/LX9FHqfsJ9Z+il8d65Km8kif5/o/nQf5TvxW7JHZYj2F+5e0f0S9u1clUdkiP0Uep+wn1n6KzD0z3WOork3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rN6zes3rOhtRr4iOt+TPs21+fviL/H3Wb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZvWb1m9ZPUJHoqva3k/6KPU/YT6z9FvBZM0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0M0OiWIujt1vyZ9qYiTNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNDNCEWP0kep+wn1n6qdH86Drfkz/XI9T9hPrP1U6P50HW/Jn+uR6n7CfWfqp0fzoOt+TP9cj1P2E+s/VTo/nQdZ8mf65HrB+wn1n6qVPm8MRKXIcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSg4lBxKDiUHEoOJQcSgXpqVnvHU3RWnb6rJY6fDy7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecy85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zl3nLvOXecu85d5y7zlXyTMtPef8A7Jef0sf0qf1c/TyP0UfZP9Tj8aPsn6ddv/WKH//EAC8RAQEBAAAFAwMCBQQDAAAAAAABAgMQESExIEFRYXCAMEAiQlBggRIykdBxkOD/2gAIAQMBCT8B/wDiY/FYrFYrFYrFYrFYrFYrFYrFYrFYrFYrFYrFYrFYrFYrFYrFYrFYq/gf5rvf6H208z/0zfT8GLyv7n6fgx/tyi/wp1rHbljtyz1rPRPLPTLHVOlT/wAJ0Y7J0v6X0/Bj3a6N9XlOXx6s/wCfRO36f0/Bjtrl5eXac/d7u0e3Lxy8zl3i/pfT8M/p+Gf0/A72/ovv+B/efDhOE4ThOE4ThOE4ThOE4ThOE4ThOE4ThOE4ThOE4ThOE4ThOE4ThOE4ThPHx/1N1msVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVisVis37rxqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqNRqLHn8M/j8M/j7L1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV9dVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/a/H2W7YRERERERERERERERERERERERERERERERERERERERERE9Xmu9REREREREREREREREREREREREREREREREREREREREREREXrm/tPj7t+39sfH3b+n9sfH2dv6F7/AKFX9a91X0Xnf2309d53z+je/wDYvx9nf5uU612vLPb5/S90f7fd7889eWejPXlPCdET0R7c/wCblOvPPSfPKd3Zjt8vDPXlOtTpf0/p6/Z7ejHZ39Xzy71OnLPZ4Z68p1qdK8s9OUZ/yndOjHblO7tWeno9uWe3zzj29Ge3zynWu15Z6J1ef3Xx9nc+Hw88vCen/Hrvr4THSXn8ejynR55eyeHw9+Xj0T/Dg8sdYnSs9ZU6a/T+nrnl4vPxy8PMe/P3R7co8Jy4TPSVOsZ6aTqx0/VjtLynt6Pf0T25Y6sdOnPh9WOn7r4+zkR4Z65Z7PMY78529fmMdX/DzGOid+U7csdXafCduefHKdk9ER4Z6xl5jHflHmOH3Xux1Tp1Z/1Rnp+n9PVPKJ3iI8xju8vFeY88p/DET+Gp1lTpHmMd17sdX/Cdcs9JHeM9J6I8xju83lO3oid4nflHvy8z0xOuU6Z556x2nx+6+PtT4n9S+n9R9v6P8fdv6f2x8fdv+b+2Pj7LTrnlVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV9XmJ0VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVXd4+P2nx9l4iIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIieuIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiftfj8M/j8M/j8M/j8M/j7rd5WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWXn7rVtttttttttttttttttttttttttttptptptppppppppppppppppttttttttttttttttttttttttttttttttttttttttttv/u1D/8QALBEBAAICAgEDAgYDAQEBAAAAAQARMWEQIUEgUZFwoTBAUGCAwXGwsaCBwP/aAAgBAgEBPxD/ANAoSiP5AR4T8FRE/ABcSoRqv4EGY8DGBfBOAvgnoByIruJUDhOQRK5PoBwSoF8JUCJcruJAuPAlcB6AOCV9fRnXodwjni4M88mI54WGZhCPBGYORljKIkIzv0XwYZjiEWXwZi1LePPHcX6+BcTgjn0F280+gxHPJmYQzHHKu48hcSoPBmP4AEcQ9Az6DLlxPr7mUcMGUTojBlExwM6hHPJmOIcJDh5Gp0yiLxYyiKcDEgzM8xh6Vzo48zplEX+CgTH5ipT/AAZuLf5i+C/72WmV9DqfpGKREYpgmInNuBMRJSy3FoiQFiji0Dv0ALwiQTESV1cC+bREgLLcWiVAWBHgL4AuW5tESAsUQXEr0BZiJAWKOAuWgLFHFvQZloiQFiji3FpTdcIkqAsUQLloCwIx4tESWiji0wOAWKJnMni3CVKQuVAWKOLRw4Mp5y8GHgFiji0wICwIyrZaAsUcW4twJiJAWKOBbLRvq5aU3UtAtlH0ewYZi8Q7E48mWzLF3DtQyibhKbmCV0VASZMtl2zw5VMSHuF3DtTzmRMmEfcyoEAjmBCK5mBHoCPuOeeIFGZcPeGN+YnQED3mUc88ovc7BKaKgJzLuPQi7gdsVudgeWUXczBdQVFTKcjB77iK2M7vuJaR7GPQj0Iu50hbZ0AjjCUpgRWk8p5MtuYnBddQTJmTMlls8GC6j2JMRUAmWZMVuYHBlPOU94r4LEDM8mWzxnQCPgVsroqBWZ0ZTB77iN2Mz2iMfdDukoHGcc8FiLuU5OM/o9g8JkTDhVw6HqJkTCGUy4uY4TCdMsVPBq+o9kC+TFy+7iZEwqURIFMyYTOGUGypswaY2wxAmEFR4Sy4dlQpmLbxozAJhKrLFTBXB3SdIcxwmEqsvGUczCBkwe+C8Q7pxlGzHoqHcCoUsBHMaYt7xWweou4u5hAyYu4zCCozoxFczAjhwXfDx4O6QVMmZMVPBeIiouE7KmzFTwwODKYPoyKgVcXDEmEFcDmYSqywBgI5nSwQ5i5RL7GJRAsmDvjKOZ4TwgDARzF7gvuV1f0yGh4IpVH6f0Y5/wBnaJl4lfoNNX+dC4lfhV1f1ocZbHu0BZeIkzxRIfMyPB3LxfAmL4FFHF4iRKIFsSoFlwF4okC+ILLx4hbHocCijkFl4lQFl5iCZfgTL8CYo4vETgLlnApWKOBMUcgsvEqOErq+EogLxrup4IlQTFHF4lfVsLE/ziKoh0njM9BZbCGcyeMiOLuCmdoEmIC4lp1M42RVh3HW2dwY9QdW4e4xYqw6i2K+bhBbF3H3USngXBCHjMRbFudACCkywOCAM6MChU7JlEuK8+Myj7jyQLgJgTMel4xFxbjhDPjBMElCLfE7SBgRiVD+rjjFTD5h3TjE9hODpcWTwZhgtnaEmJdYx3A0bgLiWJgMfc6R2KmcO6cYjF4zxSubjMoKYe7nZ4OrEtTxj2OEqdgYFzDE9JBUbudwqNwLepaTtbnxmUyTOKoSMFc54JUcJ2RKZiQHMFcz4nSRIwVPMz+rmFcYVBriqwa4qeGxxkSh5k3l93xbQa4qsOo2hSLcGuKrBrgU4qSuREW2EvIRxbQU4rcEcxE1OCWBrip5JGm4tsGuCXmKcVuL0EFOLYgjitwRFuHKt/Wg6Ytv+0kWDfFc1AicYJcZXFSuK9NPFcUzx/BdmIcMYQzHjxKOO4zxO4TuEOp5mWXPE8Q4cfwXYQ6ZUZmYhmJx44JUcSp45IzxCVGeISo4/wDIb2Ide8qZ/QcRERERERERERERERERERERq4lhXXv/AAPFn3DFKOhg/Q1eR5IPT2uz+By6PlP0Xsv+f4HfZv0X7/6bAWW/BC5eIn5AL/XPs34RgLZ1+PmOUU/iFijH4v3/ANNmDLeGzHpZAuU95VPcpRxnFj3AeWU8MC5Xuld1K90ruolNSiV1cPJlPDAtiB5h0tYP1z7N+Eqv83PKLgCHXzKzWfMoPSeElCmpb6HsqLXdjhmVdiPdQZJW7UxRoI3ssBxCvde6eGJSjBlA6V4CVSw4fwvv/psxYIuI5mc84YePCYHGcv2RtA6tZTuoNMocMynaJTKtGZGdAOGcyZ54ef659m/CFR4zF1mCgWvUTu0NQmbCMMDca/507K+0FVjrqoghLKnhxlgazQjimrMz7NAN8D7RLYq31f4X3/02DQ8KMscywKINcFtlid8KnkRKYIgk6S+74LcMEVMW2CJTLDEVPFidwB+ufZvwuyduSV5n/E6YUcEvjuCzY/BPGLuMV7sKmR3DQxiB4WeWJDI6g03AhECVfrv5iIPRTCSSht8kqesefwvv/wCBz7N+i/f/AMDn2b9F+/8A4HGOZwgUj+hoQHbEA3H+BwqElUroT2R8TQ+JofE0PiaHxND4mh8TQ+JofE0PiaHxND4mh8TQ+JofE0PiaHxND4mh8TQ+JofE0PiaHxND4mh8TQ+JofE0PiaHxND4mh8TQ+JofE90fETIb9yKrb/+F9pq/QC8hf4tNX+bpq4C8SJ6AX8kC801f1nO2ajxeBL4EwIzJ5vESAsFESHzEgLLBO64vETi8xwLYl8XiJyiQLiVBMUcJUC+LRK55xVyzpiU8lAIKeBMUcWiJxeInF4lQFl+cXnuuL8EET6xHzLe0MMUxWMFwu4+5k8HpeD32mLLY92mcyYqUVeJyxh77TzjSLfGRM50FlvoOwM98O4XcfiYjOZwht+jnF3AyY9vAtmU7AwWzshAtiqzCmGNp29pnEtFWHsliPGLycbSLcyfWMeCw84lQ9PAKYe5lxgy0WkxeecyYZ8308C0gOSDh4yJnOyODpc9hIwygph7lxnDCM/Rzit+joLxGiTpApgLM4lMEk8+C6QFxLw9DOzuItMFPGL6IMHDhmT6x2cFJdMr7c9zit8DUp7RtBrjCoNRsYl9VxhUGpX2jaDUp7Rb57GINSntFPOcdvG5H2cHvON93Ft5wqDTH0PSuBpi2zcj7ONyNoNSntG0GpT2i3Ckr4I98isNvEwqDUp7RblEUf4DdDo/2hoAWy14iVyL9IAJ0B1C3iJT6Q7jnivywO454o4C4lfjB0wEv2QVyFfjC74I55aB6ntOoAZlwnR+AJXAx6AtiV+TehwFsSn8gZg79Jb6LXCLQg9hnnMkgd1FbxgRFEENsW3i3wQdDwK3uVcPsi3B1Haipidi4eyL12TBrMt5OMpXdTshFromHHIjni3wTAeCx7lXD7It8H3FVi4Ew4yld1HukaPUey+OfA8Mk85aYBwO4NrK7qLgRsdw6Lh7oLZcDDGwKi3D2ReqTjz4yIwoxF7uJo4zg7mU8CZEwTPhaEG+mDuXA4GjohaN8bXPBnhOx1Ox3w+4lY9IODwLYqKMXAh0mLBXGFT1Hu0Ci4K5gQqLFU4VMTcwIODwqYO54EXAmEMMMxlU0cDzLzqGMo4Q9kXqkjaZlFToj2XLqFv6H4QU9kwccmYZi+XjFoS3gzG76mCZEHcGoVcquBUx7Ruu4CFkyrgOSFufQHu4O7mFDtEDccwzHtMEyIO5dSjcquGcemC+yHQvoD3A3MQdwEeBzxknnLeRheZDBfZMLh3Ap0eFvM6Rx3ATsme3HnKYZIlx04vYcEeidiPbMiYJnwliDu4u2MW88YFQum+KrnQCeEVoqYcZQdzsCQV28LuKuJUF9zoKzBhmMZ8BvpCFCriVHscGYO49gkFd8ndMvu4L7gUMwYK4zlx5EtKqGA3HoQELJkbnnDp4G67jhAv6IW4uXUtIFzAObWX6LS2CktxaW82lrBSKsFIq8BceiuBSKsGpbzaWwUluLS3lVgpFXgLj0VxaLcGpb02not4tgpFXi3FuBEVYKRV4FJb3lvFuBTm2WnFrL5tirxbkUluLcikVebWCkVebcCkVZbxczLZiW4twKc2wUirzhwKRV4tL9FubS2CkVeFWCIq8Cn1PUJrTWmtNaa01prTWmtNaa01JrTWmtNaa01prTWmtNaa01prTWmtNaa01prTWmtNaa01prTWmtNaa01prTWmtNaak1prTWmtNaa01prTWmtNaa01pqTWmtNaa01prTWmpNaa01prTWmtNaak1JrTWmtNaa01prTWmtNaa01prTU+q1gx2a2aWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWaWI5GKIsfVXJCI/biWMx/wDUMfVTJMH7d/tDH0WG7HNubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3NuNLRMHqMAWbc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Nubc25tzbm3Ei1flf7Qx9FaIpZ9oza5u/M3fmbvzN35m78zd+Zu/M3fmbvzN35m78zd+Zuzd+Zu/M3fmbvzN35m78zd+Zu/M3fmbvzN35m78zcm78zd+Zu/M3fmbvzN35m78zd+Zu/M3fmbvzN35m78zd+Zu/M3fmbvzN35m78zdm78zd+Zu/M3fmbvzN35m78zd+Zu/M3fmbvzN35m78zd+Zs/McqxCor0ndKzQHTDdm5Nybk3JuTdm7N2bk3ZuTdm5N2bk3ZuTdm5N2bk3Juzdm7N2bs3Juzcm7N2bs3JuTcm5Nybk3Zuzcm7Nybs3Juzcm7Nybs3Juzdm5N2bs3ZuTdm5N2bk3JjflFU6OT3/Kf2hj6KAdjue/6/kiKmGl7Po6U96/sR/iSp/k/wC0MfRT/sjl/X8nF+5fR9u/sT70/KX+0MfRT/sjl9YYoDza0uuRBTpxwOHocIgFsdoU+kSxxWgj+N4XqXndEPOJXNVYXyEoWGea2lfi5OL9y+j7d9bVC4iKMFUMQdBjP4Pn2342ZR6j8J2hbESJT+Da0v8AG+9Pyl/tDH0U/wCyOX1yEN3SuBJmwstHSjpJUy2PgJJBpTXCrYrUdjwwWD6ELvVcUuDU2g4EFsm1XPYI4Qco5z1Ff7os7e4u9fs3GvgYdAU+CWATTgix4OGHh6ZYDrmobUFOICC944+9iVmZdBxFBSezigeDtgMDtcOAy4uWLSJ0ymkUdQA0cdaQIzL2v+UvgaSpFMEAjeLF4cMAGbCwEsJ5PTk4v3L6Pt31oRMvUMrxw77WU4nsGgjEYzvNw1LmDVeg4CAeWXXpAUGH9tjGBiykpxPYj2RKw7ggKk4I1Qlqp7jHoj3CW6dBljCGTPApSU/M7/viE0wJ8RXZj2MhAFM6UAvMBShPMspNGTmqx/iFVwYiBiPSCVGIx3cGptES3sgKYItKVZnUE9IIIg7lCnuMrlPcJ4+IZOq+QlAO8ol7HD6vvT8nWf2hj6Kf9kc+p/cVROsA3cvHDexCe2VTo+B1Ldr7uIlynDRj2QV3npips2MKraIaXL0x0Xgt47gZ4AKGOP8Afz/uRG1iVHvCsxuEfaoySWuowY5WNueFTqP8uV9/CxdQLu+k78KGDh6NC9EGkIdzpOEX4AVFKwE63mAEe7naJqPFRiLKz7QK1PiJTGCsdTDK878J0YT1OOTi/cvo+3fUgB5YpZQlTaPDX2sdXmABHu468xIQuBfxP859T/t+Neg2p7ztvEJPwkvhZzHW5hUHuJyGNCbuUWILy6GzFS528xdSea4+54jAjknQ84RTa+7nY8pEr1EzxPxZ90wFQItA6zO1Bwzr/mTJvu50FmoaPL0x03gtnaPLnePI3KPs9EzYyL7uAd4RQAAxDRdERJAHfeO6TVR9wvHq+9Pyl/tDH0U/7I5fV/ljQBywMnCEsJncFd1MwHPYMT6Wk9pZjg7TliYp8k7TiFCrKJiFOzm7OlrJFvQe0d9y54ZEFI2lyITSyFe9mMT4iNLnJCxW8w230WWlUUwvbwGby+OA2ezGg9uOTMxiGYIRxE/7TuUirzyxipn3ie/ztI9iPj6qFquiIbNOzpht5aIfuI5unlLcJZIyt3L6cnF+5fR9u+plWgl4ISHohxcjqF6pTqMxxcA+ydwYMQ1XEhFF+2YbDHjgsUtjD5i8eAMbkWxwLi8B9kKNoHEce6ySt2Eyw06ct84Qr7DET7ExFtWMW4GAU9QqPYdwO3LMUoGk9gEIHq6IVL2w0qimIWPdlbOgjCPUuh9iWoOjU8xJV0TEednEjkxO5dHA9RGc5ioq64ZFgigpyOgtwrHtxGIeGH/7wQGXZ6vvT8pf7Qx9FP8Asjl/JGS4mpvJ/TMnF+5fR9u/qBVlxafeT+T+9Pyl/tDH0U/7o5f1/JxfuX0fbv7E+9Pyl/tDH0UB7FQ/X8kpcuBjte76O4PeP7E147ZYvyf9oY+ioEtOH3gsW/Xn+3/3/wB999t9t99/ttttt/8Afbbbbbff6pFRoPTcRLWSD1CfsJttttttttttttttttttttttttttttttttqyARylrn8o/tDH0WBwpuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3ZuxytWYPUKYZuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bs3Zuzdm7N2bsXyvyv8AaGPqpkmD9u/2hj6qZJg/bv8AaGPqpkmD9u/2hj6qZJg/bjP7Qx9FWVK5I/rN0RPZP6lpJJJJJJJJJJJJpJpNJttNJNJN36VJJJJJJJJJJJJJJJJJJJJFCliXePosxwQjwYj2QnUccFTzOjjpgTrjqP6ZlD85JJJJJJJJJJJJJDiQ50z6ZtscbM1PjjHGzzsj9NWkkkkkkkkkkkkkkkkkkkkkAB0fRZl9S/QVDPDiOIZnmM8S/Rcf2QZjCEuMvh4OGMIZ4Ywh54fqTcuXL5GXxcuX+yrl8XxcWXL9DL4vi5f+ktB3Lh5CxfUQUxpKHHIuJTyKrgW/sagCX6CV+GLv8JrkL4a+m+Lw8xUx7Lg9MO0WsEHVywEW4ZmcDBXby9CdgYPPDxmEV8C+JhMIJsSixUcR/XUsIx5zKPTFiDq4N5JVQtY4EIKuIlPC4QbGCW9oKY9YItzCWJ3MGLcyiqYRy7ZlFwJhCrQs1Hp+mWDKYZmXDBnRihitdxw4MzKJKirwFsYGiTBJTPGdqYZlwrRUuC8kFekld9QXDBT+u4HHlxlMCLwlx3lMnjBYKZlwPMHTPPgb8yxMBnYGAwy4yhnQXlgg7h0MMpkTL6ZCktLi3L6qCkW5aKsvlbiryNcDXFoqwUloKcCkUwUluLfr1paXXC3LQalvQKS0zwIluLcWirBSKZfF2xIxVmIqxbgiKsvqv/wJFSv23X1WZ4IR4I8UcVKgSuKOKOfHBAlEI/sIzGHFHFRIEqBwypUIxlSoFyj6hM8Sz0nLx4nU8zpmJ0xOPHBCVDMf2EcEJ1Hio44OGMIZ4Ywhw/UF9BGHHXoGdcdS51F46qdcE6OH9hE6lwnXHXFzqHDHg4eTh/hU/wDv8QnA6OKeDMYUHE6HIK7iU8UzKUwZ4p9FPFP8GVqoPJMCLQqK+njIjCLAo826J4MOhYK+JV0QbGectY2dEpRuYMsZmcsfwZwIvE8YlCCu2MyIxnzlLy6MOxIK+AX2QKGeXDb2Smu2BC/MC2EOf4NikVfw7eBSW82kteLXi3/VT//EAC0RAAIBAwIFBAMBAQADAQAAAAABERAxYSFBIFGh8PEwcHGRQFCAYLCBoMCx/9oACAEDAQE/EP8A2BZpP4DFSfQmk8U/wa6xWSayTSaTSSaSOs8LFSKSTR8U0kkmkkk0kn3+ik+hHCqQMQ/TkfGxVfA6Rwx7+zVUgj0VViGKjovRjhYqsVWLggj+AnwNcaqxVVVV8U8CHwMXoz/Cs/kyT/DMfkwR/wDBMtEEk1lUmkk0lUkmkqifFPFJNJpNJG6p0ms0kkbE6TqOxNJJrJJNJVJ1rNZpKpKrNZpJI3WaSTSUKsjHSVWazSUK9NxkCpJNJESN0kkmk0lUms0YhE0kYn7POiHRkFkIszcZsIVzcY6qrNjYRubjsKiNx3ptRCHVG4xcTruMQuB2EK4x0RvR2EbjgYyUbcO5djHaiHXQQrjHRUcDHYY6IVNzcYyBXo7oZIlRwNjohDHYVWSh02EK9NKOiEOq9nnSxejJpYubjNhG5YkZIhaE8Ni7GNyKiNxk0kRYYqIZIqSItRidHcdqbliaOwizLjRIh3o7CpuOkmxIhjEWLjpYdHRCGMkVNxjsMZIhjsIsSMkVNx8LGSIsMdqWoyTYkViwqXdHRCHTYXtsv17F/wBO2Sf0U/whvRXpNZE5GxOsk0kmkk0mkjpNJo2TSTejJ1pJNZJrNJJpJNJJpJNGJjsJk0kngmiuyeCaTrSSaSL3b3JEO9LMdLMYqMQ0I0Q2bCoxNUejGI3HelmOl3RXqrjEMVZQi7pYVLCHAhGjHYTXAh2EMdhQOBUu6JCuO9FcdzR03GKBwbC93NxiLOm46XYxWoxDFYQxWNGWpCFcYtWMRvS7HSzor13GIYqbmgizqqXEaU0Y9Kq9UMVhjsI0rZ0kVx3or0aEbjEaUXu5FIpFYEqRRiRFIIpBFIEqJDU1ijVYIrFIIq0QJDRFGiKQRSCKQJVirVIIq0RSBoggaIo1SCKL3pX/AFK3wR+Dv/Dr/D3/AIaX4i4n/wCoe0huXyQnqV+hu7u7u7u7u7u7u7u7u7u7u5q1ZaTqcn/B63hxetRu2QuRCIXIhciFyIXIhciEQuRC5ELkQuRC5EIhEIhELkQuRC5ELkQuRC5ELkQuRC5ELkQuRC5ELkQuRC5EIhckJLZZobBlfwcqWeyf6VYC2n8HdI/0vSe2xuCV6LcEPwW/3nSP0nCEi2hxzFyUvUdpNE29XpPbY7qiJExuCXRTwNxskbglkksnSick6jZLGyWSN/vOkfpOb8AO0YGnrNiFlOwmNA25o9JEU/nkc3ENXRJwq5PUuzGvGVC2acRvR9ds0m7ri76OiU9RNyTDN16XSe2zYNMVqm6qV3VqJEjkakloujVF0WRYQiwdv350j9JHxILyCpxKuN6VIcloFq6oEIoM0tDTFTc6mlyUMTTZKl2SE8mExnZ1zHSHkh1jBHBJb+l0ntsa1VEoIaIY0aiRDTo1WGQ2M1ZGhqJQNajUiUIghsapDQ03+86R+k1kyA8w126XGVfBcfLMvdegxpeBA2lo1PS5D8YNhy23jktyFwS7JXzzHScsW0MnjRJtc9vS6T+DnSP9L0n8HOkf8Z4iatLhIZKf6NpZCQ52on/BzwiUyXLiW432ZH2ZH2ZH2ZH2ZH2ZH2ZH2ZH2ZH2ZH2Zn2ZH2Zn2ZH2ZH2ZH2ZH2ZH2ZH2ZH2ZH2Zn2ZH2ZH2ZH2Zn2ZH2ZH2ZH2Zn2ZH2ZH2ODUn5EkbQaFSS0X/AML7P4k/lzSUTwT+LPvXI6SNirNZo2Jkk1msqrE6TxyTwzXcYkNcFxUkmkk0ms0kmrrvSVRvUTn3iZsIZAx2EhoVGaG4yDcYh3puM0NxmlWIZBZ1VxlkJDRsIZsJV3GJjc1YrCGJEDEiBmhuMUU3NHV13GaEDv7x3ZBZjox2EMVGQjQdNxiN6bjIRCpAqMQ6XdWXYxDNhDNhV3HwsgiB2ESMRIyEaUgRoNCo670gQ7+8aVGqQRSCOCBIapGtIIpGtIEhogVYpAlV2FSCKQRRVgfDFGqQRSBKkCQ0QxIaIq1JAkQNSQJDQv4Gj/qGs1E6vhVNVxMVN/xnXejf4DIYqp+s6qs8aF6TquBi/Dmq/AYuFv2UVNzcsMZsKiNxsVIEXY0bCRuO5aipAyKM2FYSLM3GKkIRdjRsQb0ZZC1LOjNjYSpuMdhKjuMlCox0uRSBkIXBFHRmwqJUY7CEIVhVagbIQqNUZAhFy1GIuWoyKXLDINhIsx0nQgSIU0Yi49KMQhalmMZsJUZBsbCNzcgZZURv7IWJEK4x8KFerFArlmM2Ezc3GKBU3HxIuzcbEMUCuWY2bCZvR0ReuBiZdm42OwqO4+C7IEIksyRmgjRivTejJorGguBDohWFRXGNGguBkiFTSjEIdGSXEMe1FYRuO5oOwnTejEIfChjGzYVGSbGwmK9Nxl0KKb+yMLiXoRWKRwQuGK70isVisKkeiqRWPUisUikcUKsUj0oIpCrBFI9GKx6UUjhikVu6RWOGKxxR7pNYRtngzwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngTwJ4E8CeBPAngRrUv6vdbccYvJJl/5zVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVWUkxIML191Vn4mMgvkS/wDNsJPVMeuo9Z7qdMzrF/nLHzR65+yzchr+/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbYR1i4rWKjw+Bt4fqNttttvD9VtttttvD4G3h8Dbw/UbbbbbeH6rbbbbbeHRsIS/iWPmj1z9lG0k2yUDNHUWQjimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpikGBiJT3XC6KptCtWzxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFMUxTFOTmbFAP8Ox80eufso99ClX7/pmK5Uz4u4EmOWP8IjnUOff4Vj5o9c/ZTqUWvj9/wBM6DouDr/8I6P8PWPmj1z9lOpRa+ON8kNVgZxNXCNLV6IrUHhkJEuZXA2kMoZSyp+m3Vo+gHSUUieU0kp0bSUsfNTResQBu1Zecx6vTOg6Lg6vjIU4hCpqzHyTRNhwkGz0f/xD1m0jbFBkv0pc8IWmSn6MPOJ9bo/w9Y+aPXP2U6lFr44xnTaSmjc0q49iaHqiT8JI1y26JLiejdNM70LR7oc1Fu47SzAoKJSqluj7DUqac3uyPbbWCGK1CWCsaJ6CSMvMLy7obKxFuxMkxVuLXuV0PfqDiszsRhjncbN/muVaDPjs1W5T6U0pd6I2I0NLsSMk009UIDRI020EzxyodpsJCr4nM2RJjJEonKtxW40auhneVcenZG3D0zoOi4Or40Cywey4511GktOdsuYRtQg7IE6UQJIqW6JebIiaJCzdEMrAK7zsmaJbtXMQww0bSItxgdyuibjkExgNlc1kIS1O1HFHc7ifnRJe0FGedNkS3lGqh2g/BWjYSJU5HZicpOkm+1xm8FabUyIWrWUqmUJBJSLJiYGGFd0SKpNoxqobWCO7QluKalhExXGtJkW4szMSYS0S64uj/D9j5o9c/ZTqUdJxf/rR5BOEEclxvOzFUbtmsLrjsiIEIKiltumSrtqhJiyUBVC+bJuaXRMG7hU5OUfpQN2oeiptGBCbUocMqJFzkZk3QXKRohqmjUzfIKqmZ2k7BrCmk3LXdF3BXElqY1oI6OrORZlq3qKEOrJRFG88tZEFpIPaeOYmribF7DuTfnKGXu1vwzpnQdFwdfxEvNkNdthCaE0PXUSADrMlEE02WGvbkfZO3ZXIsxtakAty79sTT3CtVtNDttZWEQZK6QPFolYIlXkhM7hZXKhLTzYlpRWVb3g1ZjTsWxFESiDRdhba1uRCLmsRqmjX8YpjeyHmm3Y0mncGv5BWZA1JaSXltUc7ThGj2KMdezUExG9zQRJ0kBnB2JLLbuPqfYSQaRyCbfbyI/w78XR/h6x80eufsp1KLXxxfBbGk1AUFiKpY5jxQR6IaWMKJ24ki5ej+yAmzWzGRlrDFZMOWMcYb0dZ4eUWmJE+3MWUQlYO0pFh0G4hMCzIaFA0LsQ43FGkKzI18gIXwricllyjmBAvUVbqOf8AmAhskripYx/VDsQMaoQNgPhLI6im3IWF2xcUIbEAsNjGilmKLojpmpJSd2YjIliGozNmKMKtuHpnQdFwdfxF10YZpJH63BA1lxgSRcTE4edWyOjuXJimsRpB6iar0YgOEPYHd7JCjLru6JNzAXOPXY24laZZlq6dg8HOtSORNXzULFRncSBDly1EuQ9SAAm9BqPgClL7ZMt2NpIljCmhuOqJcoRpu8CMolsWwkjNQH/kIRFuBDopY667qbohBsZq0xBVrC8WYdKtilmorGljXFxWiuxTmzRONy7It8W6P8PWPmj1z9lOpRa+PUhcUocXGlchcEL0IX4ULghUheh0zoOi4Ov9RC9SFSF6cL05Q4uKjNLF+H0f4esfNHrn7KdSi18fv+mdB0XB1f8AhHR/h6x80eufso9dCVX7/pmPAS7Rg5cDyXw/wilN3ohr78Kx80eufso0moY1zC65F8bT+KPtrgbfbVH21R9tUfbVH21R9tUfbXA2+2uBt9tcDb7a9Rttttt9tHbVW+2vUbbbbbfbVH21V7JU5ua4YSZFpjtdZb/4Jtttttttttttttttttttttttttttttttt6czYSVQlvw1j5o9c/ZZ3ymKYpimCYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKRcLQ6xcV0RmCYpimKYpimKYpimKYpimKYpimKYJimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpimKYpYF/EsfNHrn7qdMzrF/nLHzR65+6nTM6xf5yx80eufup0zOoX+csfNHrn7qdMxW/lX+cVv5Cw9d7qP5MLlkDvv8q222222222222998Ft77rb33W3vs77O+zvul774Lb33+uttttttttttttttttttttttqVrUPivWoS9mV+9by1HaSO0kdpI7SR2kjtJHaSO0kdpKi7SR2kqLtJHaSO0kdpI7SR2kjtJHaSO0kdpI7SR2kqLtJfk3UUW22002025dpI7SR2kjtJHaSO0kdpI7SR2kjtJHaSou0kdpI7SR2kjtJHaSO0kdpI7SR2kjtJHaSou0kdpKi7SR2kjtJHaSO0kdpI7SR2kjtJHaSO0kdpIakMPjW2/ZZUjhfA/8i6MfG6odUIdV7oR/jo/4n7NRVnjVhSS1VsVU6N/4bU14E/Tb9JVbii9t7kSbCUlmNalkQJm4hisJjdbsQ2RRcaHYSLssMgdiBfvU4ZNEC1QkMg2EqOipceg2QJi1EXIaHciBWFqWYgkKwlJZjuNQL2yd1Ww3HdDsJIUCu6MVhIijEizGTQqHaiS3IQxqDYhCt+9VHRCHchGwrUY0oLKIYyEKDRiESO6orUOpWEO5uh2Fb2yakgRVqaQiPQa4IVGiERRqSFSFSF++hELgikLgaIVYRCpCpCIGpIRFUtCKJCRCI/8AhflVetvwsX+Cfrv3Pfq70f8AhnR+g6IQx0Qv+S7NFwM2EWq+BUf8QoQi46M2Ey9UIZArCo+B0Zp/DKGIsOjNhVZBZjuSKwhjoqNjo4F/GUf8qf8A/9k=` ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SQLAdvisor/utils/suggestedCommandMaps.tsx ================================================ export const SuggestedCommandMaps: Record< string, (params: string[]) => string > = { execute_command: (params: string[]) => { return `${params.join(',')}` }, table_without_where_condition: (params: string[]) => { return `${params[0]} without where condition in the query` }, table_where_condition_with_func: (params: string[]) => { return `Where conditions of the ${params[0]} using funcs, it would cause index invalid` }, query_cannot_be_tuned_find_other_help: () => { return `The query can't be tuned. Please ask DBA for help` }, Found_index_in_table: () => { return `Foud correct index in the table` } } export function getSuggestedCommand(suggestion_key: string, params: string[]) { return SuggestedCommandMaps[suggestion_key] ? SuggestedCommandMaps[suggestion_key](params) : suggestion_key } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/components/Icon.tsx ================================================ import { CheckCircleTwoTone, InfoCircleTwoTone, LoadingOutlined } from '@ant-design/icons' import React from 'react' export function LoadingIcon() { return } export function SuccessIcon() { return } export function FailIcon() { return } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/components/LogRow.module.less ================================================ @import 'antd/es/style/themes/default.less'; .logRow { padding-left: @padding-page; // 48px padding-right: @padding-page; // 48px padding-top: 11px; padding-bottom: 11px; border-bottom: 1px solid rgb(243, 242, 241); // hardcode from Fluentui animation-duration: 0.367s; // hardcode from Fluentui animation-timing-function: cubic-bezier( 0.1, 0.25, 0.75, 0.9 ); // hardcode from Fluentui animation-fill-mode: both; // hardcode from Fluentui animation-name: fadeIn; font-size: 0.9rem; cursor: pointer; position: relative; pre { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 8; line-clamp: 8; -webkit-box-orient: vertical; } &.isExpanded pre { -webkit-line-clamp: initial; line-clamp: initial; } } @keyframes fadeIn { 0% { opacity: 0; } to { opacity: 1; } } .logRow { &:hover { border-left: 5px solid @gray-4; padding-left: @padding-page - 5px; } &:hover::before { content: attr(data-level); user-select: none; font-size: 0.8rem; position: absolute; left: 0; top: 0; padding: 0 5px; } } .logRow[data-level='DEBUG'], .logRow[data-level='INFO'] { &:hover::before { background-color: @gray-4; color: @gray-7; } } .logRow[data-level='WARN'] { border-left: 5px solid @orange-4; padding-left: @padding-page - 5px; &:hover::before { background-color: @orange-4; color: #fff; } } .logRow[data-level='ERROR'], .logRow[data-level='CRITICAL'] { border-left: 5px solid @red-6; padding-left: @padding-page - 5px; &:hover::before { background-color: @red-6; color: #fff; } } .cell { vertical-align: top; } .textCell { display: inline; } .infoCell { display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: @gray-3; padding: 0 5px; font-size: 0.8rem; margin-right: 5px; border-bottom: 3px solid @gray-4; } .highlight { background: @gold-3; margin: 0; padding: 0; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/components/LogRow.tsx ================================================ import cx from 'classnames' import { ModelRequestTargetNode } from '@lib/client' import { ICellStyleProps, IColumn, IDetailsRowProps } from 'office-ui-fabric-react/lib/DetailsList' import React, { useCallback, useState } from 'react' import TextHighlighter from 'react-highlight-words' import styles from './LogRow.module.less' import { Pre } from '@lib/components' import { InstanceKind, instanceKindName } from '@lib/utils/instanceTable' import { hsluvToHex } from 'hsluv' import moize from 'moize' export interface ComponentWithSortIndex extends ModelRequestTargetNode { sortIndex: number // range from [0, 1), used to determine component color } export interface ILogItem { key: number time?: string level?: string component?: ComponentWithSortIndex log?: string } export interface IRowProps extends IDetailsRowProps { item: ILogItem patterns: string[] } export function LogRow(props: IRowProps) { const [expanded, setExpanded] = useState(false) const handleClick = useCallback(() => { setExpanded((v) => !v) }, []) return (
) } interface IRowCacheableProps { // A subset of IRowProps for better caching item: ILogItem patterns: string[] cellStyleProps?: ICellStyleProps columns: IColumn[] } // This component is cached globally (instead of per-instance as React.memo) so that // it will work in virtualized lists. // When the props are unchanged, this function will always return the same vDOM. function LogRowCacheable_(props: IRowCacheableProps) { return (
      {props.columns.map((column, columnIdx) => {
        const colProps: IColProps = {
          column,
          columnIdx,
          ...props
        }
        switch (column.key) {
          case 'component':
            return 
          case 'log':
            return 
          default:
            return (
              
                {props.item[column.key]}
              
            )
        }
      })}
    
) } const LogRowCacheable = moize(LogRowCacheable_, { isShallowEqual: true, maxArgs: 2, maxSize: 1000 }) interface IColProps extends IRowCacheableProps { column: IColumn columnIdx: number children?: React.ReactNode htmlAttributes?: React.HTMLAttributes } function BaseInfoColumn({ column, columnIdx, children, htmlAttributes, cellStyleProps }: IColProps) { let maxWidth if (column.calculatedWidth) { maxWidth = column.calculatedWidth + (cellStyleProps?.cellLeftPadding ?? 0) + (cellStyleProps?.cellRightPadding ?? 0) if (columnIdx === 0) { maxWidth -= 48 // hardcoded @padding-page } } const { style, className, ...restHtmlAttributes } = htmlAttributes ?? {} return (
{children}
) } function BaseTextColumn({ column, children, htmlAttributes }: IColProps) { const { className, ...restHtmlAttributes } = htmlAttributes ?? {} return (
{children}
) } function ColumnMessage(props: IColProps) { return ( new RegExp(p, 'gi'))} textToHighlight={props.item.log} /> ) } function ColumnComponent(props: IColProps) { const { item } = props if (!item.component) { return null } return ( {item.component.kind ? instanceKindName(item.component.kind as InstanceKind) : '?'}{' '} {item.component.display_name} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/components/SearchHeader.tsx ================================================ import { LogsearchCreateTaskGroupRequest, ModelRequestTargetNode } from '@lib/client' import { Button, Form, Input, Select, Modal } from 'antd' import React, { useState, useCallback, useRef, useContext } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { useMount } from 'ahooks' import { TimeRangeSelector, TimeRange, calcTimeRange, InstanceSelect, IInstanceSelectRefProps } from '@lib/components' import { ValidLogLevels, LogLevelText } from '../utils' import { SearchLogsContext } from '../context' interface Props { taskGroupID?: number } interface IFormProps { timeRange?: TimeRange logLevel?: number instances?: string[] keywords?: string } export default function SearchHeader({ taskGroupID }: Props) { const ctx = useContext(SearchLogsContext) const { t } = useTranslation() const navigate = useNavigate() const [form] = Form.useForm() const [isSubmitting, setSubmitting] = useState(false) const instanceSelect = useRef(null) useMount(() => { async function fetchData() { if (!taskGroupID) { return } const res = await ctx!.ds.logsTaskgroupsIdGet(String(taskGroupID)) const { task_group, tasks } = res.data const { start_time, end_time, min_level, patterns } = task_group?.search_request ?? {} const fieldsValue: IFormProps = { timeRange: { type: 'absolute', value: [start_time! / 1000, end_time! / 1000] }, logLevel: min_level || 2, instances: (tasks ?? []) .filter((t) => t.target && t.target!.display_name) .map((t) => t.target!.display_name!), keywords: (patterns ?? []).join(' ') } form.setFieldsValue(fieldsValue) } fetchData() }) const handleSearch = useCallback( async (fieldsValue: IFormProps) => { if ( !fieldsValue.instances || fieldsValue.instances.length === 0 || !fieldsValue.logLevel || !fieldsValue.timeRange ) { Modal.error({ content: 'Some required fields are not filled' }) return } if (!instanceSelect.current) { Modal.error({ content: 'Internal error: Instance select is not ready' }) return } const targets: ModelRequestTargetNode[] = instanceSelect .current!.getInstanceByKeys(fieldsValue.instances) .map((instance) => { let port switch (instance.instanceKind) { case 'pd': case 'tikv': case 'tiflash': case 'ticdc': case 'tso': case 'scheduling': port = instance.port break case 'tidb': case 'tiproxy': port = instance.status_port break } return { kind: instance.instanceKind, display_name: instance.key, ip: instance.ip, port } }) .filter((i) => i.port != null) const [startTime, endTime] = calcTimeRange(fieldsValue.timeRange) const req: LogsearchCreateTaskGroupRequest = { targets, request: { start_time: startTime * 1000, // unix millionsecond end_time: endTime * 1000, // unix millionsecond min_level: fieldsValue.logLevel, patterns: (fieldsValue.keywords ?? '').split(/\s+/) // 'foo boo' => ['foo', 'boo'] } } try { setSubmitting(true) const result = await ctx!.ds.logsTaskgroupPut(req) const id = result?.data?.task_group?.id if (id) { navigate(`/search_logs/detail?id=${id}`) } } finally { setSubmitting(false) } }, [navigate, ctx] ) return (
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/components/SearchProgress.tsx ================================================ import { Button, Modal, Tree } from 'antd' import _ from 'lodash' import React, { useEffect, useState, useMemo, useCallback, useContext } from 'react' import { useTranslation } from 'react-i18next' import { getValueFormat } from '@baurine/grafana-value-formats' import { LogsearchTaskModel } from '@lib/client' import { AnimatedSkeleton } from '@lib/components' import { FailIcon, LoadingIcon, SuccessIcon } from './Icon' import { TaskState } from '../utils' import styles from './Styles.module.less' import { instanceKindName, InstanceKinds } from '@lib/utils/instanceTable' import { SearchLogsContext } from '../context' const { confirm } = Modal const taskStateIcons = { [TaskState.Running]: LoadingIcon, [TaskState.Finished]: SuccessIcon, [TaskState.Error]: FailIcon } function getLeafNodes(tasks: LogsearchTaskModel[]) { return tasks.map((task) => { const title = ( {task.target?.display_name ?? ''}{' '} ({getValueFormat('bytes')(task.size!, 1)}) ) return { key: String(task.id), title, icon: taskStateIcons[task.state || TaskState.Error], disableCheckbox: !task.size || task.state !== TaskState.Finished } }) } function parentNodeIcon(tasks: LogsearchTaskModel[]) { // Running: has at least one task running if (tasks.some((task) => task.state === TaskState.Running)) { return LoadingIcon } // Finished: all tasks are finished if (!tasks.some((task) => task.state !== TaskState.Finished)) { return SuccessIcon } // Failed: no task is running, and has failed task return FailIcon } function parentNodeCheckable(tasks: LogsearchTaskModel[]) { // Checkable: at least one task has finished and the log must not be empty return ( tasks.some((task) => task.state === TaskState.Finished) && tasks.reduce((acc, task) => (acc += task.size || 0), 0) > 0 ) } interface Props { taskGroupID: number tasks: LogsearchTaskModel[] toggleReload: () => void } export default function SearchProgress({ taskGroupID, tasks, toggleReload }: Props) { const ctx = useContext(SearchLogsContext) const [checkedKeys, setCheckedKeys] = useState([]) const [isLoading, setIsLoading] = useState(true) const { t } = useTranslation() useEffect(() => { if (tasks !== undefined && tasks.length > 0) { setIsLoading(false) } }, [tasks]) const descriptionArray = useMemo( () => [ t('search_logs.progress.running'), t('search_logs.progress.success'), t('search_logs.progress.failed') ], [t] ) const describeProgress = useCallback( (tasks: LogsearchTaskModel[]) => { const arr = [0, 0, 0] tasks.forEach((task) => { const state = task.state if (state !== undefined) { arr[state - 1]++ } }) const res: string[] = [] arr.forEach((count, index) => { if (index < 1 || count <= 0) { return } const str = `${count} ${descriptionArray[index]}` res.push(str) }) return ( res.join(', ') + ' (' + getValueFormat('bytes')(_.sumBy(tasks, 'size'), 1) + ')' ) }, [descriptionArray] ) const treeData = useMemo(() => { const data: any[] = [] const tasksByIK = _.groupBy(tasks, (t) => t.target?.kind) InstanceKinds.forEach((ik) => { const tasks = tasksByIK[ik] if (!tasks) { return } const title = ( {instanceKindName(ik)} {describeProgress(tasks)} ) data.push({ title, key: ik, icon: parentNodeIcon(tasks), disableCheckbox: !parentNodeCheckable(tasks), children: getLeafNodes(tasks) }) }) return data }, [tasks, describeProgress]) async function handleDownload() { if (taskGroupID < 0) { return } // filter out all parent node const keys = checkedKeys.filter( (key) => !InstanceKinds.some((ik) => ik === key) ) const res = await ctx!.ds.logsDownloadAcquireTokenGet(keys) const token = res.data if (!token) { return } const url = `${ctx!.cfg.apiPathBase}/logs/download?token=${token}` window.location.href = url } async function handleCancel() { if (taskGroupID < 0) { return } confirm({ title: t('search_logs.confirm.cancel_tasks'), onOk() { ctx!.ds.logsTaskgroupsIdCancelPost(taskGroupID + '') toggleReload() } }) } async function handleRetry() { if (taskGroupID < 0) { return } confirm({ title: t('search_logs.confirm.retry_tasks'), onOk() { ctx!.ds.logsTaskgroupsIdRetryPost(taskGroupID + '') toggleReload() } }) } const handleCheck = useCallback((checkedKeys) => { setCheckedKeys(checkedKeys as string[]) }, []) return ( {tasks && ( <>
{describeProgress(tasks)}
)}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/components/SearchResult.tsx ================================================ import { LogsearchTaskModel } from '@lib/client' import { CardTable, Card } from '@lib/components' import { Alert } from 'antd' import React, { useEffect, useState, useMemo, useCallback, useContext } from 'react' import { useTranslation } from 'react-i18next' import dayjs from 'dayjs' import { LogLevelText } from '../utils' import { DetailsListLayoutMode, IColumn, IDetailsRowProps } from 'office-ui-fabric-react/lib/DetailsList' import { ComponentWithSortIndex, ILogItem, LogRow } from './LogRow' import { sortBy } from 'lodash' import { SearchLogsContext } from '../context' import { tz } from '@lib/utils' interface Props { patterns: string[] taskGroupID: number tasks: LogsearchTaskModel[] } export default function SearchResult({ patterns, taskGroupID, tasks }: Props) { const ctx = useContext(SearchLogsContext) const [logPreviews, setData] = useState([]) const { t } = useTranslation() const [loading, setLoading] = useState(true) const componentByTaskId = useMemo(() => { const sortedComponents = sortBy( tasks.map((t) => ({ ...t.target, sortIndex: 0, taskId: t.id })), (target) => `${target?.kind} ${target?.display_name}` ) sortedComponents.forEach((c, idx) => { c.sortIndex = idx / sortedComponents.length }) const byTaskId: Record = {} sortedComponents.forEach((c) => { byTaskId[c.taskId ?? -1] = c }) return byTaskId }, [tasks]) useEffect(() => { async function getLogPreview() { if (!taskGroupID) { return } try { const res = await ctx!.ds.logsTaskgroupsIdPreviewGet(taskGroupID + '') setData( res.data.map((value, index): ILogItem => { return { key: index, time: dayjs(value.time) .utcOffset(tz.getTimeZone()) .format('YYYY-MM-DD HH:mm:ss (UTCZ)'), level: LogLevelText[value.level ?? 0], component: componentByTaskId[value.task_id ?? -1], log: value.message } }) ) } finally { setLoading(false) } } if (tasks.length > 0 && taskGroupID !== tasks[0].task_group_id) { setLoading(true) } getLogPreview() }, [taskGroupID, componentByTaskId, tasks, ctx]) const renderRow = useCallback( (props?: IDetailsRowProps, defaultRender?) => { if (!props) { return null } return }, [patterns] ) const columns = useMemo( () => [ { name: t('search_logs.preview.time'), key: 'time', fieldName: 'time', minWidth: 120, maxWidth: 200 }, { name: t('search_logs.preview.component'), key: 'component', minWidth: 40, maxWidth: 150 }, { name: t('search_logs.preview.log'), key: 'log', minWidth: 100, maxWidth: 100, isResizable: false } ], [t] ) return (
{!loading && ( )}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/components/Styles.module.less ================================================ // FIXME: Use .buttons { margin-top: 12px; } .buttons > :global(button) { margin-right: 12px; margin-bottom: 12px; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/components/index.ts ================================================ import SearchHeader from './SearchHeader' import SearchProgress from './SearchProgress' import SearchResult from './SearchResult' export { SearchHeader, SearchProgress, SearchResult } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { LogsearchCreateTaskGroupRequest, LogsearchTaskGroupResponse, LogsearchTaskGroupModel, LogsearchPreviewModel, TopologyTiDBInfo, ClusterinfoStoreTopologyResponse, TopologyPDInfo, TopologyTiCDCInfo, TopologyTiProxyInfo, TopologyTSOInfo, TopologySchedulingInfo } from '@lib/client' import { IContextConfig, ReqConfig } from '@lib/types' export interface ISearchLogsDataSource { logsDownloadAcquireTokenGet( id?: Array, options?: ReqConfig ): AxiosPromise // logsDownloadGet(token: string, options?: ReqConfig): AxiosPromise logsTaskgroupPut( request: LogsearchCreateTaskGroupRequest, options?: ReqConfig ): AxiosPromise logsTaskgroupsGet( options?: ReqConfig ): AxiosPromise> logsTaskgroupsIdCancelPost( id: string, options?: ReqConfig ): AxiosPromise logsTaskgroupsIdDelete(id: string, options?: ReqConfig): AxiosPromise logsTaskgroupsIdGet( id: string, options?: ReqConfig ): AxiosPromise logsTaskgroupsIdPreviewGet( id: string, options?: ReqConfig ): AxiosPromise> logsTaskgroupsIdRetryPost( id: string, options?: ReqConfig ): AxiosPromise getTiDBTopology(options?: ReqConfig): AxiosPromise> getStoreTopology( options?: ReqConfig ): AxiosPromise getPDTopology(options?: ReqConfig): AxiosPromise> getTiCDCTopology(options?: ReqConfig): AxiosPromise> getTiProxyTopology( options?: ReqConfig ): AxiosPromise> getTSOTopology(options?: ReqConfig): AxiosPromise> getSchedulingTopology( options?: ReqConfig ): AxiosPromise> } export interface ISearchLogsContext { ds: ISearchLogsDataSource cfg: IContextConfig } export const SearchLogsContext = createContext(null) export const SearchLogsProvider = SearchLogsContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/index.tsx ================================================ import React, { useContext } from 'react' import { Root, ParamsPageWrapper } from '@lib/components' import { HashRouter as Router, Route, Routes } from 'react-router-dom' import { useLocationChange } from '@lib/hooks/useLocationChange' import { addTranslations } from '@lib/utils/i18n' import { LogSearch, LogSearchHistory, LogSearchDetail } from './pages' import { SearchLogsContext } from './context' import translations from './translations' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> } /> } /> ) } export default function () { const ctx = useContext(SearchLogsContext) if (ctx === null) { throw new Error('SearchLogsContext must not be null') } return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/pages/LogSearch.tsx ================================================ import { Empty } from 'antd' import React from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { Card } from '@lib/components' import { SearchHeader } from '../components' export default function LogSearch() { const { t } = useTranslation() return (
{t('search_logs.page.view')}{' '} {t('search_logs.page.search_histroy')}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/pages/LogSearchDetail.tsx ================================================ import { Button, Drawer } from 'antd' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import React, { useContext, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { ArrowLeftOutlined } from '@ant-design/icons' import { Head } from '@lib/components' import { useClientRequestWithPolling } from '@lib/utils/useClientRequest' import { SearchHeader, SearchProgress, SearchResult } from '../components' import { TaskState } from '../utils' import useQueryParams from '@lib/utils/useQueryParams' import { SearchLogsContext } from '../context' export default function LogSearchingDetail() { const ctx = useContext(SearchLogsContext) const { t } = useTranslation() const { id } = useQueryParams() const [reloadKey, setReloadKey] = useState(false) const [taskWasUnfinished, setTaskUnfinished] = useState(false) const [sidebarOpen, setSidebarOpen] = useState(undefined) function toggleReload() { setReloadKey(!reloadKey) } const taskGroupID = id === undefined ? 0 : +id function isFinished(data) { if (taskGroupID < 0) { return true } if (!data) { return false } if (data.tasks.some((task) => task.state === TaskState.Running)) { return false } return true } const { data } = useClientRequestWithPolling( (reqConfig) => ctx!.ds.logsTaskgroupsIdGet(id, reqConfig), { shouldPoll: (data) => !isFinished(data) } ) const tasks = useMemo(() => data?.tasks ?? [], [data]) useEffect(() => { for (const task of data?.tasks ?? []) { if (task.state !== TaskState.Finished) { setTaskUnfinished(true) break } } }, [data]) return ( <>
{t('search_logs.nav.search_logs')} } titleExtra={ } >
setSidebarOpen(false)} > ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/pages/LogSearchHistory.tsx ================================================ import { LogsearchTaskGroupModel } from '@lib/client' import { Head, CardTable, DateTime } from '@lib/components' import { ArrowLeftOutlined, ExclamationCircleOutlined } from '@ant-design/icons' import { Badge, Button, Modal, Space } from 'antd' import React, { useContext, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { Selection, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import { LogLevelText } from '../utils' import { SearchLogsContext } from '../context' function componentRender({ target_stats: stats, t }) { // FIXME: Extract common util const r: Array = [] if (stats?.num_tidb_nodes) { r.push(`${stats.num_tidb_nodes} ${t('distro.tidb')}`) } if (stats?.num_tikv_nodes) { r.push(`${stats.num_tikv_nodes} ${t('distro.tikv')}`) } if (stats?.num_pd_nodes) { r.push(`${stats.num_pd_nodes} ${t('distro.pd')}`) } return {r.join(', ')} } function timeRender({ search_request }: LogsearchTaskGroupModel) { return ( {search_request?.start_time && ( )} {' ~ '} {search_request?.end_time && ( )} ) } function levelRender({ search_request: request }: LogsearchTaskGroupModel) { return LogLevelText[request?.min_level!] } function patternRender({ search_request: request }: LogsearchTaskGroupModel) { return (request?.patterns ?? []).join(' ') } export default function LogSearchingHistory() { const ctx = useContext(SearchLogsContext) const [taskGroups, setTaskGroups] = useState([]) const [selectedRowKeys, setRowKeys] = useState([]) const { t } = useTranslation() useEffect(() => { async function getData() { const res = await ctx!.ds.logsTaskgroupsGet() setTaskGroups(res.data) } getData() }, [ctx]) function stateRender({ state }: LogsearchTaskGroupModel) { switch (state) { case 1: return ( ) case 2: return ( ) default: return } } function actionRender(taskGroup: LogsearchTaskGroupModel) { if (taskGroup.id === 0) { return } return ( {t('search_logs.history.detail')} ) } async function handleDeleteSelected() { Modal.confirm({ title: t('search_logs.history.delete_confirm_title'), icon: , content: t('search_logs.history.delete_selected_confirm_content'), okText: t('search_logs.history.delete'), cancelText: t('search_logs.common.cancel'), okButtonProps: { danger: true }, onOk: async () => { for (const taskGroupID of selectedRowKeys) { await ctx!.ds.logsTaskgroupsIdDelete(taskGroupID) } const res = await ctx!.ds.logsTaskgroupsGet() setTaskGroups(res.data) } }) } async function handleDeleteAll() { Modal.confirm({ title: t('search_logs.history.delete_confirm_title'), icon: , content: t('search_logs.history.delete_all_confirm_content'), okText: t('search_logs.history.delete'), cancelText: t('search_logs.common.cancel'), okButtonProps: { danger: true }, onOk: async () => { const allKeys = taskGroups.map((taskGroup) => taskGroup.id) for (const key of allKeys) { if (key === undefined) { continue } await ctx!.ds.logsTaskgroupsIdDelete(String(key)) } const res = await ctx!.ds.logsTaskgroupsGet() setTaskGroups(res.data) } }) } const rowSelection = new Selection({ onSelectionChanged: () => { const items = rowSelection.getSelection() as LogsearchTaskGroupModel[] setRowKeys(items.map((item) => item.id!.toString())) } }) const columns = [ { name: t('search_logs.common.time_range'), key: 'time', minWidth: 200, maxWidth: 300, onRender: timeRender }, { name: t('search_logs.preview.level'), key: 'level', minWidth: 70, maxWidth: 120, onRender: levelRender }, { name: t('search_logs.history.instances'), key: 'target_stats', minWidth: 100, maxWidth: 250, onRender: (p) => componentRender({ ...p, t }) }, { name: t('search_logs.common.keywords'), key: 'keywords', minWidth: 100, maxWidth: 200, onRender: patternRender }, { name: t('search_logs.history.status'), key: 'state', minWidth: 100, maxWidth: 150, onRender: stateRender }, { name: t('search_logs.history.action'), key: 'action', minWidth: 100, maxWidth: 200, onRender: actionRender } ] return (
{t('search_logs.nav.search_logs')} } titleExtra={ } />
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/pages/index.ts ================================================ import LogSearch from './LogSearch' import LogSearchHistory from './LogSearchHistory' import LogSearchDetail from './LogSearchDetail' export { LogSearch, LogSearchHistory, LogSearchDetail } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/translations/en.yaml ================================================ search_logs: nav_title: Search Logs nav: search_logs: Search Logs detail: Search Result history: History show_sidebar: Download or Show Progress page: intro: Preview and download logs by clicking Search tip: The preview shows only the first 500 logs view: View search_histroy: search histroy common: time_range: Time Range start_time: Start Time end_time: End Time log_level: Log Level components: instances keywords: Keywords keywords_placeholder: Keywords, Optional, separated by spaces search: Search progress: Progress download_selected: Download selected cancel: Cancel retry: Retry progress: running: running success: completed failed: failed confirm: cancel_tasks: Are you sure you want to cancel all running log search tasks? retry_tasks: Are you sure you want to retry all failed log search tasks? preview: time: Time level: Level component: Component log: Log history: instances: Instances running: Running finished: Finished delete_selected: Delete selected delete_all: Delete All status: Status action: Action detail: Detail delete: Delete delete_confirm_title: Delete Log Search Histories delete_selected_confirm_content: Are you sure you want to delete selected log search histories? delete_all_confirm_content: Are you sure you want to delete all log search histories? ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/translations/zh.yaml ================================================ search_logs: nav_title: 日志搜索 nav: search_logs: 日志搜索 detail: 搜索结果 history: 历史搜索 show_sidebar: 下载或显示进度 page: intro: 点击搜索预览和下载日志 tip: 预览仅显示前 500 项日志 view: 查看 search_histroy: 搜索历史 common: time_range: 时间范围 start_time: 起始时间 end_time: 结束时间 log_level: 日志等级 components: 选择实例 keywords: 关键字 keywords_placeholder: 搜索关键字,可选,以空格分割 search: 搜索 progress: 搜索进度 download_selected: 下载选中日志 cancel: 取消 retry: 重试 progress: running: 正在运行 success: 成功 failed: 失败 confirm: cancel_tasks: 确认要取消正在运行的日志搜索任务么? retry_tasks: 确认要重试所有失败的日志搜索任务么? preview: time: 时间 level: 日志等级 component: 组件 log: 日志 history: instances: 实例 running: 正在搜索 finished: 已完成 delete_selected: 删除选中的任务 delete_all: 删除全部任务 status: 状态 action: 操作 detail: 查看详情 delete: 删除 delete_confirm_title: 删除搜索历史 delete_selected_confirm_content: 确认要删除选中的搜索历史吗? delete_all_confirm_content: 确认要删除所有的搜索历史吗? ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SearchLogs/utils/index.ts ================================================ export enum LogLevel { Unknown = 0, Debug, Info, Warn, Trace, Critical, Error } export const LogLevelText = { [LogLevel.Unknown]: 'UNKNOWN', [LogLevel.Debug]: 'DEBUG', [LogLevel.Info]: 'INFO', [LogLevel.Warn]: 'WARN', [LogLevel.Trace]: 'TRACE', [LogLevel.Critical]: 'CRITICAL', [LogLevel.Error]: 'ERROR' } export const ValidLogLevels = [ LogLevel.Debug, LogLevel.Info, LogLevel.Warn, // LogLevel.Trace, LogLevel.Critical, LogLevel.Error ] export enum TaskState { Running = 1, Finished, Error } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/components/LimitTimeRange.tsx ================================================ import { TimeRange, TimeRangeSelector } from '@lib/components' import dayjs from 'dayjs' import React, { useState } from 'react' interface LimitTimeRangeProps { value: TimeRange onChange: (val: TimeRange) => void } type RangeValue = [dayjs.Dayjs | null, dayjs.Dayjs | null] | null const RECENT_SECONDS = [10 * 60, 30 * 60, 60 * 60] export const LimitTimeRange: React.FC = ({ value, onChange }) => { const [dates, setDates] = useState(null) const disabledDate = (current: dayjs.Dayjs) => { if (!dates) { return false } const inOneDay = dates[0] && dates[0].hour() < 23 const inOneDayTooLate = dates[0] && inOneDay && current.diff(dates[0], 'day') > 0 const tooLate = dates[0] && !inOneDay && current.diff(dates[0], 'day') === 1 const inOneDay2 = dates[1] && dates[1].hour() > 0 const inOneDay2TooEarly = dates[1] && inOneDay2 && dates[1].diff(current, 'day') > 0 const tooEarly = dates[1] && !inOneDay2 && dates[1].diff(current, 'day') === 1 return !!inOneDayTooLate || !!tooLate || !!inOneDay2TooEarly || !!tooEarly } const disabledTime = (_, type) => { if (type === 'start' && dates?.[1]) { const h = dates[1].hour() return { disabledHours: () => range(0, 60).filter((r) => r !== h - 1), disabledMinutes: () => [], disabledSeconds: () => [] } } if (type === 'end' && dates?.[0]) { const h = dates[0].hour() return { disabledHours: () => range(0, 60).filter((r) => r !== h + 1), disabledMinutes: () => [], disabledSeconds: () => [] } } return { disabledHours: () => [], disabledMinutes: () => [], disabledSeconds: () => [] } } const onOpenChange = (open: boolean) => { if (open) { setDates([null, null]) } else { setDates(null) } } return ( setDates(val)} onOpenChange={onOpenChange} /> ) } const range = (start: number, end: number) => { const result: number[] = [] for (let i = start; i < end; i++) { result.push(i) } return result } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/components/SlowQueriesTable.tsx ================================================ import { useMemoizedFn } from 'ahooks' import React, { useCallback, useContext } from 'react' import { CardTable, ICardTableProps } from '@lib/components' import { ISlowQueryTableController } from '../utils/useSlowQueryTableController' import openLink from '@lib/utils/openLink' import { useNavigate } from 'react-router-dom' import { SlowQueryContext } from '../context' interface Props extends Partial { controller: ISlowQueryTableController detailPathPrefix?: string } function SlowQueriesTable({ controller, detailPathPrefix = '/slow_query/detail', ...restProps }: Props) { const ctx = useContext(SlowQueryContext) const navigate = useNavigate() const handleRowClick = useMemoizedFn( (rec, idx, ev: React.MouseEvent) => { ctx?.event?.selectSlowQueryItem(rec) controller.saveClickedItemIndex(idx) openLink( `/slow_query/detail?digest=${rec.digest}&connection_id=${rec.connection_id}×tamp=${rec.timestamp}`, ev, navigate ) } ) const getKey = useCallback((row) => `${row.digest}_${row.timestamp}`, []) return ( ) } export default SlowQueriesTable ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/components/index.ts ================================================ import SlowQueriesTable from './SlowQueriesTable' export { SlowQueriesTable } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { SlowqueryModel, SlowqueryGetListRequest } from '@lib/client' import { IContextConfig, ReqConfig } from '@lib/types' import { PromDataSuccessResponse } from '@lib/utils' export interface ISlowQueryDataSource { getDatabaseList( beginTime: number, endTime: number, options?: ReqConfig ): AxiosPromise> infoListResourceGroupNames(options?: ReqConfig): AxiosPromise> slowQueryAvailableFieldsGet(options?: ReqConfig): AxiosPromise> slowQueryListGet( beginTime?: number, db?: Array, desc?: boolean, digest?: string, endTime?: number, fields?: string, limit?: number, orderBy?: string, plans?: Array, resourceGroup?: Array, text?: string, showInternal?: boolean, options?: ReqConfig ): AxiosPromise> slowQueryDetailGet( connectId?: string, digest?: string, timestamp?: number, options?: ReqConfig ): AxiosPromise slowQueryDownloadTokenPost( request: SlowqueryGetListRequest, options?: ReqConfig ): AxiosPromise slowQueryAnalyze?(start: number, end: number): AxiosPromise slowQueryDownloadDBFile?(begin_time: number, end_time: number): AxiosPromise promqlQuery?( query: string, time: number, timeout: string ): AxiosPromise promqlQueryRange?( query: string, start: number, end: number, step: string ): AxiosPromise } export interface ISlowQueryEvent { selectSlowQueryItem(item: SlowqueryModel): void } export interface ISlowQueryConfig extends IContextConfig { enableExport: boolean showDBFilter: boolean showResourceGroupFilter: boolean showDigestFilter: boolean showHelp?: boolean // true means the list api will return all fields value of an item, not just the selected fields // in this case, the detail page doesn't need to request detail api any more listApiReturnDetail?: boolean // true means start to search instantly after changing any filter options // false means only to start searching after clicking the "Query" button instantQuery?: boolean // to limit the time range picker range timeRangeSelector?: { recentSeconds: number[] customAbsoluteRangePicker: boolean timeRangeLimit?: number } // for clinic orgName?: string clusterName?: string showTopSlowQueryLink?: boolean showDownloadSlowQueryDBFile?: boolean // show internal slow queries showInternalFilter?: boolean // show RU V2 fields (clinic-only: ru_v2, ru_v2_detail columns, // Basic-tab rows, and the dedicated RU V2 Metrics tab) showRuV2?: boolean } export interface ISlowQueryContext { ds: ISlowQueryDataSource event?: ISlowQueryEvent cfg: ISlowQueryConfig } export const SlowQueryContext = createContext(null) export const SlowQueryProvider = SlowQueryContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/index.tsx ================================================ import React, { useContext } from 'react' import { Root } from '@lib/components' import { HashRouter as Router, Route, Routes } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useLocationChange } from '@lib/hooks/useLocationChange' import { addTranslations } from '@lib/utils/i18n' import { List, Detail } from './pages' import { SlowQueryContext } from './context' import translations from './translations' addTranslations(translations) // Create a client const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1 // refetchOnMount: false, // refetchOnReconnect: false, } } }) function AppRoutes() { useLocationChange() return ( } /> } /> ) } export default function () { const context = useContext(SlowQueryContext) if (context === null) { throw new Error('SlowQueryContext must not be null') } return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabBasic.tsx ================================================ import React from 'react' import { SlowqueryModel } from '@lib/client' import { DateTime } from '@lib/components' import { getValueFormat } from '@baurine/grafana-value-formats' export const tabBasicItems = ( data: SlowqueryModel, options?: { showRuV2?: boolean } ) => [ { key: 'timestamp', value: }, { key: 'digest', value: data.digest }, { key: 'is_internal', value: data.is_internal }, { key: 'is_success', value: data.success }, { key: 'is_prepared', value: data.prepared }, { key: 'is_plan_from_cache', value: data.plan_from_cache }, { key: 'is_plan_from_binding', value: data.plan_from_binding }, { key: 'db', value: data.db }, { key: 'index_names', value: data.index_names }, { key: 'stats', value: data.stats }, { key: 'backoff_types', value: data.backoff_types }, { key: 'memory_max', value: getValueFormat('bytes')(data.memory_max || 0, 1) }, { key: 'mem_arbitration', value: getValueFormat('s')(data.mem_arbitration || 0, 1) }, { key: 'disk_max', value: getValueFormat('bytes')(data.disk_max || 0, 1) }, { key: 'instance', value: data.instance }, { key: 'connection_id', value: data.connection_id }, { key: 'user', value: data.user }, { key: 'host', value: data.host }, { key: 'ru', value: data.ru }, ...(options?.showRuV2 ? [{ key: 'ru_v2', value: data.ru_v2 }] : []), { key: 'resource_group', value: data.resource_group } ] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabCopr.tsx ================================================ import React from 'react' import { SlowqueryModel } from '@lib/client' import { ValueWithTooltip } from '@lib/components' export const tabCoprItems = (data: SlowqueryModel) => [ { key: 'request_count', value: }, { key: 'process_keys', value: }, { key: 'total_keys', value: }, { key: 'cop_proc_addr', value: data.cop_proc_addr }, { key: 'cop_wait_addr', value: data.cop_wait_addr }, { key: 'rocksdb_block_cache_hit_count', value: }, { key: 'rocksdb_block_read_byte', value: }, { key: 'rocksdb_block_read_count', value: }, { key: 'rocksdb_delete_skipped_count', value: }, { key: 'rocksdb_key_skipped_count', value: }, { key: 'unpacked_bytes_sent_tikv_total', value: ( ) }, { key: 'unpacked_bytes_received_tikv_total', value: ( ) }, { key: 'unpacked_bytes_sent_tikv_cross_zone', value: ( ) }, { key: 'unpacked_bytes_received_tikv_cross_zone', value: ( ) }, { key: 'unpacked_bytes_sent_tiflash_total', value: ( ) }, { key: 'unpacked_bytes_received_tiflash_total', value: ( ) }, { key: 'unpacked_bytes_sent_tiflash_cross_zone', value: ( ) }, { key: 'unpacked_bytes_received_tiflash_cross_zone', value: ( ) }, { key: 'ia_remote_read_segment_size', value: ( ) } ] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabRuV2.tsx ================================================ import React, { ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { getValueFormat } from '@baurine/grafana-value-formats' import { RequestUnitV2Metrics, SlowqueryModel } from '@lib/client' import { CardTable, Pre } from '@lib/components' import { valueColumns } from '@lib/utils/tableColumns' const num = (v: number | undefined, unit: 'short' | 'bytes' = 'short') => v == null ? '' : getValueFormat(unit)(v, 2) const mapToCompactJson = (map: Record | undefined): string => { if (!map || Object.keys(map).length === 0) return '' return JSON.stringify(map) } /** * Metrics struct rows for the metrics table inside the RU V2 tab. */ function buildMetricsItems(data: SlowqueryModel) { const m: RequestUnitV2Metrics = data.ru_v2_metrics ?? {} return [ { key: 'ru_v2_metrics.total_ru', value: num(m.total_ru) }, { key: 'ru_v2_metrics.tidb_ru', value: num(m.tidb_ru) }, { key: 'ru_v2_metrics.tikv_ru', value: num(m.tikv_ru) }, { key: 'ru_v2_metrics.tiflash_ru', value: num(m.tiflash_ru) }, { key: 'ru_v2_metrics.txn_cnt', value: num(m.txn_cnt) }, { key: 'ru_v2_metrics.plan_cnt', value: num(m.plan_cnt) }, { key: 'ru_v2_metrics.plan_derive_stats_paths', value: num(m.plan_derive_stats_paths) }, { key: 'ru_v2_metrics.session_parser_total', value: num(m.session_parser_total) }, { key: 'ru_v2_metrics.executor_l1', value: num(m.executor_l1) }, { key: 'ru_v2_metrics.executor_l2', value: num(m.executor_l2) }, { key: 'ru_v2_metrics.executor_l3', value: num(m.executor_l3) }, { key: 'ru_v2_metrics.executor_l5_insert_rows', value: num(m.executor_l5_insert_rows) }, { key: 'ru_v2_metrics.result_chunk_cells', value: num(m.result_chunk_cells) }, { key: 'ru_v2_metrics.resource_manager_read_cnt', value: num(m.resource_manager_read_cnt) }, { key: 'ru_v2_metrics.resource_manager_write_cnt', value: num(m.resource_manager_write_cnt) }, { key: 'ru_v2_metrics.tikv_coprocessor_executor_iterations', value: num(m.tikv_coprocessor_executor_iterations) }, { key: 'ru_v2_metrics.tikv_coprocessor_response_bytes', value: num(m.tikv_coprocessor_response_bytes, 'bytes') }, { key: 'ru_v2_metrics.tikv_coprocessor_executor_work_total', value: mapToCompactJson(m.tikv_coprocessor_executor_work_total) }, { key: 'ru_v2_metrics.tikv_storage_processed_keys_get', value: num(m.tikv_storage_processed_keys_get) }, { key: 'ru_v2_metrics.tikv_storage_processed_keys_batch_get', value: num(m.tikv_storage_processed_keys_batch_get) }, { key: 'ru_v2_metrics.tikv_kv_engine_cache_miss', value: num(m.tikv_kv_engine_cache_miss) }, { key: 'ru_v2_metrics.tikv_raftstore_store_write_trigger_wb_bytes', value: num(m.tikv_raftstore_store_write_trigger_wb_bytes, 'bytes') } ] } function Section({ title, children }: { title: string; children: ReactNode }) { return (
{title}
{children}
) } export function RuV2TabContent({ data, schemaColumns }: { data: SlowqueryModel schemaColumns: string[] }) { const { t } = useTranslation() const schemaSet = new Set(schemaColumns) const v2 = data.ru_v2 const v2Detail = data.ru_v2_detail // Schema (available_fields) tells us which RU V2 fields the backend can // populate for this cluster tier. Premium only declares `ru_v2`; Starter / // Essential declare `ru_v2`, `ru_v2_detail`, `ru_v2_metrics`. Hide each // section if its field is not in the schema, regardless of the data value. const showV2 = schemaSet.has('ru_v2') const showDetail = schemaSet.has('ru_v2_detail') const showMetrics = schemaSet.has('ru_v2_metrics') const metricsItems = showMetrics ? buildMetricsItems(data) : [] // Columns: Name + Value only (drop the Description column). const nameValueColumns = valueColumns('slow_query.fields.').slice(0, 2) return (
{showV2 && (
{num(v2)}
)} {showDetail && (
            {v2Detail ?? ''}
          
)} {showMetrics && (
)}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabTime.tsx ================================================ import React from 'react' import { SlowqueryModel } from '@lib/client' import { Typography } from 'antd' import { TFunction } from 'react-i18next' export const tabTimeItems = (data: SlowqueryModel, t: TFunction) => { return [ { key: 'query_time2', keyDisplay: ( {t('slow_query.fields.query_time2')} ), value: data.query_time! * 10e8, indentLevel: 0 }, { key: 'parse_time', value: data.parse_time! * 10e8, indentLevel: 1 }, { key: 'compile_time', value: data.compile_time! * 10e8, indentLevel: 1 }, { key: 'rewrite_time', value: data.rewrite_time! * 10e8, indentLevel: 2 }, { key: 'preproc_subqueries_time', value: data.preproc_subqueries_time! * 10e8, indentLevel: 3 }, { key: 'optimize_time', value: data.optimize_time! * 10e8, indentLevel: 2 }, { key: 'cop_time', value: data.cop_time! * 10e8, indentLevel: 1 }, { key: 'wait_time', value: data.wait_time! * 10e8, indentLevel: 2 }, { key: 'process_time', value: data.process_time! * 10e8, indentLevel: 2 }, { key: 'local_latch_wait_time', value: data.local_latch_wait_time! * 10e8, indentLevel: 1 }, { key: 'lock_keys_time', value: data.lock_keys_time! * 10e8, indentLevel: 1 }, { key: 'resolve_lock_time', value: data.resolve_lock_time! * 10e8, indentLevel: 1 }, { key: 'wait_ts', value: data.wait_ts! * 10e8, indentLevel: 1 }, { key: 'get_commit_ts_time', value: data.get_commit_ts_time! * 10e8, indentLevel: 1 }, { key: 'prewrite_time', value: data.prewrite_time! * 10e8, indentLevel: 1 }, { key: 'commit_time', value: data.commit_time! * 10e8, indentLevel: 1 }, { key: 'backoff_time', value: data.backoff_time! * 10e8, indentLevel: 1 }, { key: 'commit_backoff_time', value: data.commit_backoff_time! * 10e8, indentLevel: 1 }, { key: 'exec_retry_time', value: data.exec_retry_time! * 10e8, indentLevel: 1 }, { key: 'write_sql_response_total', value: data.write_sql_response_total! * 10e8, indentLevel: 1 }, { key: 'wait_prewrite_binlog_time', value: data.wait_prewrite_binlog_time! * 10e8, indentLevel: 1 }, { key: 'time_queued_by_rc', value: data.time_queued_by_rc! * 10e8, indentLevel: 1 }, { key: 'ia_remote_read_segment_wait_time', value: data.ia_remote_read_segment_wait_time! * 10e8, indentLevel: 1 } ] } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabTxn.tsx ================================================ import React from 'react' import { getValueFormat } from '@baurine/grafana-value-formats' import { SlowqueryModel } from '@lib/client' import { ValueWithTooltip } from '@lib/components' export const tabTxnItems = (data: SlowqueryModel) => [ { key: 'txn_start_ts', value: data.txn_start_ts }, { key: 'write_keys', value: }, { key: 'write_size', value: getValueFormat('bytes')(data.write_size || 0, 1) }, { key: 'prewrite_region', value: }, { key: 'txn_retry', value: } ] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabs.tsx ================================================ import React, { useContext, useMemo } from 'react' import { useTranslation } from 'react-i18next' import ReactJson from 'react-json-view' import { SlowqueryModel } from '@lib/client' import { valueColumns, timeValueColumns } from '@lib/utils/tableColumns' import { CardTabs, CardTable } from '@lib/components' import { tabBasicItems } from './DetailTabBasic' import { tabTimeItems } from './DetailTabTime' import { tabCoprItems } from './DetailTabCopr' import { tabTxnItems } from './DetailTabTxn' import { RuV2TabContent } from './DetailTabRuV2' import { useSchemaColumns } from '../../utils/useSchemaColumns' import { SlowQueryContext } from '../../context' export default function DetailTabs({ data }: { data: SlowqueryModel }) { const ctx = useContext(SlowQueryContext) const showRuV2 = !!ctx?.cfg.showRuV2 const { t } = useTranslation() const { schemaColumns } = useSchemaColumns( ctx!.ds.slowQueryAvailableFieldsGet ) const tabs = useMemo(() => { const schemaSet = new Set(schemaColumns) // Premium tier only declares `ru_v2` in the schema; Starter / Essential // also declare `ru_v2_detail` and `ru_v2_metrics`. Use the presence of the // metrics struct as the tier signal: when absent it's Premium, so the // ru_v2 row stays inside the Basic tab; when present, RU V2 / Detail / // Metrics get their own dedicated tab. const isEssentialLike = schemaSet.has('ru_v2_metrics') const showRuV2InBasic = showRuV2 && schemaSet.has('ru_v2') && !isEssentialLike const showRuV2Tab = showRuV2 && isEssentialLike const tbs = [ { key: 'basic', title: t('slow_query.detail.tabs.basic'), content: () => { const items = tabBasicItems(data, { showRuV2: showRuV2InBasic }) const columns = valueColumns('slow_query.fields.') return ( ) } }, { key: 'time', title: t('slow_query.detail.tabs.time'), content: () => { const items = tabTimeItems(data, t) const columns = timeValueColumns('slow_query.fields.', items) return ( ) } }, { key: 'copr', title: t('slow_query.detail.tabs.copr'), content: () => { const columnsSet = new Set(schemaColumns) const items = tabCoprItems(data).filter((item) => columnsSet.has(item.key) ) const columns = valueColumns('slow_query.fields.') return ( ) } }, { key: 'txn', title: t('slow_query.detail.tabs.txn'), content: () => { const items = tabTxnItems(data) const columns = valueColumns('slow_query.fields.') return ( ) } } ] if (showRuV2Tab) { tbs.push({ key: 'ru_v2', title: t('slow_query.detail.tabs.ru_v2'), content: () => ( ) }) } if (data.warnings) { tbs.push({ key: 'warnings', title: t('slow_query.detail.tabs.warnings'), content: () => { return ( ) } }) } return tbs }, [schemaColumns, data, t, showRuV2]) return } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/index.tsx ================================================ import React, { useState, useContext, useMemo } from 'react' import { Space, Modal, Tabs, Typography } from 'antd' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' import { ArrowLeftOutlined } from '@ant-design/icons' import { useQuery } from '@tanstack/react-query' import formatSql from '@lib/utils/sqlFormatter' import { AnimatedSkeleton, BinaryPlanTable, PlanText, CopyLink, Descriptions, ErrorBar, Expand, Head, HighlightSQL, TextWithInfo } from '@lib/components' import { VisualPlanThumbnailView, VisualPlanView } from '@lib/components/VisualPlan' import { useVersionedLocalStorageState } from '@lib/utils/useVersionedLocalStorageState' import DetailTabs from './DetailTabs' import { SlowQueryContext } from '../../context' import { useSlowQueryDetailUrlState } from '../../utils/detail-url-state' import { telemetry } from '../../utils/telemetry' const SLOW_QUERY_DETAIL_EXPAND = 'slow_query.detail_expand' function useSlowQueryDetailData() { const ctx = useContext(SlowQueryContext) const { digest, connectionId, timestamp } = useSlowQueryDetailUrlState() const query = useQuery({ queryKey: ['slow_query', 'detail', digest, connectionId, timestamp], queryFn: () => { return ctx?.ds .slowQueryDetailGet(connectionId, digest, timestamp, { handleError: 'custom' }) .then((res) => res.data) } }) return query } function DetailPage() { const location = useLocation() const navigate = useNavigate() const { t } = useTranslation() const historyBack = (location.state ?? ({} as any)).historyBack ?? false const { data, isLoading, error } = useSlowQueryDetailData() const binaryPlanObj = useMemo(() => { const json = data?.binary_plan_json ?? data?.binary_plan if (json) { return JSON.parse(json) } return undefined }, [data?.binary_plan, data?.binary_plan_json]) const [detailExpand, setDetailExpand] = useVersionedLocalStorageState( SLOW_QUERY_DETAIL_EXPAND, { defaultValue: { prev_query: false, query: false, plan: false } } ) const togglePrevQuery = () => setDetailExpand((prev) => ({ ...prev, prev_query: !prev.prev_query })) const toggleQuery = () => setDetailExpand((prev) => ({ ...prev, query: !prev.query })) const [isVpVisible, setIsVpVisible] = useState(false) const toggleVisualPlan = (action: 'open' | 'close') => { telemetry.toggleVisualPlanModal(action) setIsVpVisible(!isVpVisible) } return (
historyBack ? navigate(-1) : navigate('/slow_query') } > {t('slow_query.detail.head.back')} } > {error && } {!!data && ( <> { // avoid page hang when sql is too long data.query!.length < 10000 && ( ) } } > } > {(() => { if (!!data.prev_stmt && data.prev_stmt.length !== 0) return ( { // avoid page hang when sql is too long data.prev_stmt!.length < 10000 && ( ) } } > } > ) })()} {(!!data.binary_plan_text || !!data.plan) && ( <> {t('slow_query.detail.plan.title')} telemetry.clickPlanTabs(key, data.digest!) } > {!!data.binary_plan_text && (
)} {binaryPlanObj && !binaryPlanObj.discardedDueToTooLong && ( toggleVisualPlan('close')} footer={null} destroyOnClose={true} bodyStyle={{ background: '#f5f5f5', height: window.innerHeight - 100 }} >
toggleVisualPlan('open')}>
)} )} )}
) } export default DetailPage ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/List/DownloadDBFileModal.tsx ================================================ import React, { useContext, useState } from 'react' import { useTranslation } from 'react-i18next' import { Button, Form, Modal, Radio, Select } from 'antd' import { useMemoizedFn } from 'ahooks' import { DatePicker } from '@lib/components' import { SlowQueryContext } from '../../context' import dayjs, { Dayjs } from 'dayjs' const hoursRange = [...Array(24).keys()] function DownloadDBFileModal({ visible, setVisible }: { visible: boolean setVisible: React.Dispatch> }) { const ctx = useContext(SlowQueryContext) const { t } = useTranslation() type RangeType = 'by_day' | 'by_hour' const [rangeTypeVal, setRangeTypeVal] = useState('by_day') const [downloading, setDownloading] = useState(false) const [dateVal, setDateVal] = useState(null) const [hourVal, setHourVal] = useState(0) const downloadDBFile = useMemoizedFn( async (dateVal: Dayjs, hourVal: number, rangeTypeVal: RangeType) => { // use last effective query options if (hourVal < 0 || hourVal > 23) { console.log(`Illegegal hour value: ${hourVal}`) hourVal = hourVal % 24 } try { setDownloading(true) const offset = rangeTypeVal === 'by_day' ? 86400 : 3600 const dateAlignedUnix = dateVal.unix() - (dateVal.unix() % 86400) const begin_time = dateAlignedUnix + (rangeTypeVal === 'by_hour' ? hourVal * 3600 : 0) const res = await ctx!.ds.slowQueryDownloadDBFile!( begin_time, begin_time + offset ) const { data, headers } = res const fileName = `${begin_time}.db` const blob = new Blob([data], { type: headers['content-type'] }) let dom = document.createElement('a') let url = window.URL.createObjectURL(blob) dom.href = url dom.download = decodeURI(fileName) dom.style.display = 'none' document.body.appendChild(dom) dom.click() dom.parentNode!.removeChild(dom) window.URL.revokeObjectURL(url) } finally { setDownloading(false) } } ) return ( setVisible(false)} title={t('slow_query.download_modal.title')} >
UTC setRangeTypeVal(e.target.value)} defaultValue={rangeTypeVal} > {t('slow_query.download_modal.download_by_day')} {t('slow_query.download_modal.download_by_hour')} { date && setDateVal(date) }} /> {LIMITS.map((item) => ( ))} {ctx!.cfg.showDigestFilter && ( )} {(fetchingDbs || fetchingRuGroups || fetchingAvailableColumns) && ( )} {ctx!.cfg.showDownloadSlowQueryDBFile && ( )} {ctx!.cfg.showInternalFilter && ( {t('slow_query.toolbar.show_internal')} )} {availableColumnsInTable.length > 0 && ( setShowFullSQL(e.target.checked)} data-e2e="slow_query_show_full_sql" > {t('slow_query.toolbar.select_columns.show_full_sql')} } /> )} {ctx!.cfg.enableExport && (
)} {!isDistro() && (ctx!.cfg.showHelp ?? true) && ( { window.open(t('slow_query.toolbar.help_url'), '_blank') }} /> )}
{slowQueryData?.length === 0 ? ( ) : (
{(slowQueryData?.length ?? 0) > 0 && (
{t('slow_query.overview.result_count', { n: slowQueryData?.length })}{' '} Download to CSV
)} setOrder({ col, type: desc ? 'desc' : 'asc' }) } errors={slowQueryError ? [slowQueryError] : []} visibleColumnKeys={visibleColumnKeys} onRowClicked={handleRowClick} clickedRowIndex={rowIdx} getKey={getKey} data-e2e="detail_tabs_slow_query" />
)} {ctx!.cfg.showDownloadSlowQueryDBFile && ( )}
) } export default List ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/index.ts ================================================ import List from './List' import Detail from './Detail' export { List, Detail } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/en.yaml ================================================ slow_query_v2: overview: head: title: Slow Queries detail: head: title: Slow Query Comparison slow_query: nav_title: Slow Queries fields: instance: '{{distro.tidb}} Instance' instance_tooltip: The {{distro.tidb}} address that handles the query connection_id: Connection ID connection_id_tooltip: Unique connection ID of the query sql: Query query: Query timestamp: Finish Time timestamp_tooltip: The time this query finished execution query_time: Latency query_time_tooltip: Execution time of the query memory_max: Max Memory memory_max_tooltip: Maximum memory usage of the query mem_arbitration: Mem Arbitration mem_arbitration_tooltip: Total wait time of memory arbitration of the query disk_max: Max Disk disk_max_tooltip: Maximum disk usage of the query digest: Query Template ID digest_tooltip: a.k.a. Query digest is_internal: Is Internal? is_internal_tooltip: Whether this is an internal query is_success: Is Success? is_success_tooltip: Whether query is executed successfully is_prepared: Is Prepared? is_prepared_tooltip: Is Generated by the prepare statement is_plan_from_cache: Is Plan from Cache? is_plan_from_binding: Is Plan from Binding? result: Result result_tooltip: Whether query is executed successfully index_names: Index Names index_names_tooltip: The name of the used index stats: Used Statistics backoff_types: Backoff Types user: Execution User user_tooltip: The user that executes the query host: Client Address host_tooltip: The address of the client that sends the query db: Execution Database db_tooltip: The database used to execute the query query_time2: Query Time query_time2_tooltip: The elapsed wall time when execution the query parse_time: Parse Time parse_time_tooltip: Time consumed when parsing the query compile_time: Generate Plan Time rewrite_time: Rewrite Plan Time preproc_subqueries_time: Preprocess Sub-Query Time preproc_subqueries_time_tooltip: Time consumed when pre-processing the subquery during the rewrite plan phase optimize_time: Optimize Plan Time wait_ts: Get Start Ts Time wait_ts_tooltip: Time consumed when getting a start timestamp when transaction begins cop_time: Coprocessor Executor Time cop_time_tooltip: 'The elapsed wall time when {{distro.tidb}} Coprocessor executor waiting all Coprocessor requests to finish (note: when there are JOIN in SQL statement, multiple {{distro.tidb}} Coprocessor executors may be running in parallel, which may cause this time not being a wall time)' wait_time: Coprocessor Wait Time wait_time_tooltip: 'The total time of Coprocessor request is prepared and wait to execute in {{distro.tikv}}, which may happen when retrieving a snapshot though Raft concensus protocol (note: {{distro.tikv}} waits requests in parallel so that this is not a wall time)' process_time: Coprocessor Process Time process_time_tooltip: 'The total time of Coprocessor request being executed in {{distro.tikv}} (note: {{distro.tikv}} executes requests in parallel so that this is not a wall time)' backoff_time: Execution Backoff Time backoff_time_tooltip: 'The total backoff waiting time before retry when a query encounters errors (note: there may be multiple backoffs in parallel so that this may not be a wall time)' lock_keys_time: Lock Keys Time lock_keys_time_tooltip: Time consumed when locking keys in pessimistic transaction get_commit_ts_time: Get Commit Ts Time get_commit_ts_time_tooltip: Time consumed when getting a commit timestamp for 2PC commit phase when transaction commits local_latch_wait_time: Local Latch Wait Time local_latch_wait_time_tooltip: Time consumed when {{distro.tidb}} waits for the lock in the current {{distro.tidb}} instance before 2PC commit phase when transaction commits resolve_lock_time: Resolve Lock Time resolve_lock_time_tooltip: Time consumed when {{distro.tidb}} resolves locks from other transactions in 2PC prewrite phase when transaction commits prewrite_time: Prewrite Time prewrite_time_tooltip: Time consumed in 2PC prewrite phase when transaction commits wait_prewrite_binlog_time: Wait Binlog Prewrite Time wait_prewrite_binlog_time_tooltip: Time consumed when waiting Binlog prewrite to finish commit_time: Commit Time commit_time_tooltip: Time consumed in 2PC commit phase when transaction commits commit_backoff_time: Commit Backoff Time commit_backoff_time_tooltip: 'The total backoff waiting time when 2PC commit encounters errors (note: there may be multiple backoffs in parallel so that this may not be a wall time)' write_sql_response_total: Send response Time write_sql_response_total_tooltip: Time consumed when sending response to the SQL client exec_retry_time: Retried execution Time exec_retry_time_tooltip: Wall time consumed when SQL statement is retried and executed again, except for the last exection request_count: Request Count process_keys: Process Keys total_keys: Total Keys cop_proc_addr: Copr Address (Process) cop_proc_addr_tooltip: The address of the {{distro.tikv}} that takes most time process the Coprocessor request cop_wait_addr: Copr Address (Wait) cop_wait_addr_tooltip: The address of the {{distro.tikv}} that takes most time wait the Coprocessor request txn_start_ts: Start Timestamp txn_start_ts_tooltip: Transaction start timestamp, a.k.a. Transaction ID write_keys: Write Keys write_size: Write Size prewrite_region: Prewrite Regions txn_retry: Transaction Retries prev_stmt: Previous Query plan: Execution Plan cop_proc_avg: Mean Cop Proc # ? cop_wait_avg: Mean Cop Wait # ? rocksdb_delete_skipped_count: RocksDB Skipped Deletions rocksdb_delete_skipped_count_tooltip: Total number of deleted (a.k.a. tombstone) key versions that are skipped during iteration (RocksDB delete_skipped_count) rocksdb_key_skipped_count: RocksDB Skipped Keys rocksdb_key_skipped_count_tooltip: Total number of keys skipped during iteration (RocksDB key_skipped_count) rocksdb_block_cache_hit_count: RocksDB Block Cache Hits rocksdb_block_cache_hit_count_tooltip: Total number of hits from the block cache (RocksDB block_cache_hit_count) rocksdb_block_read_count: RocksDB Block Reads rocksdb_block_read_count_tooltip: Total number of blocks RocksDB read from file (RocksDB block_read_count) rocksdb_block_read_byte: RocksDB Read Size rocksdb_block_read_byte_tooltip: Total number of bytes RocksDB read from file (RocksDB block_read_byte) ru: RU ru_tooltip: request units ru_v2: RU V2 ru_v2_tooltip: Total RU V2 consumed by this query ru_v2_detail: RU V2 Detail ru_v2_detail_tooltip: Per-component RU V2 breakdown (raw) ru_v2_metrics_label: RU V2 Metrics ru_v2_metrics: total_ru: Total RU tidb_ru: TiDB RU tikv_ru: TiKV RU tiflash_ru: TiFlash RU txn_cnt: Transaction Count plan_cnt: Plan Count plan_derive_stats_paths: Plan Derive Stats Paths session_parser_total: Session Parser Total executor_l1: Executor L1 executor_l2: Executor L2 executor_l3: Executor L3 executor_l5_insert_rows: Executor L5 Insert Rows result_chunk_cells: Result Chunk Cells resource_manager_read_cnt: Resource Manager Read Count resource_manager_write_cnt: Resource Manager Write Count tikv_coprocessor_executor_iterations: TiKV Coprocessor Executor Iterations tikv_coprocessor_response_bytes: TiKV Coprocessor Response Bytes tikv_coprocessor_executor_work_total: TiKV Coprocessor Executor Work Total tikv_storage_processed_keys_get: TiKV Storage Processed Keys (Get) tikv_storage_processed_keys_batch_get: TiKV Storage Processed Keys (Batch Get) tikv_kv_engine_cache_miss: TiKV KV Engine Cache Miss tikv_raftstore_store_write_trigger_wb_bytes: TiKV Raftstore Write Trigger WB Bytes resource_group: Resource Group resource_group_tooltip: The resource group that the query belongs to time_queued_by_rc: The total time queued by RC time_queued_by_rc_tooltip: 'The total wait time spent in the resource queue (note: {{distro.tikv}} executes requests in parallel so that this is not a wall time)' # Network fields unpacked_bytes_sent_tikv_total: Bytes Sent to TiKV unpacked_bytes_sent_tikv_total_tooltip: The number of bytes sent to TiKV unpacked_bytes_received_tikv_total: Bytes Received from TiKV unpacked_bytes_received_tikv_total_tooltip: The number of bytes received from TiKV unpacked_bytes_sent_tikv_cross_zone: Cross-Zone Bytes Sent to TiKV unpacked_bytes_sent_tikv_cross_zone_tooltip: The number of bytes sent to TiKV across zones unpacked_bytes_received_tikv_cross_zone: Cross-Zone Bytes Received from TiKV unpacked_bytes_received_tikv_cross_zone_tooltip: The number of bytes received from TiKV across zones unpacked_bytes_sent_tiflash_total: Bytes Sent to TiFlash unpacked_bytes_sent_tiflash_total_tooltip: The number of bytes sent to TiFlash unpacked_bytes_received_tiflash_total: Bytes Received from TiFlash unpacked_bytes_received_tiflash_total_tooltip: The number of bytes received from TiFlash unpacked_bytes_sent_tiflash_cross_zone: Cross-Zone Bytes Sent to TiFlash unpacked_bytes_sent_tiflash_cross_zone_tooltip: The number of bytes sent to TiFlash across zones unpacked_bytes_received_tiflash_cross_zone: Cross-Zone Bytes Received from TiFlash unpacked_bytes_received_tiflash_cross_zone_tooltip: The number of bytes received from TiFlash across zones ia_remote_read_segment_size: IA Remote Read Segment Size ia_remote_read_segment_size_tooltip: Total number of bytes read from IA remote segments ia_remote_read_segment_wait_time: IA Remote Read Segment Wait Time ia_remote_read_segment_wait_time_tooltip: The wait time spent reading IA remote segments common: status: success: Success error: Failed overview: empty_result: No matched slow queries result_count: '{{ n }} results.' slow_load_info: On-the-fly update is disabled due to slow data loading. You can initiate query manually by clicking the "Query" button. detail: head: title: Slow Query Detail back: List sql: Query previous_sql: Previous Query plan: title: Execution Plan text: Text table: Table visual: Visual modal_title: Visual Plan tabs: basic: Basic time: Time copr: Coprocessor txn: Transaction ru_v2: RU V2 warnings: Warnings toolbar: schemas: placeholder: All Databases selected: '{{ n }} Databases' columnTitle: Execution Database Name resource_groups: placeholder: All Resource Groups selected: '{{ n }} Resource Groups' columnTitle: Resource Group Name select_columns: show_full_sql: Show Full Query Text show_internal: Show Internal refresh: Refresh digest: placeholder: Digest keyword: placeholder: Filter keyword query: Query export: Export exporting: Exporting help: Help help_url: https://docs.pingcap.com/tidb/dev/dashboard-slow-query download_db: Download DB download_modal: download: Download title: 'Download Slow Query db File' download_by_day: Download By Day download_by_hour: Download By Hour ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/zh.yaml ================================================ slow_query_v2: overview: head: title: 慢查询 detail: head: title: 慢查询对比 slow_query: nav_title: 慢查询 fields: instance: '{{distro.tidb}} 实例' instance_tooltip: 处理该 SQL 查询的 {{distro.tidb}} 实例地址 connection_id: 连接号 connection_id_tooltip: SQL 查询客户端连接 ID sql: SQL query: SQL sql_tooltip: SQL timestamp: 结束运行时间 timestamp_tooltip: 该 SQL 查询结束运行时的时间 query_time: 总执行时间 query_time_tooltip: 该 SQL 查询总的执行时间 memory_max: 最大内存 memory_max_tooltip: 该 SQL 查询执行时占用的最大内存空间 mem_arbitration: 等待内存资源耗时 mem_arbitration_tooltip: 该 SQL 等待内存资源的总耗时 disk_max: 最大磁盘空间 disk_max_tooltip: 该 SQL 查询执行时占用的最大磁盘空间 digest: SQL 模板 ID digest_tooltip: SQL 模板的唯一标识(SQL 指纹) is_internal: 是否为内部 SQL 查询 is_success: 是否执行成功 is_success_tooltip: SQL 查询是否执行成功 is_prepared: 是否由 prepare 语句生成 is_plan_from_cache: 查询计划是否来自缓存 is_plan_from_binding: 查询计划是否来自绑定 result: 执行结果 result_tooltip: SQL 查询是否执行成功 index_names: 索引名 index_names_tooltip: SQL 查询执行时使用的索引名称 stats: 使用的统计信息 backoff_types: 重试类型 user: 执行用户名 user_tooltip: 执行该 SQL 查询的用户名,可能存在多个执行用户,仅显示其中某一个 host: 客户端地址 host_tooltip: 发送 SQL 查询的客户端地址 db: 执行数据库 db_tooltip: 执行该 SQL 查询时使用的数据库名称 query_time2: SQL 执行时间 query_time2_tooltip: 执行 SQL 耗费的自然时间 parse_time: 解析耗时 parse_time_tooltip: 解析该 SQL 查询的耗时 compile_time: 生成执行计划耗时 compile_time_tooltip: 生成该 SQL 的执行计划的耗时 rewrite_time: 重写执行计划耗时 rewrite_time_tooltip: 重写执行计划的耗时,例如常量折叠等 preproc_subqueries_time: 子查询预处理耗时 optimize_time: 优化执行计划耗时 optimize_time_tooltip: 优化器寻找执行计划的耗时,包括规则优化和物理优化的耗时 wait_ts: 取事务 Start Ts 耗时 wait_ts_tooltip: 从 {{distro.pd}} 取事务开始时间戳步骤的耗时 cop_time: Coprocessor 执行耗时 cop_time_tooltip: '{{distro.tidb}} Coprocessor 算子等待所有任务在 {{distro.tikv}} 上并行执行完毕耗费的自然时间(注:当 SQL 语句中包含 JOIN 时,多个 {{distro.tidb}} Coprocessor 算子可能会并行执行,此时不再等同于自然时间)' wait_time: Coprocessor 累计等待耗时 wait_time_tooltip: '{{distro.tikv}} 准备并等待 Coprocessor 任务执行的累计时间,等待过程中包括通过 Raft 一致性协议取快照等(注:{{distro.tikv}} 会并行等待任务,因此该时间不是自然流逝时间)' process_time: Coprocessor 累计执行耗时 process_time_tooltip: '{{distro.tikv}} 执行 Coprocessor 任务的累计处理时间(注:{{distro.tikv}} 会并行处理任务,因此该时间不是自然流逝时间)' lock_keys_time: 上锁耗时 lock_keys_time_tooltip: 悲观事务中对相关行数据进行上锁的耗时 backoff_time: 执行阶段累计 Backoff 耗时 backoff_time_tooltip: 在执行失败时,Backoff 机制等待一段时间再重试时的 Backoff 累计耗时(注:可能同时存在多个 Backoff,因此该时间可能不是自然流逝时间) get_commit_ts_time: 取事务 Commit Ts 耗时 get_commit_ts_time_tooltip: 从 {{distro.pd}} 取提交时间戳(事务号)步骤的耗时 local_latch_wait_time: '{{distro.tidb}} 本地等锁耗时' local_latch_wait_time_tooltip: 事务在 {{distro.tidb}} 本地与其他事务产生了锁冲突并等待的耗时 resolve_lock_time: 解锁耗时 resolve_lock_time_tooltip: 事务在提交过程中与其他事务产生了锁冲突并处理锁冲突的耗时 prewrite_time: Prewrite 阶段耗时 prewrite_time_tooltip: 事务两阶段提交中第一阶段(prewrite 阶段)的耗时 wait_prewrite_binlog_time: Binlog Prewrite 等待耗时 wait_prewrite_binlog_time_tooltip: 等待 Binlog Prewrite 完成的耗时 commit_time: Commit 阶段耗时 commit_time_tooltip: 事务两阶段提交中第二阶段(commit 阶段)的耗时 commit_backoff_time: Commit 阶段累计 Backoff 耗时 commit_backoff_time_tooltip: 事务递交失败时,Backoff 机制等待一段时间再重试时的 Backoff 累计耗时(注:可能同时存在多个 Backoff,因此该时间可能不是自然流逝时间) write_sql_response_total: 发送结果耗时 write_sql_response_total_tooltip: 发送 SQL 语句执行结果给客户端的耗时 exec_retry_time: 前序执行耗时 exec_retry_time_tooltip: 由于锁冲突或错误,计划可能会执行失败并重试执行多次,该时间是不包含最后一次执行的前序执行自然时间(注:执行计划中的时间不含该前序时间) request_count: Coprocessor 请求数 process_keys: 可见版本数 total_keys: 遇到版本数 total_keys_tooltip: 含已删除或覆盖但未 GC 的版本 cop_proc_addr: 最长处理时间实例 cop_proc_addr_tooltip: 耗费最长时间处理 Coprocessor 请求的 {{distro.tikv}} 实例地址 cop_wait_addr: 最长等待时间实例 cop_wait_addr_tooltip: 耗费最长时间等待 Coprocessor 请求的 {{distro.tikv}} 实例地址 txn_start_ts: 事务号 txn_start_ts_tooltip: 事务开始的时间戳,也即是事务号 write_keys: 写入 Key 个数 write_size: 写入数据量 prewrite_region: Prewrite 涉及 Regions 个数 txn_retry: 事务重试次数 prev_stmt: 前一条 SQL 查询 plan: 执行计划 cop_proc_avg: 平均处理 # ? cop_wait_avg: 平均等待 # ? rocksdb_delete_skipped_count: RocksDB 已删除 Key 扫描数 rocksdb_delete_skipped_count_tooltip: RocksDB 扫数据时遇到的已删除 (tombstone) Key 数量 (delete_skipped_count) rocksdb_key_skipped_count: RocksDB Key 扫描数 rocksdb_key_skipped_count_tooltip: RocksDB 扫数据时所有遇到的 Key 数量 (key_skipped_count) rocksdb_block_cache_hit_count: RocksDB 缓存读次数 rocksdb_block_cache_hit_count_tooltip: RocksDB 从 Block Cache 缓存中读数据的次数 (block_cache_hit_count) rocksdb_block_read_count: RocksDB 文件系统读次数 rocksdb_block_read_count_tooltip: RocksDB 从文件系统中读数据的次数 (block_read_count) rocksdb_block_read_byte: RocksDB 文件系统读数据量 rocksdb_block_read_byte_tooltip: RocksDB 从文件系统中读数据的数据量 (block_read_byte) ru: RU ru_tooltip: 资源单位(RU) ru_v2: RU V2 ru_v2_tooltip: 本次查询消耗的 RU V2 总量 ru_v2_detail: RU V2 明细 ru_v2_detail_tooltip: 各组件 RU V2 拆分(原始数据) ru_v2_metrics_label: RU V2 Metrics ru_v2_metrics: total_ru: Total RU tidb_ru: TiDB RU tikv_ru: TiKV RU tiflash_ru: TiFlash RU txn_cnt: 事务数 plan_cnt: Plan 数 plan_derive_stats_paths: Plan Derive Stats Paths session_parser_total: Session Parser Total executor_l1: Executor L1 executor_l2: Executor L2 executor_l3: Executor L3 executor_l5_insert_rows: Executor L5 插入行数 result_chunk_cells: Result Chunk Cells resource_manager_read_cnt: Resource Manager 读次数 resource_manager_write_cnt: Resource Manager 写次数 tikv_coprocessor_executor_iterations: TiKV Coprocessor Executor 迭代数 tikv_coprocessor_response_bytes: TiKV Coprocessor 响应字节 tikv_coprocessor_executor_work_total: TiKV Coprocessor Executor 工作总量 tikv_storage_processed_keys_get: TiKV Storage Processed Keys (Get) tikv_storage_processed_keys_batch_get: TiKV Storage Processed Keys (Batch Get) tikv_kv_engine_cache_miss: TiKV KV Engine Cache Miss tikv_raftstore_store_write_trigger_wb_bytes: TiKV Raftstore Write Trigger WB Bytes resource_group: 资源组 resource_group_tooltip: SQL 语句所属的资源组 time_queued_by_rc: RC 等待累积耗时 time_queued_by_rc_tooltip: SQL 语句在资源组队列中等待的累积时间(注:{{distro.tikv}} 会并行等待任务,因此该时间不是自然流逝时间) # Network fields unpacked_bytes_sent_tikv_total: 发送至 TiKV 的字节数 unpacked_bytes_sent_tikv_total_tooltip: 发送至 TiKV 的字节数 unpacked_bytes_received_tikv_total: 从 TiKV 接收的字节数 unpacked_bytes_received_tikv_total_tooltip: 从 TiKV 接收的字节数 unpacked_bytes_sent_tikv_cross_zone: 跨可用区发送至 TiKV 的字节数 unpacked_bytes_sent_tikv_cross_zone_tooltip: 跨可用区发送至 TiKV 的字节数 unpacked_bytes_received_tikv_cross_zone: 跨可用区从 TiKV 接收的字节数 unpacked_bytes_received_tikv_cross_zone_tooltip: 跨可用区从 TiKV 接收的字节数 unpacked_bytes_sent_tiflash_total: 发送至 TiFlash 的字节数 unpacked_bytes_sent_tiflash_total_tooltip: 发送至 TiFlash 的字节数 unpacked_bytes_received_tiflash_total: 从 TiFlash 接收的字节数 unpacked_bytes_received_tiflash_total_tooltip: 从 TiFlash 接收的字节数 unpacked_bytes_sent_tiflash_cross_zone: 跨可用区发送至 TiFlash 的字节数 unpacked_bytes_sent_tiflash_cross_zone_tooltip: 跨可用区发送至 TiFlash 的字节数 unpacked_bytes_received_tiflash_cross_zone: 跨可用区从 TiFlash 接收的字节数 unpacked_bytes_received_tiflash_cross_zone_tooltip: 跨可用区从 TiFlash 接收的字节数 # IA remote read ia_remote_read_segment_size: IA 远程读取数据量 ia_remote_read_segment_size_tooltip: 从 IA 远程存储段读取的数据总量 ia_remote_read_segment_wait_time: IA 远程读分段等待时间 ia_remote_read_segment_wait_time_tooltip: IA 远程读分段等待时间 common: status: success: 成功 error: 失败 overview: empty_result: 没有符合条件的慢查询 result_count: '{{ n }} 条结果。' slow_load_info: 数据加载耗时较长,已禁用即时更新。修改查询条件后,您可以手工点击”查询“按钮来发起查询。 detail: head: title: 慢查询详情 back: 列表 sql: SQL 查询 previous_sql: 上一条 SQL 查询 plan: title: 执行计划 text: 文本 table: 表格 visual: 图形 modal_title: 执行计划可视化 tabs: basic: 基本信息 time: 执行时间 copr: Coprocessor 读取 txn: 事务 ru_v2: RU V2 warnings: 警告 toolbar: schemas: placeholder: 所有数据库 selected: '{{ n }} 数据库' columnTitle: 执行数据库名 resource_groups: placeholder: 所有资源组 selected: '{{ n }} 资源组' columnTitle: 资源组名 select_columns: show_full_sql: 显示完整 SQL 文本 show_internal: 显示内部查询 refresh: 刷新 digest: placeholder: Digest keyword: placeholder: 关键字过滤 query: 查询 export: 导出 exporting: 正在导出 help: 帮助 help_url: https://docs.pingcap.com/zh/tidb/dev/dashboard-slow-query download_db: 下载 DB download_modal: download: 下载 title: '下载慢查询 db 文件' download_by_day: 按天下载 download_by_hour: 按小时下载 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/detail-url-state.ts ================================================ import useUrlState from '@ahooksjs/use-url-state' type DetailUrlState = Partial< Record<'digest' | 'connection_id' | 'timestamp', string> > export function useSlowQueryDetailUrlState() { const [queryParams, _] = useUrlState() // digest const digest = queryParams.digest ?? '' // connect_id const connectionId = queryParams.connection_id ?? '' // timestamp, timestamp is float type number const timestamp = parseFloat(queryParams.timestamp) return { digest, connectionId, timestamp } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/helpers.ts ================================================ import { IColumnKeys } from '@lib/components' export const LIMITS = [100, 200, 500, 1000] export const DEF_SLOW_QUERY_COLUMN_KEYS: IColumnKeys = { query: true, timestamp: true, query_time: true, memory_max: true } export const SLOW_QUERY_VISIBLE_COLUMN_KEYS = 'slow_query.visible_column_keys' export const SLOW_QUERY_SHOW_FULL_SQL = 'slow_query.show_full_sql' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/list-url-state.ts ================================================ import { useCallback, useMemo } from 'react' import useUrlState from '@ahooksjs/use-url-state' import { DEFAULT_TIME_RANGE, TimeRange, toURLTimeRange, urlToTimeRange } from '@lib/components/TimeRangeSelector' type ListUrlState = Partial< Record< | 'from' | 'to' | 'dbs' | 'digest' | 'ru_groups' | 'term' | 'limit' | 'fields' | 'full_sql' | 'order' | 'row' | 'show_internal', string > > type OrderOpt = { col: string type: 'asc' | 'desc' } export function useSlowQueryListUrlState() { const [queryParams, setQueryParams] = useUrlState() // from & to const timeRange = useMemo(() => { const { from, to } = queryParams if (from && to) { return urlToTimeRange({ from, to }) } return DEFAULT_TIME_RANGE }, [queryParams.from, queryParams.to]) const setTimeRange = useCallback( (newTimeRange: TimeRange) => { setQueryParams({ ...toURLTimeRange(newTimeRange) }) }, [setQueryParams] ) // dbs const dbs = useMemo(() => { const dbs = queryParams.dbs return dbs ? dbs.split(',') : [] }, [queryParams.dbs]) const setDbs = useCallback( (v: string[]) => { setQueryParams({ dbs: v.join(',') }) }, [setQueryParams] ) // digest const digest = queryParams.digest ?? '' const setDigest = useCallback( (v: string) => { setQueryParams({ digest: v }) }, [setQueryParams] ) // ru_groups const ruGroups = useMemo(() => { const ruGroups = queryParams.ru_groups return ruGroups ? ruGroups.split(',') : [] }, [queryParams.ru_groups]) const setRuGroups = useCallback( (v: string[]) => { setQueryParams({ ru_groups: v.join(',') }) }, [setQueryParams] ) // term const term = queryParams.term ?? '' const setTerm = useCallback( (v: string) => { setQueryParams({ term: v }) }, [setQueryParams] ) // limit const limit = parseInt(queryParams.limit ?? '100') const setLimit = useCallback( (v: number) => { setQueryParams({ limit: v.toString() }) }, [setQueryParams] ) // order const order = useMemo(() => { const _order = queryParams.order ?? '-timestamp' let type: 'asc' | 'desc' = 'asc' let col = _order if (col.startsWith('-')) { col = col.slice(1) type = 'desc' } return { col, type } }, [queryParams.order]) const setOrder = useCallback( (v: OrderOpt) => { setQueryParams({ order: v.type === 'asc' ? v.col : '-' + v.col }) }, [setQueryParams] ) const resetOrder = useCallback(() => { setQueryParams({ order: undefined }) }, [setQueryParams]) // row const rowIdx = useMemo(() => { const r = parseInt(queryParams.row) if (r >= 0) return r return -1 }, [queryParams.row]) const setRowIdx = useCallback( (v: number) => { setQueryParams({ row: v + '' }) }, [setQueryParams] ) // show internal const showInternal = useMemo(() => { const showInternalStr = String( queryParams.show_internal ?? '' ).toLowerCase() if (showInternalStr === '1' || showInternalStr === 'true') { return true } return false }, [queryParams.show_internal]) const setShowInternal = useCallback( (show: boolean) => { setQueryParams({ show_internal: show }) }, [setQueryParams] ) return { queryParams, setQueryParams, timeRange, setTimeRange, dbs, setDbs, digest, setDigest, ruGroups, setRuGroups, term, setTerm, limit, setLimit, order, setOrder, resetOrder, rowIdx, setRowIdx, showInternal, setShowInternal } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/tableColumns.tsx ================================================ import { Badge } from 'antd' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import React from 'react' import { useTranslation } from 'react-i18next' import { SlowqueryModel } from '@lib/client' import { TableColumnFactory } from '@lib/utils/tableColumnFactory' ////////////////////////////////////////// function ResultStatusBadge({ status }: { status: 'success' | 'error' }) { const { t } = useTranslation() return ( ) } ////////////////////////////////////////// const TRANS_KEY_PREFIX = 'slow_query.fields' export const derivedFields = { cop_proc_avg: [ { tooltipPrefix: 'mean', fieldName: 'cop_proc_avg' }, { tooltipPrefix: 'max', fieldName: 'cop_proc_max' }, { tooltipPrefix: 'p90', fieldName: 'cop_proc_p90' } ], cop_wait_avg: [ { tooltipPrefix: 'mean', fieldName: 'cop_wait_avg' }, { tooltipPrefix: 'max', fieldName: 'cop_wait_max' }, { tooltipPrefix: 'p90', fieldName: 'cop_wait_p90' } ] } ////////////////////////////////////////// export function slowQueryColumns( rows: SlowqueryModel[], tableSchemaColumns: string[], showFullSQL?: boolean ): IColumn[] { const tcf = new TableColumnFactory(TRANS_KEY_PREFIX, tableSchemaColumns) return tcf.columns([ tcf.sqlText('query', showFullSQL, rows), tcf.textWithTooltip('digest', rows), tcf.textWithTooltip('instance', rows), tcf.textWithTooltip('db', rows), tcf.textWithTooltip('connection_id', rows), tcf.timestamp('timestamp', rows), tcf.bar.single('query_time', 's', rows), tcf.bar.single('parse_time', 's', rows), tcf.bar.single('compile_time', 's', rows), tcf.bar.single('process_time', 's', rows), tcf.bar.single('memory_max', 'bytes', rows), tcf.bar.single('mem_arbitration', 's', rows), tcf.bar.single('disk_max', 'bytes', rows), tcf.textWithTooltip('txn_start_ts', rows), // success columnn tcf.textWithTooltip('success', rows).patchConfig({ name: 'result', minWidth: 50, maxWidth: 100, onRender: (rec) => ( ) }), // basic // is_internal column tcf.textWithTooltip('is_internal', rows).patchConfig({ minWidth: 50, maxWidth: 100, onRender: (rec) => (rec.is_internal === 1 ? 'Yes' : 'No') }), tcf.textWithTooltip('index_names', rows), tcf.textWithTooltip('stats', rows), tcf.textWithTooltip('backoff_types', rows), // connection tcf.textWithTooltip('user', rows), tcf.textWithTooltip('host', rows), // time tcf.bar.single('wait_ts', 's', rows), tcf.bar.single('wait_time', 's', rows), tcf.bar.single('backoff_time', 's', rows), tcf.bar.single('get_commit_ts_time', 's', rows), tcf.bar.single('local_latch_wait_time', 's', rows), tcf.bar.single('prewrite_time', 's', rows), tcf.bar.single('commit_time', 's', rows), tcf.bar.single('commit_backoff_time', 's', rows), tcf.bar.single('resolve_lock_time', 's', rows), // cop tcf.bar.multiple({ sources: derivedFields.cop_proc_avg }, 's', rows), tcf.bar.multiple({ sources: derivedFields.cop_wait_avg }, 's', rows), tcf.bar.single('request_count', 'short', rows), tcf.bar.single('process_keys', 'short', rows), tcf.bar.single('total_keys', 'short', rows), tcf.textWithTooltip('cop_proc_addr', rows), tcf.textWithTooltip('cop_wait_addr', rows), // transaction tcf.bar.single('write_keys', 'short', rows), tcf.bar.single('write_size', 'bytes', rows), tcf.bar.single('prewrite_region', 'short', rows), tcf.bar.single('txn_retry', 'short', rows), // rocksdb tcf.bar.single('rocksdb_delete_skipped_count', 'short', rows).patchConfig({ minWidth: 220, maxWidth: 250 }), tcf.bar.single('rocksdb_key_skipped_count', 'short', rows).patchConfig({ minWidth: 220, maxWidth: 250 }), tcf.bar.single('rocksdb_block_cache_hit_count', 'short', rows).patchConfig({ minWidth: 220, maxWidth: 250 }), tcf.bar.single('rocksdb_block_read_count', 'short', rows).patchConfig({ minWidth: 220, maxWidth: 250 }), tcf.bar.single('rocksdb_block_read_byte', 'bytes', rows).patchConfig({ minWidth: 220, maxWidth: 250 }), // resource control tcf.bar.single('ru', 'none', rows), tcf.bar.single('ru_v2', 'none', rows), tcf.textWithTooltip('ru_v2_detail', rows), tcf.textWithTooltip('resource_group', rows), tcf.bar.single('time_queued_by_rc', 's', rows), // Network fields tcf.bar.single('unpacked_bytes_sent_tikv_total', 'bytes', rows), tcf.bar.single('unpacked_bytes_received_tikv_total', 'bytes', rows), tcf.bar.single('unpacked_bytes_sent_tikv_cross_zone', 'bytes', rows), tcf.bar.single('unpacked_bytes_received_tikv_cross_zone', 'bytes', rows), tcf.bar.single('unpacked_bytes_sent_tiflash_total', 'bytes', rows), tcf.bar.single('unpacked_bytes_received_tiflash_total', 'bytes', rows), tcf.bar.single('unpacked_bytes_sent_tiflash_cross_zone', 'bytes', rows), tcf.bar.single('unpacked_bytes_received_tiflash_cross_zone', 'bytes', rows), tcf.bar.single('ia_remote_read_segment_size', 'bytes', rows), tcf.bar.single('ia_remote_read_segment_wait_time', 's', rows) ]) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/telemetry.ts ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import { mixpanel } from '@lib/utils/telemetry' export const telemetry = { clickTopSlowQueryTab() { mixpanel.track('Slowquery: Click TopSlowquery Tab') }, clickTableRow() { mixpanel.track('Slowquery: Click Table Row') }, clickQueryButton() { mixpanel.track('Slowquery: Click Query Button') }, clickPlanTabs(tab: string, queryDigest: string) { mixpanel.track('Slowquery: Plan Tab Clicked', { tab, queryDigest }) }, toggleVisualPlanModal(action: 'open' | 'close') { mixpanel.track('Slowquery: Visual Plan Modal Toggled', { action }) }, toggleExpandBtnOnNode(nodeName: string) { mixpanel.track('Slowquery: Node Button Toggled', { nodeName }) }, clickNode(nodeName: string) { mixpanel.track('Slowquery: Node Clicked', { nodeName }) }, clickTabOnNodeDetail(tab: string) { mixpanel.track('Slowquery: Detail Tab on Node Clicked', { tab }) } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/useSchemaColumns.ts ================================================ import { useState, useEffect } from 'react' import { useClientRequest } from '@lib/utils/useClientRequest' import { ISlowQueryDataSource } from '../context' export const useSchemaColumns = ( availableFieldsFetcher: ISlowQueryDataSource['slowQueryAvailableFieldsGet'] ) => { const [schemaColumns, setSchemaColumns] = useState([]) const { data, isLoading } = useClientRequest(availableFieldsFetcher) useEffect(() => { if (!data) { return } setSchemaColumns(data.map((d) => d.toLowerCase())) }, [data]) return { schemaColumns, isLoading } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/useSlowQueryTableController.ts ================================================ import { useMemo, useState } from 'react' import { useMemoizedFn, useSessionStorageState } from 'ahooks' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { TimeRange, IColumnKeys, DEFAULT_TIME_RANGE, toTimeRangeValue } from '@lib/components' import useOrderState, { IOrderOptions } from '@lib/utils/useOrderState' import { getSelectedFields } from '@lib/utils/tableColumnFactory' import { CacheMgr } from '@lib/utils/useCache' import useCacheItemIndex from '@lib/utils/useCacheItemIndex' import { derivedFields, slowQueryColumns } from './tableColumns' import { useSchemaColumns } from './useSchemaColumns' import { useChange } from '@lib/utils/useChange' import { ISlowQueryDataSource } from '../context' import { SlowqueryModel } from '@lib/client' const SLOW_DATA_LOAD_THRESHOLD = 2000 export const DEF_SLOW_QUERY_COLUMN_KEYS: IColumnKeys = { query: true, timestamp: true, query_time: true, memory_max: true } const QUERY_OPTIONS = 'slow_query.query_options' const DEF_ORDER_OPTIONS: IOrderOptions = { orderBy: 'timestamp', desc: true } interface RuntimeCacheEntity { data: SlowqueryModel[] isDataLoadedSlowly: boolean } export interface ISlowQueryOptions { visibleColumnKeys: IColumnKeys timeRange: TimeRange schemas: string[] groups: string[] searchText: string limit: number showInternal: boolean // below is for showing slow queries in the statement detail page digest: string plans: string[] } export const DEF_SLOW_QUERY_OPTIONS: ISlowQueryOptions = { visibleColumnKeys: DEF_SLOW_QUERY_COLUMN_KEYS, timeRange: DEFAULT_TIME_RANGE, schemas: [], searchText: '', limit: 100, showInternal: false, digest: '', plans: [], groups: [] } function useQueryOptions( initial?: ISlowQueryOptions, persistInSession: boolean = true ) { const [memoryQueryOptions, setMemoryQueryOptions] = useState( initial || DEF_SLOW_QUERY_OPTIONS ) const [sessionQueryOptions, setSessionQueryOptions] = useSessionStorageState( QUERY_OPTIONS, { defaultValue: initial || DEF_SLOW_QUERY_OPTIONS } ) const queryOptions = persistInSession ? sessionQueryOptions : memoryQueryOptions const setQueryOptions = useMemoizedFn( (value: React.SetStateAction) => { if (persistInSession) { setSessionQueryOptions(value as any) } else { setMemoryQueryOptions(value) } } ) return { queryOptions, setQueryOptions } } export interface ISlowQueryTableControllerOpts { cacheMgr?: CacheMgr showFullSQL?: boolean fetchSchemas?: boolean initialQueryOptions?: ISlowQueryOptions persistQueryInSession?: boolean filters?: Set ds: ISlowQueryDataSource } export interface ISlowQueryTableController { queryOptions: ISlowQueryOptions setQueryOptions: (value: React.SetStateAction) => void // Updating query options will result in a refresh orderOptions: IOrderOptions changeOrder: (orderBy: string, desc: boolean) => void resetOrder: () => void isLoading: boolean data?: SlowqueryModel[] isDataLoadedSlowly: boolean | null // SLOW_DATA_LOAD_THRESHOLD. NULL = Unknown allSchemas: string[] allGroups: string[] errors: Error[] availableColumnsInTable: IColumn[] // returned from backend saveClickedItemIndex: (idx: number) => void getClickedItemIndex: () => number } export default function useSlowQueryTableController({ cacheMgr, showFullSQL = false, fetchSchemas = true, initialQueryOptions, persistQueryInSession = true, ds, filters }: ISlowQueryTableControllerOpts): ISlowQueryTableController { const { orderOptions, changeOrder } = useOrderState( 'slow_query', persistQueryInSession, DEF_ORDER_OPTIONS ) function resetOrder() { changeOrder(DEF_ORDER_OPTIONS.orderBy, DEF_ORDER_OPTIONS.desc) } const { queryOptions, setQueryOptions } = useQueryOptions( initialQueryOptions, persistQueryInSession ) const [allSchemas, setAllSchemas] = useState([]) const [allGroups, setAllGroups] = useState([]) const [isOptionsLoading, setOptionsLoading] = useState(true) const [data, setData] = useState(undefined) const [isDataLoading, setDataLoading] = useState(false) const [isDataLoadedSlowly, setDataLoadedSlowly] = useState( null ) const [errors, setErrors] = useState([]) const { schemaColumns, isLoading: isColumnsLoading } = useSchemaColumns( ds.slowQueryAvailableFieldsGet ) const filteredData = useMemo(() => { if (!filters) { return data } return data?.filter((d) => filters.has(d.digest!)) }, [data, filters]) // Reload these options when sending a new request. useChange(() => { async function querySchemas() { if (!fetchSchemas) { return } try { // this file will be removed later const res = await ds.getDatabaseList(0, 0, { handleError: 'custom' }) setAllSchemas(res?.data || []) } catch (e) { setErrors((prev) => prev.concat(e as Error)) } } async function queryGroups() { try { const res = await ds.infoListResourceGroupNames({ handleError: 'custom' }) setAllGroups(res?.data || []) } catch (e) { // setErrors((prev) => prev.concat(e as Error)) } } async function doRequest() { setOptionsLoading(true) try { await Promise.all([ querySchemas(), queryGroups() // Multiple query options can be added later ]) } finally { setOptionsLoading(false) } } doRequest() }, [queryOptions]) useChange(() => { async function getSlowQueryList() { // Try cache if options are unchanged. // Note: When clicking "Query" manually, cache will be cleared before reach here. So that it // will always send a request without looking up in the cache. // The cache key is built over queryOptions, instead of evaluated one. // So that when passing in same relative times options (e.g. Recent 15min) // the cache can be reused. const cacheKey = JSON.stringify({ queryOptions, orderOptions }) { const cache = cacheMgr?.get(cacheKey) if (cache) { const cacheCloned = JSON.parse( JSON.stringify(cache) ) as RuntimeCacheEntity setData(cacheCloned.data) setDataLoadedSlowly(cacheCloned.isDataLoadedSlowly) setDataLoading(false) return } } // May be caused by visibleColumnKeys is empty (when available columns are not yet loaded) // In this case, we don't send any requests. const actualVisibleColumnKeys = getSelectedFields( queryOptions.visibleColumnKeys, derivedFields ).join(',') if (actualVisibleColumnKeys.length === 0) { return } const requestBeginAt = performance.now() setDataLoading(true) const timeRange = toTimeRangeValue(queryOptions.timeRange) try { const res = await ds.slowQueryListGet( timeRange[0], queryOptions.schemas, orderOptions.desc, queryOptions.digest, timeRange[1], actualVisibleColumnKeys, queryOptions.limit, orderOptions.orderBy, queryOptions.plans, queryOptions.groups, queryOptions.searchText, queryOptions.showInternal, { handleError: 'custom' } ) const data = res?.data || [] setData(data) setErrors([]) const elapsed = performance.now() - requestBeginAt const isLoadSlow = elapsed >= SLOW_DATA_LOAD_THRESHOLD setDataLoadedSlowly(isLoadSlow) const cacheEntity: RuntimeCacheEntity = { data, isDataLoadedSlowly: isLoadSlow } cacheMgr?.set(cacheKey, cacheEntity) } catch (e) { setData(undefined) setErrors((prev) => prev.concat(e)) } finally { setDataLoading(false) } } getSlowQueryList() }, [queryOptions, orderOptions]) const availableColumnsInTable = useMemo( () => slowQueryColumns(data ?? [], schemaColumns, showFullSQL), [data, schemaColumns, showFullSQL] ) const { saveClickedItemIndex, getClickedItemIndex } = useCacheItemIndex(cacheMgr) return { queryOptions, setQueryOptions, orderOptions, changeOrder, resetOrder, isLoading: isColumnsLoading || isDataLoading || isOptionsLoading, data: filteredData, isDataLoadedSlowly, allSchemas, allGroups, errors, availableColumnsInTable, saveClickedItemIndex, getClickedItemIndex } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/components/StatementsTable.tsx ================================================ import { useMemoizedFn } from 'ahooks' import React, { useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { getTheme } from 'office-ui-fabric-react/lib/Styling' import openLink from '@lib/utils/openLink' import { CardTable, ICardTableProps } from '@lib/components' import DetailPage from '../pages/Detail' import { IStatementTableController } from '../utils/useStatementTableController' import { DetailsRow, IDetailsListProps, IDetailsRowStyles } from 'office-ui-fabric-react/lib/DetailsList' interface Props extends Partial { controller: IStatementTableController } const theme = getTheme() export default function StatementsTable({ controller, ...restPrpos }: Props) { const navigate = useNavigate() const handleRowClick = useMemoizedFn( (rec, idx, ev: React.MouseEvent) => { // the evicted record's digest is empty string if (!rec.digest) { return } controller.saveClickedItemIndex(idx) const qs = DetailPage.buildQuery({ digest: rec.digest, schema: rec.schema_name, beginTime: controller.data!.timeRange[0], endTime: controller.data!.timeRange[1] }) openLink(`/statement/detail?${qs}`, ev, navigate) } ) const getKey = useCallback((row) => `${row.digest}_${row.schema_name}`, []) return ( ) } const renderRow: IDetailsListProps['onRenderRow'] = (props) => { if (!props) { return null } const customStyles: Partial = {} // the evicted record's digest is empty string if (!props.item.digest) { customStyles.root = { backgroundColor: theme.palette.neutralLighter, cursor: 'not-allowed' } } return } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/components/index.ts ================================================ import StatementsTable from './StatementsTable' export { StatementsTable } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { StatementEditableConfig, StatementGetStatementsRequest, StatementModel, StatementBinding } from '@lib/client' import { IContextConfig, ReqConfig } from '@lib/types' import { ISlowQueryDataSource } from '@lib/apps/SlowQuery' export type StatementTimeRange = { begin_time: number end_time: number } export interface IStatementDataSource extends ISlowQueryDataSource { statementsAvailableFieldsGet(options?: ReqConfig): AxiosPromise> statementsConfigGet( options?: ReqConfig ): AxiosPromise statementsConfigPost( request: StatementEditableConfig, options?: ReqConfig ): AxiosPromise statementsDownloadGet(token: string, options?: ReqConfig): AxiosPromise statementsDownloadTokenPost( request: StatementGetStatementsRequest, options?: ReqConfig ): AxiosPromise statementsListGet( beginTime?: number, endTime?: number, fields?: string, schemas?: Array, resourceGroups?: Array, stmtTypes?: Array, text?: string, options?: ReqConfig ): AxiosPromise> statementsPlanDetailGet( beginTime?: number, digest?: string, endTime?: number, plans?: Array, schemaName?: string, options?: ReqConfig ): AxiosPromise statementsPlansGet( beginTime?: number, digest?: string, endTime?: number, schemaName?: string, options?: ReqConfig ): AxiosPromise> statementsStmtTypesGet(options?: ReqConfig): AxiosPromise> statementsTimeRangesGet( options?: ReqConfig ): AxiosPromise> statementsPlanBindStatusGet?( sqlDigest: string, beginTime: number, endTime: number, options?: ReqConfig ): AxiosPromise statementsPlanBindCreate?( planDigest: string, options?: ReqConfig ): AxiosPromise statementsPlanBindDelete?( sqlDigest: string, options?: ReqConfig ): AxiosPromise } export interface IStatementConfig extends IContextConfig { enableExport?: boolean showConfig?: boolean // default is true showDBFilter?: boolean // default is true showResourceGroupFilter?: boolean // default is true showHelp?: boolean // default is true persistQueryOptions?: boolean // default is true // control whether show statement actual time range // for example: // Due to time window and expiration configurations, currently displaying data in time range: Today at 1:00 PM (UTC+08:00) ~ Today at 3:30 PM (UTC+08:00) // for serverless, the statement window is 1 minutes, instead of 30 mins in OP // so for serverless, this message is unnecessary showActualTimeRange?: boolean enablePlanBinding?: boolean // true means start to search instantly after changing any filter options // false means only to start searching after clicking the "Query" button instantQuery?: boolean // to limit the time range picker range timeRangeSelector?: { recentSeconds: number[] customAbsoluteRangePicker: boolean timeRangeLimit?: number } // show RU V2 fields (clinic-only: avg_ru_v2 / sum_ru_v2 columns and the // matching Basic-tab rows) showRuV2?: boolean } export interface IStatementContext { ds: IStatementDataSource cfg: IStatementConfig } export const StatementContext = createContext(null) export const StatementProvider = StatementContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import { Root } from '@lib/components' import useCache, { CacheContext } from '@lib/utils/useCache' import { addTranslations } from '@lib/utils/i18n' import { Detail, List } from './pages' import { StatementContext } from './context' import translations from './translations' import { useLocationChange } from '@lib/hooks/useLocationChange' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> } /> ) } export default function () { const statementCacheMgr = useCache(2) const ctx = useContext(StatementContext) if (ctx === null) { throw new Error('StatementContext must not be null') } return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanBind.module.less ================================================ .GreenDot { background-color: #0bc40b; height: 8px; width: 8px; border-radius: 50%; display: inline-block; } .GreyDot { background-color: #c3c3c3; height: 8px; width: 8px; border-radius: 50%; display: inline-block; } .PreBlock { background: #f1f1f1; padding: 10px; } .SmallFont { font-size: 13px; font-weight: normal; } .Center { text-align: center; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanBind.tsx ================================================ import React, { useContext, useState, useMemo, useRef, useEffect } from 'react' import { Space, Button, Modal, Tooltip, Radio, Alert } from 'antd' import { InfoCircleOutlined } from '@ant-design/icons' import { useClientRequest } from '@lib/utils/useClientRequest' import { StatementModel } from '@lib/client' import { useTranslation } from 'react-i18next' import { IPageQuery } from '.' import { StatementContext } from '../../context' import { CardTable } from '@lib/components' import styles from './PlanBind.module.less' import { planColumns as genPlanColumns } from '../../utils/tableColumns' import { SelectionMode, CheckboxVisibility } from 'office-ui-fabric-react/lib/DetailsList' import { Selection } from 'office-ui-fabric-react/lib/Selection' interface PlanBindProps { query: IPageQuery plans: StatementModel[] } const PlanBind = ({ query, plans }: PlanBindProps) => { const ctx = useContext(StatementContext) const { t } = useTranslation() const { data: planBindingStatus } = useClientRequest((reqConfig) => ctx!.ds.statementsPlanBindStatusGet!( query.digest!, query.beginTime!, query.endTime!, reqConfig ) ) const [boundPlanDigest, setBoundPlanDigest] = useState(null) const [showPlanBindModal, setShowPlanBindModal] = useState(false) useEffect(() => { if (planBindingStatus) { setBoundPlanDigest(planBindingStatus.plan_digest!) } }, [planBindingStatus]) const hasPlanToBind = plans[0].plan_can_be_bound return ( {!hasPlanToBind ? ( Unavailable ) : ( <> {boundPlanDigest ? ( {t('statement.pages.detail.plan_bind.bound')} ) : ( {t('statement.pages.detail.plan_bind.not_bound')} )} )} ) } interface PlanBindModalProps { showPlanBindModal: boolean boundPlanDigest: string | null plans: StatementModel[] sqlDigest: string onHandleModalVisibility: (visibility: boolean) => void onHandleSetBoundPlanDigets: (planDigest: string | null) => void } const PlanBindModal = ({ showPlanBindModal, boundPlanDigest, plans, sqlDigest, onHandleModalVisibility, onHandleSetBoundPlanDigets }: PlanBindModalProps) => { const ctx = useContext(StatementContext) const { t } = useTranslation() const [selectedPlan, setSelectedPlan] = useState(null) const [isLoading, setIsLoading] = useState(false) const handlePlanBind = async () => { setIsLoading(true) try { await ctx!.ds.statementsPlanBindCreate!(selectedPlan!) onHandleSetBoundPlanDigets(selectedPlan!) } catch (error) { console.log(error) } finally { setIsLoading(false) } } const handleDropPlan = async () => { setIsLoading(true) try { await ctx!.ds.statementsPlanBindDelete!(sqlDigest) setSelectedPlan(null) onHandleSetBoundPlanDigets(null) } catch (error) { console.log(error) } finally { setIsLoading(false) } } const handleSelectedPlanChange = (plan: StatementModel[] | []) => { if (plan.length === 0) return setSelectedPlan(null) return setSelectedPlan(plan[0].plan_digest!) } return ( {t('statement.pages.detail.plan_bind.title')}{' '} {boundPlanDigest ? ( {t('statement.pages.detail.plan_bind.bound')} ) : ( {t('statement.pages.detail.plan_bind.not_bound')} )} } onCancel={() => onHandleModalVisibility(false)} width={1000} footer={
{boundPlanDigest ? ( {t('statement.pages.detail.plan_bind.bound_status_desc')} ) : ( )}
} destroyOnClose >

{t('statement.pages.detail.plan_bind.bound_sql')}

        {sqlDigest}
      

{t('statement.pages.detail.plan_bind.to_plan')}

{!isLoading && ( )}
) } interface PlanTableProps { boundPlanDigest: string | null plans: StatementModel[] onHandleSelectedPlanChange: (plan: StatementModel[] | []) => void } const PlanTable = ({ boundPlanDigest, plans, onHandleSelectedPlanChange }: PlanTableProps) => { const planColumns = useMemo(() => genPlanColumns(plans || []), [plans]) const selection = useRef( new Selection({ canSelectItem: (item) => { const digest = (item as StatementModel).plan_digest return !boundPlanDigest || digest === boundPlanDigest }, onSelectionChanged: () => { const s = selection.current.getSelection() as StatementModel[] onHandleSelectedPlanChange(s) if (!boundPlanDigest) return // if bound plan is selected, keep it selected const selectedPlanIndex = plans.findIndex( (v) => v.plan_digest === boundPlanDigest ) if (s.length === 0) { selection.current.setIndexSelected(selectedPlanIndex, true, true) } } }) ) useEffect(() => { if (boundPlanDigest && plans.length > 0) { const selectedPlanIndex = plans.findIndex( (v) => v.plan_digest === boundPlanDigest ) selection.current.setIndexSelected(selectedPlanIndex, true, true) } else if (!boundPlanDigest) { selection.current.setAllSelected(false) } }, [boundPlanDigest]) return ( ( )} /> ) } export default PlanBind ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetail.tsx ================================================ import React, { useState, useContext, useMemo } from 'react' import { Space, Tabs, Modal } from 'antd' import { useTranslation } from 'react-i18next' import { AnimatedSkeleton, BinaryPlanTable, PlanText, Card, CopyLink, Descriptions, ErrorBar, Expand, HighlightSQL, TextWithInfo } from '@lib/components' import { VisualPlanThumbnailView, VisualPlanView } from '@lib/components/VisualPlan' import { useClientRequest } from '@lib/utils/useClientRequest' import formatSql from '@lib/utils/sqlFormatter' import { useVersionedLocalStorageState } from '@lib/utils/useVersionedLocalStorageState' import type { IPageQuery } from '.' import DetailTabs from './PlanDetailTabs' import { useSchemaColumns } from '../../utils/useSchemaColumns' import { telemetry } from '../../utils/telemetry' import { StatementContext } from '../../context' export interface IQuery extends IPageQuery { plans: string[] allPlans: number } export interface IPlanDetailProps { query: IQuery } const STMT_DETAIL_PLAN_EXPAND = 'statement.detail_plan_expand' function PlanDetail({ query }: IPlanDetailProps) { const ctx = useContext(StatementContext) const { t } = useTranslation() const { data, isLoading: isDataLoading, error } = useClientRequest((reqConfig) => ctx!.ds.statementsPlanDetailGet( query.beginTime!, query.digest!, query.endTime!, query.plans, query.schema!, reqConfig ) ) const { isLoading: isSchemaLoading } = useSchemaColumns( ctx!.ds.statementsAvailableFieldsGet ) const isLoading = isDataLoading || isSchemaLoading const binaryPlanObj = useMemo(() => { const json = data?.binary_plan_json ?? data?.binary_plan if (json) { return JSON.parse(json) } return undefined }, [data?.binary_plan, data?.binary_plan_json]) const [isVpVisible, setIsVpVisable] = useState(false) const toggleVisualPlan = (action: 'open' | 'close') => { telemetry.toggleVisualPlanModal(action) setIsVpVisable(!isVpVisible) } const [detailExpand, setDetailExpand] = useVersionedLocalStorageState( STMT_DETAIL_PLAN_EXPAND, { defaultValue: { prev_query: false, query: false, plan: false } } ) const togglePrevQuery = () => setDetailExpand((prev) => ({ ...prev, prev_query: !prev.prev_query })) const toggleQuery = () => setDetailExpand((prev) => ({ ...prev, query: !prev.query })) let titleKey if (query.allPlans === 1) { titleKey = 'one_for_all' } else if (query.plans.length === query.allPlans) { titleKey = 'all' } else { titleKey = 'some' } return ( {error && } {data && ( <> { // avoid page hang when sql is too long data.query_sample_text!.length < 10000 && ( ) } } > } > {data.prev_sample_text ? ( { // avoid page hang when sql is too long data.prev_sample_text!.length < 10000 && ( ) } } > } > ) : null} {(!!data.binary_plan_text || !!data.plan) && ( <> {t('statement.pages.detail.desc.plans.execution.title')} telemetry.clickPlanTabs(key, data.digest!) } > {!!data.binary_plan_text && (
)} {binaryPlanObj && !binaryPlanObj.main.discardedDueToTooLong && ( toggleVisualPlan('close')} footer={null} destroyOnClose={true} bodyStyle={{ background: '#f5f5f5', height: window.innerHeight - 100 }} >
toggleVisualPlan('open')}>
)} )} )} ) } export default PlanDetail ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabBasic.tsx ================================================ import { Tooltip } from 'antd' import React from 'react' import { getValueFormat } from '@baurine/grafana-value-formats' import { StatementModel } from '@lib/client' import { DateTime, Pre, ValueWithTooltip, TextWrap } from '@lib/components' export const tabBasicItems = ( data: StatementModel, options?: { showRuV2?: boolean } ) => [ { key: 'table_names', value: (
{data.table_names}
) }, { key: 'index_names', value: data.index_names }, { key: 'first_seen', value: data.first_seen && ( ) }, { key: 'last_seen', value: data.last_seen && ( ) }, { key: 'exec_count', value: }, { key: 'plan_cache_hits', value: }, { key: 'sum_latency', value: getValueFormat('ns')(data.sum_latency || 0, 1) }, { key: 'sample_user', value: data.sample_user }, { key: 'sum_errors', value: }, { key: 'sum_warnings', value: }, { key: 'avg_mem', value: getValueFormat('bytes')(data.avg_mem || 0, 1) }, { key: 'max_mem', value: getValueFormat('bytes')(data.max_mem || 0, 1) }, { key: 'avg_mem_arbitration', value: getValueFormat('s')(data.avg_mem_arbitration || 0, 1) }, { key: 'max_mem_arbitration', value: getValueFormat('s')(data.max_mem_arbitration || 0, 1) }, { key: 'avg_disk', value: getValueFormat('bytes')(data.avg_disk || 0, 1) }, { key: 'max_disk', value: getValueFormat('bytes')(data.max_disk || 0, 1) }, { key: 'avg_ru', value: getValueFormat('short')(data.avg_ru || 0, 1) }, { key: 'max_ru', value: getValueFormat('short')(data.max_ru || 0, 1) }, ...(options?.showRuV2 ? [ { key: 'avg_ru_v2', value: getValueFormat('short')(data.avg_ru_v2 || 0, 1) }, { key: 'sum_ru_v2', value: getValueFormat('short')(data.sum_ru_v2 || 0, 1) }, { key: 'max_ru_v2', value: getValueFormat('short')(data.max_ru_v2 || 0, 1) }, { key: 'sum_ru', value: getValueFormat('short')(data.sum_ru || 0, 1) } ] : []), { key: 'resource_group', value: data.resource_group } ] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabCopr.tsx ================================================ import React from 'react' import { getValueFormat } from '@baurine/grafana-value-formats' import { StatementModel } from '@lib/client' import { ValueWithTooltip } from '@lib/components' export const tabCoprItems = (data: StatementModel) => [ { key: 'sum_cop_task_num', value: data.sum_cop_task_num }, { key: 'avg_processed_keys', value: }, { key: 'max_processed_keys', value: }, { key: 'avg_total_keys', value: }, { key: 'max_total_keys', value: }, { key: 'avg_rocksdb_block_cache_hit_count', value: ( ) }, { key: 'max_rocksdb_block_cache_hit_count', value: ( ) }, { key: 'avg_rocksdb_block_read_byte', value: ( ) }, { key: 'max_rocksdb_block_read_byte', value: ( ) }, { key: 'avg_rocksdb_block_read_count', value: }, { key: 'max_rocksdb_block_read_count', value: }, { key: 'avg_rocksdb_delete_skipped_count', value: ( ) }, { key: 'max_rocksdb_delete_skipped_count', value: ( ) }, { key: 'avg_rocksdb_key_skipped_count', value: }, { key: 'max_rocksdb_key_skipped_count', value: }, { key: 'sum_unpacked_bytes_sent_tikv_total', value: ( ) }, { key: 'sum_unpacked_bytes_received_tikv_total', value: ( ) }, { key: 'sum_unpacked_bytes_sent_tikv_cross_zone', value: ( ) }, { key: 'sum_unpacked_bytes_received_tikv_cross_zone', value: ( ) }, { key: 'sum_unpacked_bytes_sent_tiflash_total', value: ( ) }, { key: 'sum_unpacked_bytes_received_tiflash_total', value: ( ) }, { key: 'sum_unpacked_bytes_sent_tiflash_cross_zone', value: ( ) }, { key: 'sum_unpacked_bytes_received_tiflash_cross_zone', value: ( ) }, { key: 'avg_ia_remote_read_segment_size', value: ( ) }, { key: 'max_ia_remote_read_segment_size', value: ( ) } ] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabTime.tsx ================================================ import React from 'react' import { StatementModel } from '@lib/client' import { Typography } from 'antd' import { TFunction } from 'react-i18next' export const tabTimeItems = (data: StatementModel, t: TFunction) => [ { key: 'parse_latency', avg: data.avg_parse_latency, max: data.max_parse_latency }, { key: 'compile_latency', avg: data.avg_compile_latency, max: data.max_compile_latency }, { key: 'wait_time', avg: data.avg_wait_time, max: data.max_wait_time }, { key: 'process_time', avg: data.avg_process_time, max: data.max_process_time }, { key: 'backoff_time', avg: data.avg_backoff_time, max: data.max_backoff_time }, { key: 'get_commit_ts_time', avg: data.avg_get_commit_ts_time, max: data.max_get_commit_ts_time }, { key: 'local_latch_wait_time', avg: data.avg_local_latch_wait_time, max: data.max_local_latch_wait_time }, { key: 'resolve_lock_time', avg: data.avg_resolve_lock_time, max: data.max_resolve_lock_time }, { key: 'prewrite_time', avg: data.avg_prewrite_time, max: data.max_prewrite_time }, { key: 'commit_time', avg: data.avg_commit_time, max: data.max_commit_time }, { key: 'commit_backoff_time', avg: data.avg_commit_backoff_time, max: data.max_commit_backoff_time }, { key: 'rc_wait_time', avg: data.avg_time_queued_by_rc, max: data.max_time_queued_by_rc }, { key: 'query_time2', keyDisplay: ( {t('statement.fields.query_time2')} ), avg: data.avg_latency, min: data.min_latency, max: data.max_latency }, { key: 'ia_remote_read_segment_wait_time', avg: data.avg_ia_remote_read_segment_wait_time, max: data.max_ia_remote_read_segment_wait_time } ] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabTxn.tsx ================================================ import React from 'react' import { getValueFormat } from '@baurine/grafana-value-formats' import { StatementModel } from '@lib/client' import { ValueWithTooltip } from '@lib/components' export const tabTxnItems = (data: StatementModel) => [ { key: 'avg_affected_rows', value: }, { key: 'sum_backoff_times', value: }, { key: 'avg_write_keys', value: }, { key: 'max_write_keys', value: }, { key: 'avg_write_size', value: getValueFormat('bytes')(data.avg_write_size || 0, 1) }, { key: 'max_write_size', value: getValueFormat('bytes')(data.max_write_size || 0, 1) }, { key: 'avg_prewrite_regions', value: }, { key: 'max_prewrite_regions', value: }, { key: 'avg_txn_retry', value: }, { key: 'max_txn_retry', value: } ] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabs.tsx ================================================ import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' import { CardTable, CardTabs } from '@lib/components' import { StatementModel } from '@lib/client' import { valueColumns, timeValueColumns } from '@lib/utils/tableColumns' import { tabBasicItems } from './PlanDetailTabBasic' import { tabTimeItems } from './PlanDetailTabTime' import { tabCoprItems } from './PlanDetailTabCopr' import { tabTxnItems } from './PlanDetailTabTxn' import SlowQueryTab from './SlowQueryTab' import { useSchemaColumns } from '../../utils/useSchemaColumns' import type { IQuery } from './PlanDetail' import { StatementContext } from '../../context' import { telemetry as stmtTelemetry } from '../../utils/telemetry' export default function DetailTabs({ data, query }: { data: StatementModel query: IQuery }) { const ctx = useContext(StatementContext) const { t } = useTranslation() const { schemaColumns } = useSchemaColumns( ctx!.ds.statementsAvailableFieldsGet ) const columnsSet = new Set(schemaColumns) const tabs = [ { key: 'basic', title: t('statement.pages.detail.tabs.basic'), content: () => { const items = tabBasicItems(data, { showRuV2: !!ctx?.cfg.showRuV2 }) const columns = valueColumns('statement.fields.') return ( ) } }, { key: 'time', title: t('statement.pages.detail.tabs.time'), content: () => { const items = tabTimeItems(data, t) const columns = timeValueColumns('statement.fields.', items) return ( ) } }, { key: 'copr', title: t('statement.pages.detail.tabs.copr'), content: () => { const items = tabCoprItems(data).filter((item) => columnsSet.has(item.key) ) const columns = valueColumns('statement.fields.') return ( ) } }, { key: 'txn', title: t('statement.pages.detail.tabs.txn'), content: () => { const items = tabTxnItems(data) const columns = valueColumns('statement.fields.') return ( ) } }, { key: 'slow_query', title: t('statement.pages.detail.tabs.slow_query'), content: () => } ] return ( { stmtTelemetry.switchDetailTab(tab) }} /> ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/SlowQueryTab.tsx ================================================ import React, { useContext } from 'react' import { fromTimeRangeValue } from '@lib/components' import useSlowQueryTableController, { DEF_SLOW_QUERY_OPTIONS } from '@lib/apps/SlowQuery/utils/useSlowQueryTableController' import SlowQueriesTable from '@lib/apps/SlowQuery/components/SlowQueriesTable' import { IQuery } from './PlanDetail' import { StatementContext } from '../../context' export interface ISlowQueryTabProps { query: IQuery } export default function SlowQueryTab({ query }: ISlowQueryTabProps) { const ctx = useContext(StatementContext) const controller = useSlowQueryTableController({ initialQueryOptions: { ...DEF_SLOW_QUERY_OPTIONS, timeRange: fromTimeRangeValue([query.beginTime!, query.endTime!]), limit: 100, digest: query.digest!, plans: query.plans }, persistQueryInSession: false, fetchSchemas: false, ds: ctx!.ds }) return } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/index.tsx ================================================ import { Alert, Space, Typography } from 'antd' import { SelectionMode } from 'office-ui-fabric-react/lib/DetailsList' import { Selection } from 'office-ui-fabric-react/lib/Selection' import React, { useContext, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' import { ArrowLeftOutlined } from '@ant-design/icons' import { useIsFeatureSupport } from '@lib/utils/store' import { StatementModel } from '@lib/client' import { AnimatedSkeleton, CardTable, DateTime, Descriptions, ErrorBar, Expand, Head, HighlightSQL, TextWithInfo } from '@lib/components' import CopyLink from '@lib/components/CopyLink' import formatSql from '@lib/utils/sqlFormatter' import { buildQueryFn, parseQueryFn } from '@lib/utils/query' import { useClientRequest } from '@lib/utils/useClientRequest' import { useVersionedLocalStorageState } from '@lib/utils/useVersionedLocalStorageState' import { planColumns as genPlanColumns } from '../../utils/tableColumns' import PlanDetail from './PlanDetail' import PlanBind from './PlanBind' import { StatementContext } from '../../context' export interface IPageQuery { digest?: string schema?: string beginTime?: number endTime?: number } const STMT_DETAIL_EXPAND = 'statement.detail_expand' // sort plans by plan_count first, // if plan_count is the same, sort plans by ava_latency const compareFn = (a: StatementModel, b: StatementModel) => { if (a.exec_count! === b.exec_count!) { return b.avg_latency! - a.avg_latency! } return b.exec_count! - a.exec_count! } function DetailPage() { const ctx = useContext(StatementContext) const location = useLocation() const navigate = useNavigate() const query = DetailPage.parseQuery(location.search) const historyBack = (location.state ?? ({} as any)).historyBack ?? false const { data: plans, isLoading, error } = useClientRequest((reqConfig) => ctx!.ds.statementsPlansGet( query.beginTime!, query.digest!, query.endTime!, query.schema!, reqConfig ) ) const { t } = useTranslation() const planColumns = useMemo(() => genPlanColumns(plans || []), [plans]) const [selectedPlans, setSelectedPlans] = useState([]) const selection = useRef( new Selection({ onSelectionChanged: () => { const s = selection.current.getSelection() as StatementModel[] setSelectedPlans(s.map((v) => v.plan_digest || '')) } }) ) const [sqlExpanded, setSqlExpanded] = useVersionedLocalStorageState( STMT_DETAIL_EXPAND, { defaultValue: false } ) const toggleSqlExpanded = () => setSqlExpanded((prev) => !prev) useEffect(() => { if (plans && plans.length > 0) { selection.current.setAllSelected(true) plans.sort(compareFn) } }, [plans]) const supportPlanBinding = useIsFeatureSupport('plan_binding') return (
historyBack ? navigate(-1) : navigate('/statement') } > {t('statement.pages.detail.head.back')} } titleExtra={ ctx?.cfg.enablePlanBinding && supportPlanBinding && plans && plans.length > 0 ? ( ) : null } > {error && } {plans && plans.length > 0 && ( <> { // avoid page hang when sql is too long plans[0].digest_text!.length < 10000 && ( ) } } > } > } >
{plans[0].digest}
} > {' ~ '} } > {plans.length} } > {query.schema!}
1 ? 'block' : 'none' }} data-e2e="statement_multiple_execution_plans" >
)}
{selectedPlans.length > 0 && plans && plans.length > 0 && ( )}
) } DetailPage.buildQuery = buildQueryFn() DetailPage.parseQuery = parseQueryFn() export default DetailPage ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/List/List.module.less ================================================ @import 'antd/es/style/themes/default.less'; .list { &_container { display: flex; flex-direction: column; height: 100vh; } &_toolbar { @media only screen and (max-width: @screen-md) { flex-direction: column; } } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/List/StatementSettingForm.tsx ================================================ import React, { useState, useCallback } from 'react' import { Form, Skeleton, Switch, Input, Slider, Space, Button, Modal } from 'antd' import { ExclamationCircleOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { StatementEditableConfig } from '@lib/client' import { useClientRequest } from '@lib/utils/useClientRequest' import { DrawerFooter, ErrorBar } from '@lib/components' import { useIsWriteable } from '@lib/utils/store' import { ReqConfig } from '@lib/types' import { AxiosPromise } from 'axios' interface Props { onClose?: () => void onConfigUpdated?: () => any getStatementConfig: ( reqConfig: ReqConfig ) => AxiosPromise updateStatementConfig: ( request: StatementEditableConfig, options?: ReqConfig ) => AxiosPromise } const convertArrToObj = (arr: number[]) => arr.reduce((acc, cur) => { acc[cur] = cur return acc }, {}) function StatementSettingForm({ onClose, onConfigUpdated, getStatementConfig, updateStatementConfig }: Props) { const [submitting, setSubmitting] = useState(false) const { t } = useTranslation() const isWriteable = useIsWriteable() const { data: initialConfig, isLoading: loading, error } = useClientRequest((reqConfig) => getStatementConfig(reqConfig)) const handleSubmit = useCallback( (values) => { async function updateConfig(values) { const newConfig: StatementEditableConfig = { enable: values.enable, max_size: values.max_size, refresh_interval: values.refresh_interval * 60, history_size: values.history_size, internal_query: values.internal_query } try { setSubmitting(true) await updateStatementConfig(newConfig) onClose?.() onConfigUpdated?.() } finally { setSubmitting(false) } } if (!values.enable && (initialConfig?.enable ?? true)) { // warning Modal.confirm({ title: t('statement.settings.close_statement'), icon: , content: t('statement.settings.close_statement_warning'), okText: t('statement.settings.actions.close'), cancelText: t('statement.settings.actions.cancel'), okButtonProps: { danger: true }, onOk: () => updateConfig(values) }) } else { updateConfig(values) } }, [t, onClose, onConfigUpdated, initialConfig, updateStatementConfig] ) return ( <> {error && } {loading && } {!loading && initialConfig && (
prev.enable !== cur.enable} > {({ getFieldValue }) => getFieldValue('enable') && ( <> prev.refresh_interval !== cur.refresh_interval || prev.history_size !== cur.history_size } data-e2e="statement_setting_keep_duration" > {({ getFieldValue }) => { const refreshInterval = getFieldValue('refresh_interval') || 0 const historySize = getFieldValue('history_size') || 0 const totalMins = refreshInterval * historySize const day = Math.floor(totalMins / (24 * 60)) const hour = Math.floor((totalMins - day * 24 * 60) / 60) const min = totalMins - day * 24 * 60 - hour * 60 return `${day} day ${hour} hour ${min} min` }} ) } )} ) } export default StatementSettingForm ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/List/index.tsx ================================================ import React, { useState, useContext, useMemo } from 'react' import { Space, Tooltip, Drawer, Button, Checkbox, Result, Input, Dropdown, Menu, Alert, message } from 'antd' import { LoadingOutlined, SettingOutlined, ExportOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons' import { useURLTimeRange } from '@lib/hooks/useURLTimeRange' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import { useTranslation } from 'react-i18next' import { CacheContext } from '@lib/utils/useCache' import { Card, ColumnsSelector, Toolbar, MultiSelect, TimeRangeSelector, DateTime, toTimeRangeValue, IColumnKeys, LimitTimeRange } from '@lib/components' import { useVersionedLocalStorageState } from '@lib/utils/useVersionedLocalStorageState' import { StatementsTable } from '../../components' import StatementSettingForm from './StatementSettingForm' import useStatementTableController, { DEF_STMT_COLUMN_KEYS, DEF_STMT_QUERY_OPTIONS } from '../../utils/useStatementTableController' import styles from './List.module.less' import { useDebounceFn, useMemoizedFn } from 'ahooks' import { useDeepCompareChange } from '@lib/utils/useChange' import { StatementModel } from '@lib/client' import { isDistro } from '@lib/utils/distro' import { StatementContext } from '../../context' import { telemetry as stmtTelmetry } from '../../utils/telemetry' const STMT_VISIBLE_COLUMN_KEYS = 'statement.visible_column_keys' const STMT_SHOW_FULL_SQL = 'statement.show_full_sql' function getDataTimeRange( list?: StatementModel[] ): [number, number] | undefined { if (!list || list?.length === 0) { return } let min = list[0].summary_begin_time ?? 0 let max = list[0].summary_end_time ?? 0 for (const item of list) { if ((item.summary_begin_time ?? 0) < min) { min = item.summary_begin_time ?? 0 } if ((item.summary_end_time ?? 0) > max) { max = item.summary_end_time ?? 0 } } if (min === 0 || max === 0) { return } return [min, max] } export default function StatementsOverview() { const { t } = useTranslation() const ctx = useContext(StatementContext) const cacheMgr = useContext(CacheContext) const [showSettings, setShowSettings] = useState(false) const [visibleColumnKeys, setVisibleColumnKeys] = useVersionedLocalStorageState( STMT_VISIBLE_COLUMN_KEYS, { defaultValue: DEF_STMT_COLUMN_KEYS }, ctx?.cfg.persistQueryOptions ) const [showFullSQL, setShowFullSQL] = useVersionedLocalStorageState( STMT_SHOW_FULL_SQL, { defaultValue: false } ) const [downloading, setDownloading] = useState(false) const { timeRange, setTimeRange } = useURLTimeRange() const controller = useStatementTableController({ cacheMgr, showFullSQL, fetchSchemas: ctx?.cfg.showDBFilter, fetchGroups: ctx?.cfg.showResourceGroupFilter, fetchConfig: ctx?.cfg.showConfig, persistQueryInSession: ctx?.cfg.persistQueryOptions, initialQueryOptions: { ...DEF_STMT_QUERY_OPTIONS, visibleColumnKeys, timeRange }, ds: ctx!.ds }) function updateVisibleColumnKeys(v: IColumnKeys) { setVisibleColumnKeys(v) stmtTelmetry.changeVisibleColumns(v) if (!v[controller.orderOptions.orderBy]) { controller.resetOrder() } } function menuItemClick({ key }) { switch (key) { case 'export': const hide = message.loading( t('statement.pages.overview.toolbar.exporting') + '...', 0 ) downloadCSV().finally(hide) stmtTelmetry.export() break } } const dropdownMenu = ( } data-e2e="statement_export_btn" > {downloading ? t('statement.pages.overview.toolbar.exporting') : t('statement.pages.overview.toolbar.export')} ) const [filterSchema, setFilterSchema] = useState( controller.queryOptions.schemas ) const [filterGroup, setFilterGroup] = useState( controller.queryOptions.groups ) const [filterStmtType, setFilterStmtType] = useState( controller.queryOptions.stmtTypes ) const [filterText, setFilterText] = useState( controller.queryOptions.searchText ) const sendQueryNow = useMemoizedFn(() => { cacheMgr?.clear() controller.setQueryOptions({ timeRange, schemas: filterSchema, groups: filterGroup, stmtTypes: filterStmtType, searchText: filterText, visibleColumnKeys }) stmtTelmetry.search() }) const sendQueryDebounced = useDebounceFn(sendQueryNow, { wait: 300 }).run useDeepCompareChange(() => { if ( ctx?.cfg.instantQuery === false || controller.isDataLoadedSlowly || // if data was loaded slowly controller.isDataLoadedSlowly === null // or a request is not yet finished (which means slow network).. ) { // do not send requests on-the-fly. return } sendQueryDebounced() }, [ timeRange, filterSchema, filterGroup, filterStmtType, filterText, visibleColumnKeys ]) const downloadCSV = useMemoizedFn(async () => { // use last effective query options const timeRangeValue = toTimeRangeValue(controller.queryOptions.timeRange) try { setDownloading(true) const res = await ctx!.ds.statementsDownloadTokenPost({ begin_time: timeRangeValue[0], end_time: timeRangeValue[1], fields: '*', schemas: controller.queryOptions.schemas, resource_groups: controller.queryOptions.groups, stmt_types: controller.queryOptions.stmtTypes, text: controller.queryOptions.searchText }) const token = res.data if (token) { window.location.href = `${ ctx!.cfg.apiPathBase }/statements/download?token=${token}` } } finally { setDownloading(false) } }) const dataTimeRange = useMemo(() => { return getDataTimeRange(controller.data?.list) }, [controller.data]) return (
{ctx?.cfg.timeRangeSelector !== undefined ? ( {}} timeRangeLimit={ctx?.cfg.timeRangeSelector?.timeRangeLimit} /> ) : ( { setTimeRange(t) stmtTelmetry.changeTimeRange(t) }} data-e2e="statement_time_range_selector" /> )} {(ctx?.cfg.showDBFilter ?? true) && ( { setFilterSchema(d) stmtTelmetry.changeDatabases() }} items={controller.allSchemas} data-e2e="execution_database_name" /> )} {(ctx?.cfg.showResourceGroupFilter ?? true) && controller.allGroups?.length > 1 && ( { setFilterGroup(d) }} items={controller.allGroups} data-e2e="resource_group_name_select" /> )} { setFilterStmtType(v) stmtTelmetry.changeStmtTypes() }} items={controller.allStmtTypes} data-e2e="statement_types" /> { setFilterText(e.target.value) stmtTelmetry.changeSearchText() }} onSearch={sendQueryNow} placeholder={t( 'statement.pages.overview.toolbar.keyword.placeholder' )} data-e2e="sql_statements_search" enterButton={t('statement.pages.overview.toolbar.query')} /> {controller.isLoading && ( )} {controller.availableColumnsInTable.length > 0 && ( { setShowFullSQL(e.target.checked) stmtTelmetry.toggleShowFullSQL(e.target.checked) }} data-e2e="statement_show_full_sql" > {t( 'statement.pages.overview.toolbar.select_columns.show_full_sql' )} } /> )} {(ctx?.cfg.showConfig ?? true) && ( { setShowSettings(true) stmtTelmetry.openSetting() }} data-e2e="statement_setting" /> )} {(ctx?.cfg.enableExport ?? true) && (
)} {!isDistro() && (ctx!.cfg.showHelp ?? true) && ( { window.open(t('statement.settings.help_url'), '_blank') stmtTelmetry.openHelp() }} /> )}
{controller.isEnabled ? (
{controller.isDataLoadedSlowly && (ctx?.cfg.instantQuery ?? true) && ( )}

{dataTimeRange && (ctx?.cfg.showActualTimeRange ?? true) && (

{t('statement.pages.overview.actual_range')} {' ~ '}
)} {(controller.data?.list.length ?? 0) > 0 && (
{t('statement.pages.overview.result_count', { n: controller.data?.list.length })}
)}

) : ( {!isDistro() && ( )} } /> )} setShowSettings(false)} destroyOnClose={true} > setShowSettings(false)} onConfigUpdated={sendQueryNow} getStatementConfig={ctx!.ds.statementsConfigGet} updateStatementConfig={ctx!.ds.statementsConfigPost} />
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/index.ts ================================================ import List from './List' import Detail from './Detail' export { List, Detail } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/translations/en.yaml ================================================ statement: nav_title: SQL Statements pages: detail: head: back: List title: Statement Information plan_bind: title: Plan Binding bound: Bound not_bound: Not Bound bound_available_tooltip: 'Plan Binding only supported bindable SQL statements including SELECT, DELETE, UPDATE, and INSERT / REPLACE with SELECT subqueries.' notice: 'Notice: This feature does not work for queries with subqueries, queries that access TiFlash, or queries that join 3 or more tables.' bound_sql: 'Bind this SQL' to_plan: 'to a special plan' bound_status_desc: The plan has bound to this SQL drop_btn_txt: Drop bind_btn_txt: Bind desc: time_range: Time Range plans: note: There are multiple execution plans for this kind of SQL statement. You can choose to view one or multiple of them. title: one_for_all: Execution Detail all: Execution Detail of All Plans some: 'Execution Detail of Selected {{n}} Plans' execution: title: Execution Plan text: Text table: Table visual: Visual modal_title: Visual Plan tabs: basic: Basic time: Time copr: Coprocessor Read txn: Transaction slow_query: Slow Query overview: toolbar: schemas: placeholder: All Databases selected: '{{ n }} Databases' columnTitle: Execution Database Name resource_groups: placeholder: All Resource Groups selected: '{{ n }} Resource Groups' columnTitle: Resource Group Name statement_types: placeholder: All Kinds selected_one: '{{ count }} Kind' selected_other: '{{ count }} Kinds' columnTitle: Statement Kind select_columns: show_full_sql: Show Full Query Text query: Query keyword: placeholder: Filter keyword time_range_selector: name: Select Time Range recent: Recent usual_time_ranges: Common custom_time_ranges: Custom export: Export exporting: Exporting result_count: '{{ n }} results.' actual_range: 'Due to time window and expiration configurations, currently displaying data in time range: ' slow_load_info: On-the-fly update is disabled due to slow data loading. You can initiate query manually by clicking the "Query" button. settings: title: Settings disabled_result: title: Feature Not Enabled sub_title: | Statement feature is not enabled so that statement history cannot be viewed. You can modify settings to enable the feature and wait for new data being collected. open_setting: Open Settings close_statement: Disable Statement Feature close_statement_warning: Are you sure want to disable this feature? Current statement history will be cleared. switch: Enable Feature switch_tooltip: Whether Statement feature is enabled. When enabled, there will be a small SQL statement execution overhead. max_size: 'Max # Statement' max_size_tooltip: Max number of statement to collect. After exceeding, old statement information will be dropped. You may enlarge this setting when memory is sufficient and you discovered that data displayed in UI is incomplete. refresh_interval: Window Size (min) refresh_interval_tooltip: By reducing this setting you can select time range more precisely. history_size: '# Windows' history_size_tooltip: By enlarging this setting more statement history will be preserved, with larger memory cost. keep_duration: SQL Statement History Size keep_duration_tooltip: Window Size × Number of Windows internal_query: Collect Internal Queries internal_query_tooltip: After enabled, {{distro.tidb}} internal queries will be collected as well. actions: save: Save close: Disable cancel: Cancel help: Help help_url: https://docs.pingcap.com/tidb/dev/dashboard-statement-list fields: table_names: Table Names related_schemas: Database related_schemas_tooltip: Related databases of the statement plan_digest: Plan ID plan_digest_tooltip: Different execution plans have different plan ID digest_text: Statement Template digest_text_tooltip: Similar queries have same statement template even for different query parameters sum_latency: Total Latency sum_latency_tooltip: Total execution time for this kind of statement exec_count: '# Exec' exec_count_tooltip: Total execution count for this kind of statement plan_count: '# Plans' plan_count_tooltip: Number of distinct execution plans of this statement in current time range plan_cache_hits: '# Plan Cache Hits' plan_cache_hits_tooltip: Number of times the execution plan cache is hit avg_latency: Mean Latency avg_latency_tooltip: Execution time of single query avg_mem: Mean Memory avg_mem_tooltip: Memory usage of single query max_mem: Max Memory max_mem_tooltip: Maximum memory usage of single query max_mem_arbitration: Max Mem Arbitration max_mem_arbitration_tooltip: Maximum wait time of memory arbitration of single query avg_mem_arbitration: Mean Mem Arbitration avg_mem_arbitration_tooltip: Average wait time of memory arbitration of single query avg_disk: Mean Disk avg_disk_tooltip: Disk usage of single query max_disk: Max Disk max_disk_tooltip: Maximum disk usage of single query index_names: Index Name index_names_tooltip: The name of the used index first_seen: First Seen last_seen: Last Seen sample_user: Execution User sample_user_tooltip: The user that executes the query (sampled) sum_errors: Total Errors sum_warnings: Total Warnings errors_warnings: Errors / Warnings errors_warnings_tooltip: Total Errors and Total Warnings parse_latency: Parse Time parse_latency_tooltip: Time consumed when parsing the query compile_latency: Compile compile_latency_tooltip: Time consumed when optimizing the query wait_time: Coprocessor Wait Time process_time: Coprocessor Execution Time total_process_time: Total Execution Time total_wait_time: Total Wait Time backoff_time: Backoff Retry Time backoff_time_tooltip: The waiting time before retry when a query encounters errors that require a retry get_commit_ts_time: Get Commit Ts Time local_latch_wait_time: Local Latch Wait Time resolve_lock_time: Resolve Lock Time prewrite_time: Prewrite Time commit_time: Commit Time commit_backoff_time: Commit Backoff Retry Time latency: Query query_time2: Query Time query_time2_tooltip: The execution time of a query (due to the parallel execution, it may be significantly smaller than the above time) sum_cop_task_num: Total Coprocessor Tasks avg_processed_keys: Mean Visible Versions Per Query max_processed_keys: Max Visible Versions Per Query avg_total_keys: Mean Meet Versions Per Query avg_total_keys_tooltip: Meet versions contains overwritten or deleted versions max_total_keys: Max Meet Versions Per Query avg_affected_rows: Mean Affected Rows sum_backoff_times: Total Backoff Count avg_write_keys: Mean Written Keys max_write_keys: Max Written Keys avg_write_size: Mean Written Data Size max_write_size: Max Written Data Size avg_prewrite_regions: Mean Prewrite Regions max_prewrite_regions: Max Prewrite Regions avg_txn_retry: Mean Transaction Retries max_txn_retry: Max Transaction Retries digest: Query Template ID digest_tooltip: a.k.a. Query digest schema_name: Execution Database schema_name_tooltip: The database used to execute the query query_sample_text: Query Sample prev_sample_text: Previous Query Sample plan: Execution Plan avg_rocksdb_delete_skipped_count: Mean RocksDB Skipped Deletions avg_rocksdb_delete_skipped_count_tooltip: Total number of deleted (a.k.a. tombstone) key versions that are skipped during iteration (RocksDB delete_skipped_count) max_rocksdb_delete_skipped_count: Max RocksDB Skipped Deletions avg_rocksdb_key_skipped_count: Mean RocksDB Skipped Keys avg_rocksdb_key_skipped_count_tooltip: Total number of keys skipped during iteration (RocksDB key_skipped_count) max_rocksdb_key_skipped_count: Max RocksDB Skipped Keys avg_rocksdb_block_cache_hit_count: Mean RocksDB Block Cache Hits avg_rocksdb_block_cache_hit_count_tooltip: Total number of hits from the block cache (RocksDB block_cache_hit_count) max_rocksdb_block_cache_hit_count: Max RocksDB Block Cache Hits avg_rocksdb_block_read_count: Mean RocksDB Block Reads avg_rocksdb_block_read_count_tooltip: Total number of blocks RocksDB read from file (RocksDB block_read_count) max_rocksdb_block_read_count: Max RocksDB Block Reads avg_rocksdb_block_read_byte: Mean RocksDB FS Read Size avg_rocksdb_block_read_byte_tooltip: Total number of bytes RocksDB read from file (RocksDB block_read_byte) max_rocksdb_block_read_byte: Max RocksDB FS Read Size resource_group: Resource Group resource_group_tooltip: The resource group that the query belongs to avg_ru: Mean RU avg_ru_tooltip: The average number of request units (RU) consumed by the statement max_ru: Max RU max_ru_tooltip: The maximum number of request units (RU) consumed by the statement sum_ru: Total RU sum_ru_tooltip: The total number of request units (RU) consumed by the statement avg_ru_v2: Mean RU V2 avg_ru_v2_tooltip: The average number of RU V2 consumed by the statement sum_ru_v2: Total RU V2 sum_ru_v2_tooltip: The total number of RU V2 consumed by the statement max_ru_v2: Max RU V2 max_ru_v2_tooltip: The maximum number of RU V2 consumed by the statement avg_time_queued_by_rc: Mean RC Wait Time in Queue avg_time_queued_by_rc_tooltip: The average time that the query waits in the resource control's queue (not a wall time) max_time_queued_by_rc: Max RC Wait Time in Queue max_time_queued_by_rc_tooltip: The maximum time that the query waits in the resource control's queue (not a wall time) rc_wait_time_tooltip: 'The total wait time spent in the resource queue (note: {{distro.tikv}} executes requests in parallel so that this is not a wall time)' sum_unpacked_bytes_sent_tikv_total: Total Bytes Sent to TiKV sum_unpacked_bytes_sent_tikv_total_tooltip: The total number of bytes sent to TiKV sum_unpacked_bytes_received_tikv_total: Total Bytes Received from TiKV sum_unpacked_bytes_received_tikv_total_tooltip: The total number of bytes received from TiKV sum_unpacked_bytes_sent_tikv_cross_zone: Total Cross-Zone Bytes Sent to TiKV sum_unpacked_bytes_sent_tikv_cross_zone_tooltip: The total number of bytes sent to TiKV across zones sum_unpacked_bytes_received_tikv_cross_zone: Total Cross-Zone Bytes Received from TiKV sum_unpacked_bytes_received_tikv_cross_zone_tooltip: The total number of bytes received from TiKV across zones sum_unpacked_bytes_sent_tiflash_total: Total Bytes Sent to TiFlash sum_unpacked_bytes_sent_tiflash_total_tooltip: The total number of bytes sent to TiFlash sum_unpacked_bytes_received_tiflash_total: Total Bytes Received from TiFlash sum_unpacked_bytes_received_tiflash_total_tooltip: The total number of bytes received from TiFlash sum_unpacked_bytes_sent_tiflash_cross_zone: Total Cross-Zone Bytes Sent to TiFlash sum_unpacked_bytes_sent_tiflash_cross_zone_tooltip: The total number of bytes sent to TiFlash across zones sum_unpacked_bytes_received_tiflash_cross_zone: Total Cross-Zone Bytes Received from TiFlash sum_unpacked_bytes_received_tiflash_cross_zone_tooltip: The total number of bytes received from TiFlash across zones avg_ia_read_segment_count: Mean IA Read Segments avg_ia_read_segment_count_tooltip: The average number of IA read segments per execution avg_ia_remote_read_segment_size: Mean IA Remote Read Segment Size avg_ia_remote_read_segment_size_tooltip: The average of bytes read from IA remote segments avg_ia_remote_read_segment_wait_time: Mean IA Remote Read Segment Wait Time avg_ia_remote_read_segment_wait_time_tooltip: The average wait time spent reading IA remote segments max_ia_read_segment_count: Max IA Read Segments max_ia_read_segment_count_tooltip: Maximum IA read segment count max_ia_remote_read_segment_size: Max IA Remote Read Segment Size max_ia_remote_read_segment_size_tooltip: Maximum number of bytes read from IA remote segments max_ia_remote_read_segment_wait_time: Max IA Remote Read Segment Wait Time max_ia_remote_read_segment_wait_time_tooltip: Maximum wait time spent reading IA remote segments ia_remote_read_segment_wait_time: IA Remote Read Segment Wait Time ia_remote_read_segment_wait_time_tooltip: The wait time spent reading IA remote segments ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/translations/zh.yaml ================================================ statement: nav_title: SQL 语句分析 pages: detail: head: back: 返回列表 title: SQL 语句信息 plan_bind: title: 执行计划绑定 bound: 已绑定 not_bound: 未绑定 bound_available_tooltip: 计划绑定仅支持可绑定的 SQL 语句,包括 SELECT、DELETE、UPDATE 和 INSERT / REPLACE with SELECT 子查询。 notice: '注意: 此功能不适用于带有子查询的查询、访问 TiFlash 的查询或连接 3 个或更多表的查询。' bound_sql: '绑定这条 SQL' to_plan: '到执行计划' bound_status_desc: 该 SQL 已绑定执行计划 drop_btn_txt: 解绑 bind_btn_txt: 绑定 desc: time_range: 时间范围 plans: note: 该 SQL 模板在选定的时间范围内有多个执行计划,您可以选择查看其中一个或多个执行计划。 title: one_for_all: 执行详情 all: 所有执行计划的执行详情 some: '{{n}} 个执行计划的执行详情' execution: title: 执行计划 text: 文本 table: 表格 visual: 图形 modal_title: 执行计划可视化 tabs: basic: 基本信息 time: 执行时间 copr: Coprocessor 读取 txn: 事务 slow_query: 慢查询 overview: toolbar: schemas: placeholder: 所有数据库 selected: '{{ n }} 数据库' columnTitle: 执行数据库名 resource_groups: placeholder: 所有资源组 selected: '{{ n }} 资源组' columnTitle: 资源组名 statement_types: placeholder: 所有类型 selected: '{{ count }} 类型' columnTitle: SQL 语句类型 select_columns: show_full_sql: 显示完整 SQL 文本 query: 查询 keyword: placeholder: 关键字过滤 time_range_selector: name: 选择时间段 recent: 最近 usual_time_ranges: 常用时间范围 custom_time_ranges: 自定义时间范围 export: 导出 exporting: 正在导出 result_count: '{{ n }} 条结果。' actual_range: 基于设置的时间窗及过期时间,当前显示数据的时间范围: slow_load_info: 数据加载耗时较长,已禁用即时更新。修改查询条件后,您可以手工点击"查询"按钮来发起查询。 settings: title: 设置 disabled_result: title: 该功能未启用 sub_title: | SQL 语句分析功能未启用,因此无法查看历史记录。 您可以修改设置打开该功能后等待新数据收集。 open_setting: 打开设置 close_statement: 关闭 SQL 语句分析功能 close_statement_warning: 确认要关闭该功能吗?关闭后现有历史记录也将被清空! switch: 启用功能 switch_tooltip: 是否启用 SQL 语句分析功能,关闭后将不能使用 SQL 语句分析功能,但能提升少量 {{distro.tidb}} 性能。 max_size: 最大收集 SQL 语句个数 max_size_tooltip: 收集的 SQL 语句个数上限,当实际执行的 SQL 语句种类超过设定个数后最早执行的 SQL 语句信息将被丢弃。若您发现界面上呈现的 SQL 语句信息不完整,建议在内存允许的情况下调大本参数。 refresh_interval: 时间窗大小 (min) refresh_interval_tooltip: 缩小时间窗大小可以使得选择的时间范围更精细。 history_size: 时间窗个数 history_size_tooltip: 扩大时间窗个数可以保留更长时间的执行历史,但也会引入更大的内存开销。 keep_duration: SQL 语句历史保留时长 keep_duration_tooltip: 时间窗大小 × 时间窗个数 internal_query: 收集内部查询 internal_query_tooltip: 开启后 {{distro.tidb}} 内部执行的 SQL 语句信息也将被收集。 actions: save: 保存 close: 确认 cancel: 取消 help: 帮助 help_url: https://docs.pingcap.com/zh/tidb/dev/dashboard-statement-list fields: related_schemas: 数据库 related_schemas_tooltip: SQL 语句涉及的数据库 plan_digest: 执行计划 ID plan_digest_tooltip: 不同的执行计划有不同的 ID digest_text: SQL 模板 digest_text_tooltip: 相似的 SQL 查询即使查询参数不一样也具有相同的 SQL 模板 sum_latency: 累计耗时 sum_latency_tooltip: 该类 SQL 语句在时间段内的累计执行时间 exec_count: 执行次数 exec_count_tooltip: 该类 SQL 语句在时间段内被执行的总次数 plan_count: 计划数 plan_count_tooltip: 该类 SQL 语句在时间段内的不同执行计划数量 plan_cache_hits: 计划缓存命中次数 plan_cache_hits_tooltip: 该类 SQL 语句在时间段内的计划缓存命中次数 avg_latency: 平均耗时 avg_latency_tooltip: 单条 SQL 查询的执行时间 avg_mem: 平均内存 avg_mem_tooltip: 单条 SQL 查询的消耗内存大小 max_mem: 最大内存 max_mem_tooltip: 最大单条 SQL 查询消耗内存大小 max_mem_arbitration: 最大等待内存资源的耗时 max_mem_arbitration_tooltip: 最大单条 SQL 查询等待内存资源的耗时 avg_mem_arbitration: 平均等待内存资源的耗时 avg_mem_arbitration_tooltip: 单条 SQL 查询平均等待内存资源的耗时 avg_disk: 平均磁盘空间 avg_disk_tooltip: 单条 SQL 查询占用的磁盘空间大小 max_disk: 最大磁盘空间 max_disk_tooltip: 最大单条 SQL 查询占用的磁盘空间大小 table_names: 表名 index_names: 索引名 index_names_tooltip: SQL 执行时使用的索引名称 first_seen: 首次出现时间 last_seen: 最后出现时间 sample_user: 执行用户名 sample_user_tooltip: 执行该类 SQL 的用户名,可能存在多个执行用户,仅显示其中某一个 sum_errors: 累计 Error 个数 sum_warnings: 累计 Warning 个数 errors_warnings: 错误 / 警告 errors_warnings_tooltip: 累计错误和警告个数 parse_latency: 解析耗时 parse_latency_tooltip: 解析 SQL 查询的耗时 compile_latency: 优化耗时 compile_latency_tooltip: 编译并优化 SQL 查询的耗时 wait_time: Coprocessor 等待耗时 wait_time_tooltip: SQL 查询在 {{distro.tikv}} Coprocessor 上被等待执行的耗时,单个 SQL 查询所有 Coprocessor 任务累计后计算 process_time: Coprocessor 执行耗时 process_time_tooltip: SQL 查询在 {{distro.tikv}} Coprocessor 上的执行耗时,单个 SQL 查询所有 Coprocessor 任务累计后计算 total_process_time: 所有执行耗时 total_wait_time: 所有等待耗时 backoff_time: 重试等待耗时 backoff_time_tooltip: 单个 SQL 查询所有重试累计后计算 get_commit_ts_time: 取 Commit Ts 耗时 get_commit_ts_time_tooltip: 从 {{distro.pd}} 取递交时间戳(事务号)步骤的耗时 local_latch_wait_time: Local Latch Wait 耗时 local_latch_wait_time_tooltip: 事务在 {{distro.tidb}} 本地与其他事务产生了锁冲突并等待的耗时 resolve_lock_time: Resolve Lock 耗时 resolve_lock_time_tooltip: 事务在 {{distro.tikv}} 与其他事务产生了锁冲突并处理锁冲突的耗时 prewrite_time: Prewrite 阶段耗时 commit_time: Commit 阶段耗时 commit_backoff_time: Commit 重试等待耗时 latency: 执行耗时 query_time2: SQL 执行时间 query_time2_tooltip: 由于存在并行执行,因此 SQL 执行时间可能远小于上述各项时间 sum_cop_task_num: 累计 Coprocessor 请求数 sum_cop_task_num_tooltip: 时间段内该类 SQL 语句累计发送的 Coprocessor 请求数 avg_processed_keys: 单 SQL 查询平均可见版本数 max_processed_keys: 单 SQL 查询最大可见版本数 avg_total_keys: 单 SQL 查询平均遇到版本数 avg_total_keys_tooltip: 含已删除或覆盖但未 GC 的版本 max_total_keys: 单 SQL 查询最大遇到版本数 avg_affected_rows: 平均影响行数 sum_backoff_times: 累计重试次数 sum_backoff_times_tooltip: 这类 SQL 语句遇到需要重试的错误后的总重试次数 avg_write_keys: 平均写入 Key 个数 max_write_keys: 最大写入 Key 个数 avg_write_size: 平均写入数据量 max_write_size: 最大写入数据量 avg_prewrite_regions: Prewrite 平均涉及 Region 个数 max_prewrite_regions: Prewrite 最大涉及 Region 个数 avg_txn_retry: 事务平均重试次数 max_txn_retry: 事务最大重试次数 digest: SQL 模板 ID digest_tooltip: SQL 模板的唯一标识(SQL 指纹) schema_name: 执行数据库 schema_name_tooltip: 执行该 SQL 查询时使用的数据库名称 query_sample_text: SQL 查询样例 prev_sample_text: 前一条 SQL 查询样例 prev_sample_text_tooltip: 一般来说你可能只需要看 COMMIT 语句的前一条 SQL 查询 plan: 执行计划 avg_rocksdb_delete_skipped_count: RocksDB 已删除 Key 平均扫描数 avg_rocksdb_delete_skipped_count_tooltip: RocksDB 扫数据时遇到的已删除 (tombstone) Key 数量 (delete_skipped_count) max_rocksdb_delete_skipped_count: RocksDB 已删除 Key 最大扫描数 avg_rocksdb_key_skipped_count: RocksDB Key 平均扫描数 avg_rocksdb_key_skipped_count_tooltip: RocksDB 扫数据时所有遇到的 Key 数量 (key_skipped_count) max_rocksdb_key_skipped_count: RocksDB Key 最大扫描数 avg_rocksdb_block_cache_hit_count: RocksDB 缓存平均读次数 avg_rocksdb_block_cache_hit_count_tooltip: RocksDB 从 Block Cache 缓存中读数据的次数 (block_cache_hit_count) max_rocksdb_block_cache_hit_count: RocksDB 缓存最大读次数 avg_rocksdb_block_read_count: RocksDB 文件系统平均读次数 avg_rocksdb_block_read_count_tooltip: RocksDB 从文件系统中读数据的次数 (block_read_count) max_rocksdb_block_read_count: RocksDB 文件系统最大读次数 avg_rocksdb_block_read_byte: RocksDB 文件系统平均读数据量 avg_rocksdb_block_read_byte_tooltip: RocksDB 从文件系统中读数据的数据量 (block_read_byte) max_rocksdb_block_read_byte: RocksDB 文件系统最大读数据量 resource_group: 资源组 resource_group_tooltip: SQL 语句所属的资源组 avg_ru: 平均 RU avg_ru_tooltip: Statement 语句的平均 RU max_ru: 最大 RU max_ru_tooltip: 该 Statement 执行中使用过的最大 RU sum_ru: 累积 RU sum_ru_tooltip: 该 Statement 的 RU 累积值 avg_ru_v2: 平均 RU V2 avg_ru_v2_tooltip: Statement 语句的平均 RU V2 sum_ru_v2: 累积 RU V2 sum_ru_v2_tooltip: 该 Statement 的 RU V2 累积值 max_ru_v2: 最大 RU V2 max_ru_v2_tooltip: 该 Statement 执行中使用过的最大 RU V2 avg_time_queued_by_rc: RC 平均等待耗时 avg_time_queued_by_rc_tooltip: SQL 语句在资源组控制队列中平均等待的时间 (Resource Control)(注:{{distro.tikv}} 会并行处理任务,因此该时间不是自然流逝时间) max_time_queued_by_rc: RC 最大等待耗时 max_time_queued_by_rc_tooltip: SQL 语句在资源组控制队列中最大等待的时间 (Resource Control)(注:{{distro.tikv}} 会并行处理任务,因此该时间不是自然流逝时间) rc_wait_time: RC 资源控制等待累积耗时 rc_wait_time_tooltip: SQL 语句在资源组队列中等待的累积时间(注:{{distro.tikv}} 会并行等待任务,因此该时间不是自然流逝时间) sum_unpacked_bytes_sent_tikv_total: 发给 TiKV 的总字节数 sum_unpacked_bytes_sent_tikv_total_tooltip: SQL 语句发送给 TiKV 的总字节数 sum_unpacked_bytes_received_tikv_total: 从 TiKV 接收的总字节数 sum_unpacked_bytes_received_tikv_total_tooltip: 从 TiKV 接收的总字节数 sum_unpacked_bytes_sent_tikv_cross_zone: 跨可用区发给 TiKV 的总字节数 sum_unpacked_bytes_sent_tikv_cross_zone_tooltip: 跨可用区发送给 TiKV 的总字节数 sum_unpacked_bytes_received_tikv_cross_zone: 跨可用区从 TiKV 接收的总字节数 sum_unpacked_bytes_received_tikv_cross_zone_tooltip: 跨可用区从 TiKV 接收的总字节数 sum_unpacked_bytes_sent_tiflash_total: 发给 TiFlash 的总字节数 sum_unpacked_bytes_sent_tiflash_total_tooltip: SQL 语句发送给 TiFlash 的总字节数 sum_unpacked_bytes_received_tiflash_total: 从 TiFlash 接收的总字节数 sum_unpacked_bytes_received_tiflash_total_tooltip: 从 TiFlash 接收的总字节数 sum_unpacked_bytes_sent_tiflash_cross_zone: 跨可用区发给 TiFlash 的总字节数 sum_unpacked_bytes_sent_tiflash_cross_zone_tooltip: 跨可用区发送给 TiFlash 的总字节数 sum_unpacked_bytes_received_tiflash_cross_zone: 跨可用区从 TiFlash 接收的总字节数 sum_unpacked_bytes_received_tiflash_cross_zone_tooltip: 跨可用区从 TiFlash 接收的总字节数 avg_ia_read_segment_count: 平均 IA 读分段数 avg_ia_read_segment_count_tooltip: 按执行次数加权的平均 IA 读分段数 avg_ia_remote_read_segment_size: 平均 IA 远程读分段数据量 avg_ia_remote_read_segment_size_tooltip: 按执行次数加权的平均 IA 远程读分段数据量 avg_ia_remote_read_segment_wait_time: 平均 IA 远程读分段等待时间 avg_ia_remote_read_segment_wait_time_tooltip: 按执行次数加权的平均 IA 远程读分段等待时间 max_ia_read_segment_count: 最大 IA 读分段数 max_ia_read_segment_count_tooltip: 单次执行中 IA 读分段数的最大值 max_ia_remote_read_segment_size: 最大 IA 远程读分段数据量 max_ia_remote_read_segment_size_tooltip: 单次执行中 IA 远程读分段数据量的最大值 max_ia_remote_read_segment_wait_time: 最大 IA 远程读分段等待时间 max_ia_remote_read_segment_wait_time_tooltip: 单次执行中 IA 远程读分段等待时间的最大值 ia_remote_read_segment_wait_time: IA 远程读分段等待时间 ia_remote_read_segment_wait_time_tooltip: IA 远程读分段数据量的最大值 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/utils/tableColumns.tsx ================================================ import { Tooltip } from 'antd' import { max } from 'lodash' import { ColumnActionsMode, IColumn } from 'office-ui-fabric-react/lib/DetailsList' import React from 'react' import { orange, red } from '@ant-design/colors' import { StatementModel } from '@lib/client' import { Bar, Pre } from '@lib/components' import { formatVal, genDerivedBarSources, TableColumnFactory, Column } from '@lib/utils/tableColumnFactory' /////////////////////////////////////// // statements order list in local by fieldName of IColumn // slow query order list in backend by key of IColumn const TRANS_KEY_PREFIX = 'statement.fields' export const derivedFields = { avg_latency: genDerivedBarSources( 'avg_latency', 'max_latency', 'min_latency' ), parse_latency: genDerivedBarSources('avg_parse_latency', 'max_parse_latency'), compile_latency: genDerivedBarSources( 'avg_compile_latency', 'max_compile_latency' ), process_time: genDerivedBarSources( 'avg_cop_process_time', 'max_cop_process_time' ), wait_time: genDerivedBarSources('avg_cop_wait_time', 'max_cop_wait_time'), total_process_time: genDerivedBarSources( 'avg_process_time', 'max_process_time' ), total_wait_time: genDerivedBarSources('avg_wait_time', 'max_wait_time'), backoff_time: genDerivedBarSources('avg_backoff_time', 'max_backoff_time'), avg_write_keys: genDerivedBarSources('avg_write_keys', 'max_write_keys'), avg_processed_keys: genDerivedBarSources( 'avg_processed_keys', 'max_processed_keys' ), avg_total_keys: genDerivedBarSources('avg_total_keys', 'max_total_keys'), prewrite_time: genDerivedBarSources('avg_prewrite_time', 'max_prewrite_time'), commit_time: genDerivedBarSources('avg_commit_time', 'max_commit_time'), get_commit_ts_time: genDerivedBarSources( 'avg_get_commit_ts_time', 'max_get_commit_ts_time' ), commit_backoff_time: genDerivedBarSources( 'avg_commit_backoff_time', 'max_commit_backoff_time' ), resolve_lock_time: genDerivedBarSources( 'avg_resolve_lock_time', 'max_resolve_lock_time' ), local_latch_wait_time: genDerivedBarSources( 'avg_local_latch_wait_time', 'max_local_latch_wait_time' ), avg_write_size: genDerivedBarSources('avg_write_size', 'max_write_size'), avg_prewrite_regions: genDerivedBarSources( 'avg_prewrite_regions', 'max_prewrite_regions' ), avg_txn_retry: genDerivedBarSources('avg_txn_retry', 'max_txn_retry'), avg_mem: genDerivedBarSources('avg_mem', 'max_mem'), avg_disk: genDerivedBarSources('avg_disk', 'max_disk'), avg_mem_arbitration: genDerivedBarSources( 'avg_mem_arbitration', 'max_mem_arbitration' ), sum_errors: ['sum_errors', 'sum_warnings'], related_schemas: ['table_names'], avg_rocksdb_delete_skipped_count: genDerivedBarSources( 'avg_rocksdb_delete_skipped_count', 'max_rocksdb_delete_skipped_count' ), avg_rocksdb_key_skipped_count: genDerivedBarSources( 'avg_rocksdb_key_skipped_count', 'max_rocksdb_key_skipped_count' ), avg_rocksdb_block_cache_hit_count: genDerivedBarSources( 'avg_rocksdb_block_cache_hit_count', 'max_rocksdb_block_cache_hit_count' ), avg_rocksdb_block_read_count: genDerivedBarSources( 'avg_rocksdb_block_read_count', 'max_rocksdb_block_read_count' ), avg_rocksdb_block_read_byte: genDerivedBarSources( 'avg_rocksdb_block_read_byte', 'max_rocksdb_block_read_byte' ), avg_ru: genDerivedBarSources('avg_ru', 'max_ru'), avg_ru_v2: genDerivedBarSources('avg_ru_v2', 'max_ru_v2'), avg_time_queued_by_rc: genDerivedBarSources( 'avg_time_queued_by_rc', 'max_time_queued_by_rc' ), avg_ia_read_segment_count: genDerivedBarSources( 'avg_ia_read_segment_count', 'max_ia_read_segment_count' ), avg_ia_remote_read_segment_size: genDerivedBarSources( 'avg_ia_remote_read_segment_size', 'max_ia_remote_read_segment_size' ), avg_ia_remote_read_segment_wait_time: genDerivedBarSources( 'avg_ia_remote_read_segment_wait_time', 'max_ia_remote_read_segment_wait_time' ) } ////////////////////////////////////////// function avgMinMaxLatencyColumn( tcf: TableColumnFactory, rows?: { max_latency?: number; min_latency?: number; avg_latency?: number }[] ): Column { return tcf.bar.multiple({ sources: derivedFields.avg_latency }, 'ns', rows) } function errorsWarningsColumn( tcf: TableColumnFactory, rows?: { sum_errors?: number; sum_warnings?: number }[] ): Column { const capacity = rows ? max(rows.map((v) => v.sum_errors! + v.sum_warnings!)) ?? 0 : 0 const key = 'sum_errors' return tcf.control({ name: 'errors_warnings', key, fieldName: key, minWidth: 140, maxWidth: 200, columnActionsMode: ColumnActionsMode.clickable, onRender: (rec) => { const errorsFmtVal = formatVal(rec.sum_errors, 'short') const warningsFmtVal = formatVal(rec.sum_warnings, 'short') const tooltipContent = ` Errors: ${errorsFmtVal} Warnings: ${warningsFmtVal}` return ( {tooltipContent.trim()}}> {`${errorsFmtVal} / ${warningsFmtVal}`} ) } }) } //////////////////////////////////////////////// // util methods function avgMaxColumn( tcf: TableColumnFactory, displayTransKey: string, unit: string, rows?: T[] ): Column { return tcf.bar.multiple( { displayTransKey, sources: derivedFields[displayTransKey] }, unit, rows ) } //////////////////////////////////////////////// export function statementColumns( rows: StatementModel[], tableSchemaColumns: string[], showFullSQL?: boolean ): IColumn[] { const tcf = new TableColumnFactory(TRANS_KEY_PREFIX, tableSchemaColumns) return tcf.columns([ evictedRenderColumn( tcf.sqlText('digest_text', showFullSQL, rows).getConfig() ), evictedRenderColumn(tcf.textWithTooltip('digest', rows).getConfig()), tcf.bar.single('sum_latency', 'ns', rows), avgMinMaxLatencyColumn(tcf, rows), tcf.bar.single('exec_count', 'short', rows), tcf.textWithTooltip('plan_count', rows).patchConfig({ minWidth: 100, maxWidth: 300, columnActionsMode: ColumnActionsMode.clickable }), tcf.bar.single('plan_cache_hits', 'short', rows), avgMaxColumn(tcf, 'avg_mem', 'bytes', rows), avgMaxColumn(tcf, 'avg_mem_arbitration', 's', rows), avgMaxColumn(tcf, 'avg_disk', 'bytes', rows), errorsWarningsColumn(tcf, rows), avgMaxColumn(tcf, 'parse_latency', 'ns', rows), avgMaxColumn(tcf, 'compile_latency', 'ns', rows), tcf.bar.single('sum_cop_task_num', 'short', rows), avgMaxColumn(tcf, 'process_time', 'ns', rows), avgMaxColumn(tcf, 'wait_time', 'ns', rows), avgMaxColumn(tcf, 'total_process_time', 'ns', rows), avgMaxColumn(tcf, 'total_wait_time', 'ns', rows), avgMaxColumn(tcf, 'backoff_time', 'ns', rows), avgMaxColumn(tcf, 'avg_write_keys', 'short', rows), avgMaxColumn(tcf, 'avg_processed_keys', 'short', rows), avgMaxColumn(tcf, 'avg_total_keys', 'short', rows), avgMaxColumn(tcf, 'prewrite_time', 'ns', rows), avgMaxColumn(tcf, 'commit_time', 'ns', rows), avgMaxColumn(tcf, 'get_commit_ts_time', 'ns', rows), avgMaxColumn(tcf, 'commit_backoff_time', 'ns', rows), avgMaxColumn(tcf, 'resolve_lock_time', 'ns', rows), avgMaxColumn(tcf, 'local_latch_wait_time', 'ns', rows), avgMaxColumn(tcf, 'avg_write_size', 'bytes', rows), avgMaxColumn(tcf, 'avg_prewrite_regions', 'short', rows), avgMaxColumn(tcf, 'avg_txn_retry', 'short', rows), tcf.bar.single('sum_backoff_times', 'short', rows), tcf.bar.single('avg_affected_rows', 'short', rows), tcf.timestamp('first_seen', rows), tcf.timestamp('last_seen', rows), tcf.textWithTooltip('sample_user', rows), tcf.sqlText('query_sample_text', showFullSQL, rows), tcf.sqlText('prev_sample_text', showFullSQL, rows), tcf.textWithTooltip('schema_name', rows), tcf.textWithTooltip('table_names', rows), tcf.textWithTooltip('index_names', rows), tcf.textWithTooltip('plan_digest', rows), tcf.textWithTooltip('related_schemas', rows).patchConfig({ minWidth: 160, maxWidth: 240 }), // rocksdb avgMaxColumn( tcf, 'avg_rocksdb_delete_skipped_count', 'short', rows ).patchConfig({ minWidth: 220, maxWidth: 250 }), avgMaxColumn( tcf, 'avg_rocksdb_key_skipped_count', 'short', rows ).patchConfig({ minWidth: 220, maxWidth: 250 }), avgMaxColumn( tcf, 'avg_rocksdb_block_cache_hit_count', 'short', rows ).patchConfig({ minWidth: 220, maxWidth: 250 }), avgMaxColumn( tcf, 'avg_rocksdb_block_read_count', 'short', rows ).patchConfig({ minWidth: 220, maxWidth: 250 }), avgMaxColumn(tcf, 'avg_rocksdb_block_read_byte', 'bytes', rows).patchConfig( { minWidth: 220, maxWidth: 250 } ), //resource control tcf.textWithTooltip('resource_group', rows), avgMaxColumn(tcf, 'avg_ru', 'none', rows), tcf.textWithTooltip('sum_ru', rows).patchConfig({ minWidth: 100, maxWidth: 300, columnActionsMode: ColumnActionsMode.clickable }), avgMaxColumn(tcf, 'avg_ru_v2', 'none', rows), tcf.textWithTooltip('sum_ru_v2', rows).patchConfig({ minWidth: 100, maxWidth: 300, columnActionsMode: ColumnActionsMode.clickable }), avgMaxColumn(tcf, 'avg_time_queued_by_rc', 'ns', rows), // Network fields tcf.bar.single('sum_unpacked_bytes_sent_tikv_total', 'bytes', rows), tcf.bar.single('sum_unpacked_bytes_received_tikv_total', 'bytes', rows), tcf.bar.single('sum_unpacked_bytes_sent_tikv_cross_zone', 'bytes', rows), tcf.bar.single( 'sum_unpacked_bytes_received_tikv_cross_zone', 'bytes', rows ), tcf.bar.single('sum_unpacked_bytes_sent_tiflash_total', 'bytes', rows), tcf.bar.single('sum_unpacked_bytes_received_tiflash_total', 'bytes', rows), tcf.bar.single('sum_unpacked_bytes_sent_tiflash_cross_zone', 'bytes', rows), tcf.bar.single( 'sum_unpacked_bytes_received_tiflash_cross_zone', 'bytes', rows ), avgMaxColumn(tcf, 'avg_ia_read_segment_count', 'short', rows), avgMaxColumn(tcf, 'avg_ia_remote_read_segment_size', 'bytes', rows), avgMaxColumn(tcf, 'avg_ia_remote_read_segment_wait_time', 'ns', rows) ]) } export function planColumns(rows: StatementModel[]): IColumn[] { const tcf = new TableColumnFactory(TRANS_KEY_PREFIX) return tcf.columns([ tcf.textWithTooltip('plan_digest').patchConfig({ minWidth: 100, maxWidth: 300 }), tcf.bar.single('sum_latency', 'ns', rows), avgMinMaxLatencyColumn(tcf, rows), tcf.bar.single('exec_count', 'short', rows), avgMaxColumn(tcf, 'avg_mem', 'bytes', rows) ]) } export function evictedRenderColumn(defaultRenderColumn: IColumn): IColumn { return { ...defaultRenderColumn, onRender: (...props) => { const rec = props[0] // the evicted record's digest is empty string return rec.digest ? ( defaultRenderColumn.onRender!(...props) ) : ( Others ) } } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/utils/telemetry.ts ================================================ import { IColumnKeys, TimeRange } from '@lib/components' import { mixpanel } from '@lib/utils/telemetry' export const telemetry = { // list changeTimeRange(t: TimeRange) { mixpanel.track('Statement: Change Time Range Filter', { t }) }, changeDatabases() { mixpanel.track('Statement: Change Databases Filter') }, changeStmtTypes() { mixpanel.track('Statement: Change Stmt Types Filter') }, changeSearchText() { mixpanel.track('Statement: Change Search Text') }, search() { mixpanel.track('Statement: Search') }, changeVisibleColumns(columns: IColumnKeys) { mixpanel.track('Statement: Change Visible Columns', { columns }) }, toggleShowFullSQL(showFull: boolean) { mixpanel.track('Statement: Toggle Show Full SQL', { showFull }) }, openSetting() { mixpanel.track('Statement: Open Setting') }, export() { mixpanel.track('Statement: Export') }, openHelp() { mixpanel.track('Statement: Open Help') }, // detail switchDetailTab(tab: string) { mixpanel.track('Statement: Switch Detail Tab', { tab }) }, clickPlanTabs(tab: string, queryDigest: string) { mixpanel.track('Statement: Plan Tab Clicked', { tab, queryDigest }) }, toggleVisualPlanModal(action: 'open' | 'close') { mixpanel.track('Statement: Visual Plan Modal Toggled', { action }) }, toggleExpandBtnOnNode(nodeName: string) { mixpanel.track('Statement: Node Button Toggled', { nodeName }) }, clickNode(nodeName: string) { mixpanel.track('Statement: Node Clicked', { nodeName }) }, clickTabOnNodeDetail(tab: string) { mixpanel.track('Statement: Detail Tab on Node Clicked', { tab }) } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/utils/useSchemaColumns.ts ================================================ import { useMemo } from 'react' import { useClientRequest } from '@lib/utils/useClientRequest' import { AxiosPromise } from 'axios' import { ReqConfig } from '@lib/types' export function useSchemaColumns( getAvaiableFields: (options?: ReqConfig) => AxiosPromise> ) { const { data, isLoading } = useClientRequest((options) => { return getAvaiableFields(options) }) const schemaColumns = useMemo(() => { if (!data) { return [] } return data.map((d) => d.toLowerCase()) }, [data]) return { schemaColumns, isLoading } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/Statement/utils/useStatementTableController.ts ================================================ import React, { useEffect, useMemo, useState } from 'react' import { useMemoizedFn, useSessionStorageState } from 'ahooks' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { StatementModel } from '@lib/client' import { DEFAULT_TIME_RANGE, IColumnKeys, TimeRange, toTimeRangeValue } from '@lib/components' import { getSelectedFields } from '@lib/utils/tableColumnFactory' import { CacheMgr } from '@lib/utils/useCache' import useOrderState, { IOrderOptions } from '@lib/utils/useOrderState' import useCacheItemIndex from '@lib/utils/useCacheItemIndex' import { derivedFields, statementColumns } from './tableColumns' import { useSchemaColumns } from './useSchemaColumns' import { useChange } from '@lib/utils/useChange' import { IStatementDataSource } from '../context' const SLOW_DATA_LOAD_THRESHOLD = 2000 export const DEF_STMT_COLUMN_KEYS: IColumnKeys = { digest_text: true, sum_latency: true, avg_latency: true, exec_count: true, plan_count: true } const QUERY_OPTIONS = 'statement.query_options' const DEF_ORDER_OPTIONS: IOrderOptions = { orderBy: 'sum_latency', desc: true } interface RuntimeCacheEntity { data: IStatementList isDataLoadedSlowly: boolean } export interface IStatementQueryOptions { visibleColumnKeys: IColumnKeys timeRange: TimeRange schemas: string[] groups: string[] stmtTypes: string[] searchText: string } export interface IStatementList { list: StatementModel[] timeRange: [number, number] // Useful for sending detail requests } export const DEF_STMT_QUERY_OPTIONS: IStatementQueryOptions = { visibleColumnKeys: DEF_STMT_COLUMN_KEYS, timeRange: DEFAULT_TIME_RANGE, schemas: [], groups: [], stmtTypes: [], searchText: '' } function useQueryOptions( initial?: IStatementQueryOptions, persistInSession: boolean = true ) { const [memoryQueryOptions, setMemoryQueryOptions] = useState( initial || DEF_STMT_QUERY_OPTIONS ) const [sessionQueryOptions, setSessionQueryOptions] = useSessionStorageState( QUERY_OPTIONS, { defaultValue: initial || DEF_STMT_QUERY_OPTIONS } ) const queryOptions = persistInSession ? sessionQueryOptions : memoryQueryOptions const setQueryOptions = useMemoizedFn( (value: React.SetStateAction) => { if (persistInSession) { // as any is a workaround for https://github.com/alibaba/hooks/issues/1582 setSessionQueryOptions(value as any) } else { setMemoryQueryOptions(value) } } ) return { queryOptions, setQueryOptions } } export interface IStatementTableControllerOpts { cacheMgr?: CacheMgr showFullSQL?: boolean fetchSchemas?: boolean fetchGroups?: boolean fetchConfig?: boolean initialQueryOptions?: IStatementQueryOptions persistQueryInSession?: boolean ds: IStatementDataSource } export interface IStatementTableController { queryOptions: IStatementQueryOptions setQueryOptions: (value: React.SetStateAction) => void // Updating query options will result in a refresh orderOptions: IOrderOptions changeOrder: (orderBy: string, desc: boolean) => void resetOrder: () => void isEnabled: boolean // returned from backend isLoading: boolean data?: IStatementList isDataLoadedSlowly: boolean | null // SLOW_DATA_LOAD_THRESHOLD. NULL = Unknown allSchemas: string[] allGroups: string[] allStmtTypes: string[] errors: Error[] availableColumnsInTable: IColumn[] // returned from backend saveClickedItemIndex: (idx: number) => void getClickedItemIndex: () => number } export default function useStatementTableController({ cacheMgr, showFullSQL = false, fetchSchemas = true, fetchGroups = true, fetchConfig = true, initialQueryOptions, persistQueryInSession = true, ds }: IStatementTableControllerOpts): IStatementTableController { const { orderOptions, changeOrder } = useOrderState( 'statement', persistQueryInSession, DEF_ORDER_OPTIONS ) function resetOrder() { changeOrder(DEF_ORDER_OPTIONS.orderBy, DEF_ORDER_OPTIONS.desc) } const { queryOptions, setQueryOptions } = useQueryOptions( initialQueryOptions, persistQueryInSession ) const [isEnabled, setEnabled] = useState(true) const [allSchemas, setAllSchemas] = useState([]) const [allGroups, setAllGroups] = useState([]) const [allStmtTypes, setAllStmtTypes] = useState([]) const [isOptionsLoading, setOptionsLoading] = useState(true) const [data, setData] = useState(undefined) const [isDataLoading, setDataLoading] = useState(true) const [isDataLoadedSlowly, setDataLoadedSlowly] = useState( null ) const [errors, setErrors] = useState([]) const { schemaColumns, isLoading: isColumnsLoading } = useSchemaColumns( ds.statementsAvailableFieldsGet ) // By PR https://github.com/pingcap/tidb-dashboard/pull/1234 (feat: improve statement) // which brings in v2022.05.16.1 and PD >=5.4.2, >=6.1.0 // The statement API logic changes a bit // related code: https://github.com/pingcap/tidb-dashboard/pull/1234/files#diff-4bebd6011f602ac611ee19697803dc09877df197bf0176d1f27f84133b15e68bR54 // The new UI can't work with the old tidb-dashboard backend API well // So we try to make the new UI compatible with the old tidb-dashboard backend // By enlarging the selected time range with window size const [windowSize, setWindowSize] = useState(0) // assume the backend is old at first // then update it after the first request const [oldBackend, setOldBackend] = useState(true) // check old or new backend // the new backend removed the `/statements/time_ranges` API // so if get 404, then it's the new backend // else the old backend useEffect(() => { async function queryTimeRanges() { try { await ds.statementsTimeRangesGet({ handleError: 'custom' }) } catch (e) { if ((e as any).response?.status === 404) { setOldBackend(false) } } } queryTimeRanges() }, [ds]) // Reload these options when sending a new request. useChange(() => { async function queryStatementStatus() { if (!fetchConfig) { return } try { const res = await ds.statementsConfigGet({ handleError: 'custom' }) setEnabled(res?.data.enable!) setWindowSize(res?.data?.refresh_interval ?? 0) } catch (e) { setErrors((prev) => prev.concat(e)) } } async function querySchemas() { if (!fetchSchemas) { return } try { const res = await ds.getDatabaseList(0, 0, { handleError: 'custom' }) setAllSchemas(res?.data || []) } catch (e) { setErrors((prev) => prev.concat(e)) } } async function queryGroups() { if (!fetchGroups) { return } try { const res = await ds.infoListResourceGroupNames({ handleError: 'custom' }) setAllGroups(res?.data || []) } catch (e) { setErrors((prev) => prev.concat(e as Error)) } } async function queryStmtTypes() { try { const res = await ds.statementsStmtTypesGet({ handleError: 'custom' }) const stmtTypes = (res?.data || []).sort() setAllStmtTypes(stmtTypes) } catch (e) { setErrors((prev) => prev.concat(e)) } } async function doRequest() { setOptionsLoading(true) try { await Promise.all([ queryStatementStatus(), querySchemas(), queryGroups(), queryStmtTypes() ]) } finally { setOptionsLoading(false) } } doRequest() }, [queryOptions]) useChange(() => { async function queryStatementList() { // Try cache if options are unchanged. // Note: When clicking "Query" manually, cache will be cleared before reach here. So that it // will always send a request without looking up in the cache. // The cache key is built over queryOptions, instead of evaluated one. // So that when passing in same relative times options (e.g. Recent 15min) // the cache can be reused. const cacheKey = JSON.stringify(queryOptions) { const cache = cacheMgr?.get(cacheKey) if (cache) { const cacheCloned = JSON.parse( JSON.stringify(cache) ) as RuntimeCacheEntity setData(cacheCloned.data) setDataLoadedSlowly(cacheCloned.isDataLoadedSlowly) setDataLoading(false) return } } // May be caused by visibleColumnKeys is empty (when available columns are not yet loaded) // In this case, we don't send any requests. const actualVisibleColumnKeys = getSelectedFields( queryOptions.visibleColumnKeys, derivedFields ).join(',') if (actualVisibleColumnKeys.length === 0) { return } const requestBeginAt = performance.now() setDataLoading(true) const timeRange = toTimeRangeValue(queryOptions.timeRange) // enlarge the time range automatically for old tidb-dashboard backend if (oldBackend) { timeRange[0] -= windowSize timeRange[1] += windowSize } try { console.log('queryOptions', queryOptions) const res = await ds.statementsListGet( timeRange[0], timeRange[1], actualVisibleColumnKeys, queryOptions.schemas, queryOptions.groups, queryOptions.stmtTypes, queryOptions.searchText, { handleError: 'custom' } ) const data = { list: res?.data || [], timeRange } setData(data) setErrors([]) const elapsed = performance.now() - requestBeginAt const isLoadSlow = elapsed >= SLOW_DATA_LOAD_THRESHOLD setDataLoadedSlowly(isLoadSlow) const cacheEntity: RuntimeCacheEntity = { data, isDataLoadedSlowly: isLoadSlow } cacheMgr?.set(cacheKey, cacheEntity) } catch (e) { setData(undefined) setErrors((prev) => prev.concat(e)) } finally { setDataLoading(false) } } queryStatementList() }, [queryOptions, windowSize, oldBackend]) const availableColumnsInTable = useMemo( () => statementColumns(data?.list ?? [], schemaColumns, showFullSQL), [data, schemaColumns, showFullSQL] ) const { saveClickedItemIndex, getClickedItemIndex } = useCacheItemIndex(cacheMgr) return { queryOptions, setQueryOptions, orderOptions, changeOrder, resetOrder, isEnabled, isLoading: isColumnsLoading || isDataLoading || isOptionsLoading, data, isDataLoadedSlowly, allSchemas, allGroups, allStmtTypes, errors, availableColumnsInTable, saveClickedItemIndex, getClickedItemIndex } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SystemReport/components/ReportHistory.tsx ================================================ import { Badge } from 'antd' import dayjs from 'dayjs' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import React, { useContext, useMemo } from 'react' import { useTranslation, TFunction } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { useMemoizedFn } from 'ahooks' import { DiagnoseReport } from '@lib/client' import { CardTable, DateTime } from '@lib/components' import openLink from '@lib/utils/openLink' import { useClientRequest } from '@lib/utils/useClientRequest' import { SystemReportContext } from '../context' const tableColumns = (t: TFunction): IColumn[] => [ { name: t('system_report.list_table.id'), key: 'id', fieldName: 'id', minWidth: 200, maxWidth: 350 }, { name: t('system_report.list_table.report_create_time'), key: 'created_at', minWidth: 100, maxWidth: 200, onRender: (rec: DiagnoseReport) => ( ) }, { name: t('system_report.list_table.status'), key: 'progress', minWidth: 100, maxWidth: 150, onRender: (rec: DiagnoseReport) => { if (rec.progress! < 100) { return ( ) } else { return ( ) } } }, { name: t('system_report.list_table.range'), key: 'start_time', minWidth: 200, maxWidth: 350, onRender: (rec: DiagnoseReport) => { return ( {' '} ~{' '} ) } }, { name: t('system_report.list_table.compare_range'), key: 'compare_start_time', minWidth: 200, maxWidth: 350, onRender: (rec: DiagnoseReport) => rec.compare_start_time && ( {' '} ~{' '} ) } ] export default function ReportHistory() { const ctx = useContext(SystemReportContext) const navigate = useNavigate() const { t } = useTranslation() const { data, isLoading, error } = useClientRequest( ctx!.ds.diagnoseReportsGet ) const columns = useMemo(() => tableColumns(t), [t]) const handleRowClick = useMemoizedFn( (rec, _idx, ev: React.MouseEvent) => { openLink(`/system_report/detail?id=${rec.id}`, ev, navigate) } ) return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SystemReport/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { DiagnoseReport, DiagnoseGenerateReportRequest, DiagnoseGenerateMetricsRelationRequest } from '@lib/client' import { IContextConfig, ReqConfig } from '@lib/types' export interface ISystemReportDataSource { diagnoseReportsGet(options?: ReqConfig): AxiosPromise> diagnoseReportsPost( request: DiagnoseGenerateReportRequest, options?: ReqConfig ): AxiosPromise diagnoseGenerateMetricsRelationship( request: DiagnoseGenerateMetricsRelationRequest, options?: ReqConfig ): AxiosPromise diagnoseReportsIdStatusGet( id: string, options?: ReqConfig ): AxiosPromise } export interface ISystemReportConfig extends IContextConfig { publicPathBase: string fullReportLink(reportId: string): string } export interface ISystemReportContext { ds: ISystemReportDataSource cfg: ISystemReportConfig } export const SystemReportContext = createContext( null ) export const SystemReportProvider = SystemReportContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SystemReport/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router, Route, Routes } from 'react-router-dom' import { ParamsPageWrapper, Root } from '@lib/components' import { ReportGenerator, ReportStatus } from './pages' import translations from './translations' import { addTranslations } from '@lib/utils/i18n' import { SystemReportContext } from './context' import { useLocationChange } from '@lib/hooks/useLocationChange' addTranslations(translations) function AppRoutes() { useLocationChange() return ( } /> } /> ) } const App = () => { const ctx = useContext(SystemReportContext) if (ctx === null) { throw new Error('SystemReportÇontext must not be null') } return ( ) } export default App export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SystemReport/pages/ReportGenerator.tsx ================================================ import { Button, Form, Input, InputNumber, Select, Switch, Modal } from 'antd' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import React, { useState, useCallback, useContext } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { getValueFormat } from '@baurine/grafana-value-formats' import { Card, Pre, DatePicker } from '@lib/components' import ReportHistory from '../components/ReportHistory' import { ISystemReportDataSource, SystemReportContext } from '../context' import { useIsWriteable } from '@lib/utils' const useFinishHandler = ( navigate, genReport: ISystemReportDataSource['diagnoseReportsPost'] ) => { return async (fieldsValue) => { const start_time = fieldsValue['rangeBegin'].unix() let range_duration = fieldsValue['rangeDuration'] if (fieldsValue['rangeDuration'] === 0) { range_duration = fieldsValue['rangeDurationCustom'] } const is_compare = fieldsValue['isCompare'] const compare_range_begin = fieldsValue['compareRangeBegin'] const end_time = start_time + range_duration * 60 const compare_start_time = is_compare ? compare_range_begin.unix() : 0 const compare_end_time = is_compare ? compare_start_time + range_duration * 60 : 0 const res = await genReport({ start_time, end_time, compare_start_time, compare_end_time }) navigate(`/system_report/detail?id=${res.data}`) } } const DURATIONS = [5, 10, 30, 60, 24 * 60] export default function ReportGenerator() { const ctx = useContext(SystemReportContext) const { t } = useTranslation() const navigate = useNavigate() const handleFinish = useFinishHandler(navigate, ctx!.ds.diagnoseReportsPost) const isWriteable = useIsWriteable() const [form] = Form.useForm() const [isGenerateRelationPosting, setGenerateRelationPosting] = useState(false) const handleMetricsRelation = useCallback(async () => { try { await form.validateFields() } catch (e) { return } const fieldsValue = form.getFieldsValue() const start_time = fieldsValue['rangeBegin'].unix() let range_duration = fieldsValue['rangeDuration'] if (fieldsValue['rangeDuration'] === 0) { range_duration = fieldsValue['rangeDurationCustom'] } const end_time = start_time + range_duration * 60 try { setGenerateRelationPosting(true) const resp = await ctx!.ds.diagnoseGenerateMetricsRelationship({ start_time, end_time, type: 'sum' }) Modal.success({ title: t('system_report.metrics_relation.success.title'), okText: t('system_report.metrics_relation.success.button'), okButtonProps: { target: '_blank', href: `${ctx!.cfg.apiPathBase}/diagnose/metrics_relation/view?token=` + encodeURIComponent(resp.data) } }) } catch (e) { const err = e as any Modal.error({ title: 'Error', content:
{err?.response?.data?.message ?? err.message}
}) } setGenerateRelationPosting(false) }, [t, form, ctx]) return (
prev.rangeDuration !== cur.rangeDuration } > {({ getFieldValue }) => { return ( getFieldValue('rangeDuration') === 0 && ( `${value} min`} parser={(value) => parseInt(value?.replace(/[^\d]/g, '') || '') } style={{ width: 120 }} /> ) ) }} prev.isCompare !== cur.isCompare} > {({ getFieldValue }) => { return ( getFieldValue('isCompare') && ( ) ) }}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SystemReport/pages/ReportStatus.tsx ================================================ import { Button, Descriptions, Progress } from 'antd' import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { ArrowLeftOutlined } from '@ant-design/icons' import { AnimatedSkeleton, DateTime, ErrorBar, Head } from '@lib/components' import { useClientRequestWithPolling } from '@lib/utils/useClientRequest' import useQueryParams from '@lib/utils/useQueryParams' import { SystemReportContext } from '../context' function ReportStatus() { const ctx = useContext(SystemReportContext) const { id } = useQueryParams() const { t } = useTranslation() const { data: report, isLoading, error } = useClientRequestWithPolling( (reqConfig) => ctx!.ds.diagnoseReportsIdStatusGet(id, reqConfig), { shouldPoll: (data) => data?.progress! < 100 } ) return ( {t('system_report.status.head.back')} } titleExtra={ report && ( ) } > {error && } {report && ( {report.compare_start_time && ( )} )} ) } export default ReportStatus ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SystemReport/pages/index.ts ================================================ import ReportGenerator from './ReportGenerator' import ReportStatus from './ReportStatus' export { ReportGenerator, ReportStatus } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SystemReport/translations/en.yaml ================================================ system_report: nav_title: Cluster Diagnostics generate: title: New Diagnostic Report range_begin: Range Start Time range_duration: Range Duration is_compare: Compare by Baseline compare_range_begin: Baseline Range Start Time submit: Start metrics_relation: Generate Metrics Relation list_table: id: Report ID report_create_time: Diagnose At status: Status status_running: Running status_finish: Finish range: Range compare_range: Baseline Range status: head: title: Diagnostic Status back: New Diagnostic Report view: View Full Report range_begin: Range Start Time range_end: Range End Time baseline_begin: Baseline Range Start Time progress: Progress time_duration: custom: Custom metrics_relation: success: title: Generate metrics relation graph successfully button: View Graph ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SystemReport/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/SystemReport/translations/zh.yaml ================================================ system_report: nav_title: 集群诊断 generate: title: 生成诊断报告 range_begin: 区间起始时间 range_duration: 区间长度 is_compare: 与基线区间对比 compare_range_begin: 基线区间起始时间 submit: 开始 metrics_relation: 生成监控关系图 list_table: id: 报告 ID report_create_time: 诊断时间 status: 状态 status_running: 诊断中 status_finish: 完成 range: 诊断区间 compare_range: 对比区间 status: head: title: 诊断状态 back: 生成诊断报告 view: 查看完整诊断报告 range_begin: 区间起始时间 range_end: 区间结束时间 baseline_begin: 基线区间起始时间 progress: 生成进度 time_duration: custom: 自定义 metrics_relation: success: title: 监控耗时关系图生成成功 button: 查看关系图 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/components/Filter/InstanceSelect.tsx ================================================ import React, { useMemo } from 'react' import { Select } from 'antd' import { TopsqlInstanceItem } from '@lib/client' import commonStyles from './common.module.less' interface InstanceGroup { name: string instances: TopsqlInstanceItem[] } export interface InstanceSelectProps { value: TopsqlInstanceItem | null onChange: (instance: TopsqlInstanceItem) => void instances: TopsqlInstanceItem[] disabled?: boolean onDropdownVisibleChange?: (visible: boolean) => void } const splitter = ' - ' const combineSelectValue = (item: TopsqlInstanceItem | null) => { if (!item) { return '' } return `${item.instance_type}${splitter}${item.instance}` } const splitSelectValue = (v: string): TopsqlInstanceItem => { const [instance_type, instance] = v.split(splitter) return { instance, instance_type } } export function InstanceSelect({ value, onChange, instances, disabled = false, ...otherProps }: InstanceSelectProps) { const instanceGroups: InstanceGroup[] = useMemo(() => { if (!instances) { return [] } // Depend on the ordered instances return instances.reduce((prev, instance) => { const lastGroup = prev[prev.length - 1] if (!lastGroup || lastGroup.name !== instance.instance_type) { prev.push({ name: instance.instance_type!, instances: [instance] }) return prev } lastGroup.instances.push(instance) return prev }, [] as InstanceGroup[]) }, [instances]) return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/components/Filter/common.module.less ================================================ // A hack displays content in selection area, but not in options .select_option { .hide { display: none; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/components/Filter/index.ts ================================================ export * from './InstanceSelect' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { RestErrorResponse, TopsqlEditableConfig, TopsqlInstanceResponse, TopsqlSummaryResponse } from '@lib/client' import { ReqConfig } from '@lib/types' export interface TopsqlTikvNetworkIoCollectionConfig { /** * Whether enable TiKV network IO collection (resource-metering.enable-network-io-collection) */ enable: boolean /** * Whether values are not identical across TiKV nodes */ is_multi_value?: boolean } export interface TopsqlTikvNetworkIoCollectionUpdateResponse { warnings: RestErrorResponse[] } export interface ITopSQLDataSource { topsqlConfigGet(options?: ReqConfig): AxiosPromise topsqlConfigPost( request: TopsqlEditableConfig, options?: ReqConfig ): AxiosPromise topsqlTikvNetworkIoCollectionGet( options?: ReqConfig ): AxiosPromise topsqlTikvNetworkIoCollectionPost( request: TopsqlTikvNetworkIoCollectionConfig, options?: ReqConfig ): AxiosPromise topsqlInstancesGet( end?: string, start?: string, dataSource?: string, options?: ReqConfig ): AxiosPromise topsqlSummaryGet( end?: string, groupBy?: string, instance?: string, instanceType?: string, orderBy?: string, start?: string, top?: string, window?: string, dataSource?: string, options?: ReqConfig ): AxiosPromise } export interface ITopSQLConfig { checkNgm: boolean showSetting: boolean // to limit the time range picker range timeRangeSelector?: { recentSeconds: number[] customAbsoluteRangePicker: boolean timeRangeLimit?: number } autoRefresh?: boolean // for clinic orgName?: string clusterName?: string userName?: string showSearchInStatements?: boolean showLimit?: boolean showGroupBy?: boolean showGroupByRegion?: boolean showOrderBy?: boolean minWindowInterval?: number dataSource?: string } export interface ITopSQLContext { ds: ITopSQLDataSource cfg: ITopSQLConfig } export const TopSQLContext = createContext(null) export const TopSQLProvider = TopSQLContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/index.tsx ================================================ import React, { useContext } from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import { Root, NgmNotStartedGuard } from '@lib/components' import { addTranslations } from '@lib/utils/i18n' import { useLocationChange } from '@lib/hooks/useLocationChange' import { TopSQLList } from './pages/List/List' import { TopSQLContext } from './context' import translations from './translations' addTranslations(translations) function AppRoutes() { const ctx = useContext(TopSQLContext) useLocationChange() return ( ) : ( ) } /> ) } export default function () { const ctx = useContext(TopSQLContext) if (ctx === null) { throw new Error('TopSQLContext must not be null') } return ( ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/List.module.less ================================================ @import 'antd/es/style/themes/default.less'; .container { display: flex; height: 100%; flex-direction: column; .chart_container { margin: 24px 24px 24px 48px; height: 200px; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/List.tsx ================================================ import { BrushEndListener, BrushEvent } from '@elastic/charts' import React, { useCallback, useContext, useEffect, useRef, useState, useMemo } from 'react' import { Select, Space, Button, Spin, Alert, Tooltip, Drawer, Result } from 'antd' import { LoadingOutlined, QuestionCircleOutlined, SettingOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { useMount, useSessionStorage } from 'react-use' import { useMemoizedFn } from 'ahooks' import { sortBy } from 'lodash' import formatSql from '@lib/utils/sqlFormatter' import { TopsqlInstanceItem, TopsqlSummaryByItem, TopsqlSummaryItem, TopsqlSummaryResponse } from '@lib/client' import { Card, toTimeRangeValue as _toTimeRangeValue, Toolbar, AutoRefreshButton, TimeRange, fromTimeRangeValue, TimeRangeValue, LimitTimeRange } from '@lib/components' import { useClientRequest } from '@lib/utils/useClientRequest' import { telemetry } from '../../utils/telemetry' import { InstanceSelect } from '../../components/Filter' import styles from './List.module.less' import { ListTable } from './ListTable' import { ListChart } from './ListChart' import { SettingsForm } from './SettingsForm' import { onLegendItemOver, onLegendItemOut } from './legendAction' import { InstanceType } from './ListDetail/ListDetailTable' import { isDistro } from '@lib/utils/distro' import { TopSQLContext } from '../../context' import { useURLTimeRange } from '@lib/hooks/useURLTimeRange' import { useQueryParams } from '@lib/hooks/useQueryParams' const { Option } = Select const CHART_BAR_WIDTH = 8 const RECENT_RANGE_OFFSET = -60 const LIMITS = [5, 20, 100] export enum AggLevel { Query = 'query', Table = 'table', Schema = 'db', Region = 'region' } export enum OrderBy { CpuTime = 'cpu', NetworkBytes = 'network', LogicalIoBytes = 'logical_io' } const formatLabel = (item: AggLevel): string => { if (item === AggLevel.Schema) return 'DB' // Special case for 'db' return item.charAt(0).toUpperCase() + item.slice(1) // Capitalize first letter } const formatOrderByLabel = (item: OrderBy): string => { const labels: Record = { [OrderBy.CpuTime]: 'CPU', [OrderBy.NetworkBytes]: 'Network', [OrderBy.LogicalIoBytes]: 'Logical IO' } return labels[item] || item } const GROUP = [AggLevel.Query, AggLevel.Table, AggLevel.Schema, AggLevel.Region] const toTimeRangeValue: typeof _toTimeRangeValue = (v) => { return _toTimeRangeValue(v, v?.type === 'recent' ? RECENT_RANGE_OFFSET : 0) } export function TopSQLList() { const ctx = useContext(TopSQLContext) const { t } = useTranslation() const canOpenSettings = ctx?.cfg.showSetting !== false const { topSQLConfig, isConfigLoading, updateConfig, haveHistoryData } = useTopSQLConfig() const [showSettings, setShowSettings] = useState(false) const [instance, setInstance] = useSessionStorage( 'topsql.instance', null ) const { queryParams, setQueryParams } = useQueryParams<{ instance: string }>({ instance: '' }) const { timeRange, setTimeRange } = useURLTimeRange() const [limit, setLimit] = useState(5) const [groupBy, setGroupBy] = useState(AggLevel.Query) const [orderBy, setOrderBy] = useState(OrderBy.CpuTime) const [timeWindowSize, setTimeWindowSize] = useState(0) const containerRef = useRef(null) const computeTimeWindowSize = useMemoizedFn( ([min, max]: TimeRangeValue): number => { const screenWidth = containerRef.current?.offsetWidth || 0 const windowSize = Math.ceil( (CHART_BAR_WIDTH * (max - min)) / screenWidth ) const finalWindowSize = ctx?.cfg.minWindowInterval !== undefined ? Math.max(windowSize, ctx.cfg.minWindowInterval) : windowSize setTimeWindowSize(finalWindowSize) return finalWindowSize } ) const { topSQLData, isLoading: isDataLoading, updateTopSQLData } = useTopSQLData( instance, timeRange, limit, groupBy, orderBy, computeTimeWindowSize ) const isLoading = isConfigLoading || isDataLoading const { instances, isLoading: isInstancesLoading, fetchInstances } = useInstances(timeRange) const { data: tikvNetworkIoCollection, isLoading: isTikvNetworkIoCollectionLoading, sendRequest: refreshTikvNetworkIoCollection } = useClientRequest(ctx!.ds.topsqlTikvNetworkIoCollectionGet, { immediate: false }) const handleBrushEnd: BrushEndListener = useCallback( (v: BrushEvent) => { if (!v.x) { return } let value: [number, number] const tr = v.x.map((d) => d / 1000) const delta = tr[1] - tr[0] if (delta < 60) { const offset = Math.floor(delta / 2) value = [ Math.ceil(tr[0] + offset - 30), Math.floor(tr[1] - offset + 30) ] } else { value = [Math.ceil(tr[0]), Math.floor(tr[1])] } setTimeRange(fromTimeRangeValue(value)) telemetry.dndZoomIn(value) }, [setTimeRange] ) const fetchInstancesAndSelectInstance = useMemoizedFn(async () => { const instances = await fetchInstances(timeRange) const instanceFromURL = queryParams.instance if (instanceFromURL) { const instance = instances.find( (instance) => instance.instance === instanceFromURL ) if (instance) { setInstance(instance) return } } // Select the first instance if there not instance selected if (!!instance) { return } setInstance(instances[0]) }) useMount(() => { fetchInstancesAndSelectInstance() }) const chartRef = useRef(null) // only for clinic const clusterInfo = useMemo(() => { const infos: string[] = [] if (ctx?.cfg.orgName) { infos.push(`Org: ${ctx?.cfg.orgName}`) } if (ctx?.cfg.clusterName) { infos.push(`Cluster: ${ctx?.cfg.clusterName}`) } return infos.join(' | ') }, [ctx?.cfg.orgName, ctx?.cfg.clusterName]) const shouldCheckNetworkIoCollection = canOpenSettings && instance?.instance_type === 'tikv' && (orderBy === OrderBy.NetworkBytes || orderBy === OrderBy.LogicalIoBytes || groupBy === AggLevel.Region) const shouldShowNetworkIoTip = shouldCheckNetworkIoCollection && !isTikvNetworkIoCollectionLoading && (tikvNetworkIoCollection?.enable === false || tikvNetworkIoCollection?.is_multi_value === true) const networkIoTipBody = tikvNetworkIoCollection?.is_multi_value === true ? t('topsql.tikv_network_io_collection_tip.body_partial') : t('topsql.tikv_network_io_collection_tip.body') useEffect(() => { if (shouldCheckNetworkIoCollection) { refreshTikvNetworkIoCollection() } }, [shouldCheckNetworkIoCollection, refreshTikvNetworkIoCollection]) return ( <>
{/* Show "not enabled" Alert when there are historical data */} {!isConfigLoading && !topSQLConfig?.enable && haveHistoryData && ( {t(`topsql.alert_header.body`)} {` `} { setShowSettings(true) telemetry.clickSettings('bannerTips') }} > {t('topsql.alert_header.settings')} ) : ( t(`topsql.alert_header.body`) ) } type="info" showIcon /> )} {clusterInfo && (
{clusterInfo}
)} { setInstance(inst) if (!!inst?.instance) { setQueryParams({ instance: inst.instance }) } if (inst) { telemetry.finishSelectInstance(inst?.instance_type!) } // only group by sql when instance is not tikv if (inst?.instance_type !== 'tikv') { setGroupBy(AggLevel.Query) } // Reset orderBy if current selection is not supported by new instance type if ( inst?.instance_type !== 'tikv' && orderBy === OrderBy.LogicalIoBytes ) { setOrderBy(OrderBy.CpuTime) } }} instances={instances} disabled={isLoading || isInstancesLoading} onDropdownVisibleChange={(open) => open && telemetry.openSelectInstance() } /> { setTimeRange(v) telemetry.selectTimeRange(v) }} onZoomOutClick={(start, end) => telemetry.clickZoomOut([start, end]) } disabled={isLoading} /> {ctx?.cfg.showLimit && ( )} {ctx?.cfg.showGroupBy && instance?.instance_type === 'tikv' && ( )} {ctx?.cfg.showOrderBy && instance && ( )} { await fetchInstancesAndSelectInstance() updateTopSQLData(instance, timeRange, limit) }} /> {isLoading && ( } /> )} {canOpenSettings && ( { setShowSettings(true) telemetry.clickSettings('settingIcon') }} /> )} {!isDistro() && ( { window.open(t('topsql.settings.help_url'), '_blank') }} /> )}
{shouldShowNetworkIoTip && ( {networkIoTipBody} {` `} { setShowSettings(true) telemetry.clickSettings('settingIcon') }} > {t('topsql.tikv_network_io_collection_tip.action')} } type="warning" showIcon /> )} {/* Show "not enabled" Result when there are no historical data */} {!isConfigLoading && !topSQLConfig?.enable && !haveHistoryData ? ( {canOpenSettings && ( )} {!isDistro() && ( )} ) : undefined } /> ) : ( <>
{Boolean(topSQLData?.length) && ( onLegendItemOver(chartRef.current, key) } onRowLeave={() => onLegendItemOut(chartRef.current)} topN={limit} instanceType={instance?.instance_type as InstanceType} data={topSQLData} groupBy={groupBy} orderBy={orderBy} timeRange={timeRange} /> )} {Boolean(!topSQLData?.length && timeRange.type === 'recent') && (

{t('topsql.table.description_no_recent_data')}

)} )}
{canOpenSettings && ( setShowSettings(false)} destroyOnClose={true} > setShowSettings(false)} onConfigUpdated={() => { updateConfig() refreshTikvNetworkIoCollection() }} /> )} ) } const useTopSQLData = ( instance: TopsqlInstanceItem | null, timeRange: TimeRange, limit: number, groupBy: string, orderBy: OrderBy, computeTimeWindowSize: (ts: TimeRangeValue) => number ) => { const ctx = useContext(TopSQLContext) const [topSQLData, setTopSQLData] = useState([]) const [isLoading, setIsLoading] = useState(false) const updateTopSQLData = useMemoizedFn( async ( _instance: TopsqlInstanceItem | null, _timeRange: TimeRange, _limit: number | 5 ) => { if (!_instance) { return } let dataResp: TopsqlSummaryResponse const ts = toTimeRangeValue(_timeRange) const timeWindowSize = computeTimeWindowSize(ts) const [start, end] = ts try { setIsLoading(true) const resp = await ctx!.ds.topsqlSummaryGet( String(end), ctx?.cfg.showGroupBy === true ? _instance.instance_type === 'tidb' ? AggLevel.Query : groupBy : undefined, _instance.instance, _instance.instance_type, ctx?.cfg.showOrderBy === true ? orderBy : undefined, String(start), String(limit), `${timeWindowSize}s`, ctx?.cfg.dataSource ) dataResp = resp.data } finally { setIsLoading(false) } if (groupBy === AggLevel.Query || instance?.instance_type === 'tidb') { // Sort data by digest let data: TopsqlSummaryItem[] = dataResp.data ?? [] // If this digest occurs continuously on the timeline, we can easily see the sequential overhead data.sort((a, b) => a.sql_digest?.localeCompare(b.sql_digest!) || 0) data.forEach((d) => { d.sql_text = formatSql(d.sql_text) d.plans?.forEach((item) => { // Filter data based on orderBy dimension let filterFn: (index: number) => boolean switch (orderBy) { case OrderBy.NetworkBytes: filterFn = (index: number) => !!item.network_bytes?.[index] break case OrderBy.LogicalIoBytes: filterFn = (index: number) => !!item.logical_io_bytes?.[index] break case OrderBy.CpuTime: default: filterFn = (index: number) => !!item.cpu_time_ms?.[index] break } item.timestamp_sec = item.timestamp_sec?.filter((_, index) => filterFn(index) ) // Filter the corresponding data arrays if (item.cpu_time_ms) { item.cpu_time_ms = item.cpu_time_ms.filter((_, index) => filterFn(index) ) } if (item.network_bytes) { item.network_bytes = item.network_bytes.filter((_, index) => filterFn(index) ) } if (item.logical_io_bytes) { item.logical_io_bytes = item.logical_io_bytes.filter((_, index) => filterFn(index) ) } item.timestamp_sec = item.timestamp_sec?.map((t) => t * 1000) }) }) setTopSQLData(data) } if ( groupBy === AggLevel.Table || groupBy === AggLevel.Schema || groupBy === AggLevel.Region ) { let data: TopsqlSummaryByItem[] = dataResp.data_by ?? [] // Sort data by table // If this table occurs continuously on the timeline, we can easily see the sequential overhead // data.sort((a, b) => a.table_?.localeCompare(b.table!) || 0) data.forEach((d) => { // Filter data based on orderBy dimension // Note: TopsqlSummaryByItem may have network_bytes and logical_io_bytes arrays // but they're not in the type definition, so we use type assertion const byItem = d as any let filterFn: (index: number) => boolean switch (orderBy) { case OrderBy.NetworkBytes: filterFn = (index: number) => !!byItem.network_bytes?.[index] break case OrderBy.LogicalIoBytes: filterFn = (index: number) => !!byItem.logical_io_bytes?.[index] break case OrderBy.CpuTime: default: filterFn = (index: number) => !!d.cpu_time_ms?.[index] break } d.timestamp_sec = d.timestamp_sec?.filter((_, index) => filterFn(index) ) // Filter the corresponding data arrays if (d.cpu_time_ms) { d.cpu_time_ms = d.cpu_time_ms.filter((_, index) => filterFn(index)) } if (byItem.network_bytes) { byItem.network_bytes = byItem.network_bytes.filter((_, index) => filterFn(index) ) } if (byItem.logical_io_bytes) { byItem.logical_io_bytes = byItem.logical_io_bytes.filter( (_, index) => filterFn(index) ) } d.timestamp_sec = d.timestamp_sec?.map((t) => t * 1000) }) setTopSQLData(data) } } ) useEffect(() => { updateTopSQLData(instance, timeRange, limit) }, [instance, timeRange, limit, groupBy, orderBy, updateTopSQLData]) return { topSQLData, isLoading, updateTopSQLData } } const useTopSQLConfig = () => { const ctx = useContext(TopSQLContext) // Use the instance interface to query if historical data is available const { data: topSQLConfig, isLoading: isConfigLoading, sendRequest: updateConfig } = useClientRequest(ctx!.ds.topsqlConfigGet) const [haveHistoryData, setHaveHistoryData] = useState(true) const [loadingHistory, setLoadingHistory] = useState(true) useEffect(() => { if (!topSQLConfig) { return } if (!!topSQLConfig.enable) { setLoadingHistory(false) return } ;(async function () { const now = Date.now() / 1000 const sevenDaysAgo = now - 7 * 24 * 60 * 60 setLoadingHistory(true) try { const res = await ctx!.ds.topsqlInstancesGet( String(now), String(sevenDaysAgo), ctx?.cfg.dataSource ) const data = res.data.data if (!!data?.length) { setHaveHistoryData(true) } else { setHaveHistoryData(false) } } finally { setLoadingHistory(false) } })() }, [topSQLConfig, ctx]) return { topSQLConfig, isConfigLoading: isConfigLoading || loadingHistory, updateConfig, haveHistoryData } } const useInstances = (timeRange: TimeRange) => { const ctx = useContext(TopSQLContext) const [instances, setInstances] = useState([]) const [isLoading, setLoading] = useState(false) const fetchInstances = useCallback( async (_timeRange: TimeRange | null) => { if (!_timeRange) { return [] } const [start, end] = toTimeRangeValue(_timeRange) const resp = await ctx!.ds.topsqlInstancesGet( String(end), String(start), ctx?.cfg.dataSource ) // Deduplicate by instance and instance_type combination const instanceMap = new Map() ;(resp.data.data || []).forEach((item) => { const key = `${item.instance}_${item.instance_type}` if (item.instance && item.instance_type && !instanceMap.has(key)) { instanceMap.set(key, item) } }) const data = sortBy(Array.from(instanceMap.values()), [ 'instance_type', 'instance' ]) setInstances(data) return data }, [ctx] ) useEffect(() => { setLoading(true) fetchInstances(timeRange).finally(() => { setLoading(false) }) }, [timeRange, fetchInstances]) return { instances, fetchInstances, isLoading } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListChart.tsx ================================================ import { Axis, BarSeries, Chart, Position, ScaleType, Settings, BrushEndListener } from '@elastic/charts' import { orderBy as lodashOrderBy, toPairs } from 'lodash' import React, { useMemo, useState, forwardRef } from 'react' import { getValueFormat } from '@baurine/grafana-value-formats' import { TopsqlSummaryItem, TopsqlSummaryByItem } from '@lib/client' import { useTranslation } from 'react-i18next' import { useChange } from '@lib/utils/useChange' import { DEFAULT_CHART_SETTINGS, timeTickFormatter } from '@lib/utils/charts' import { AggLevel, OrderBy } from './List' export interface ListChartProps { data: any[] timeWindowSize: number groupBy: string orderBy: OrderBy timeRangeTimestamp: [number, number] onBrushEnd: BrushEndListener } const isQueryAggLevel = (groupBy: string) => { // default is query return !( groupBy === AggLevel.Table || groupBy === AggLevel.Schema || groupBy === AggLevel.Region ) } const getAxisTitle = (orderBy: OrderBy, t: any): string => { switch (orderBy) { case OrderBy.NetworkBytes: return t('topsql.chart.network_bytes') || 'Network Bytes' case OrderBy.LogicalIoBytes: return t('topsql.chart.logical_io_bytes') || 'Logical IO Bytes' case OrderBy.CpuTime: default: return t('topsql.chart.cpu_time') } } const getAxisTickFormatter = (orderBy: OrderBy) => { switch (orderBy) { case OrderBy.NetworkBytes: return (v: number, decimals: number) => getValueFormat('bytes')(v, decimals) case OrderBy.LogicalIoBytes: return (v: number, decimals: number) => getValueFormat('bytes')(v, decimals) case OrderBy.CpuTime: default: return (v: number, decimals: number) => getValueFormat('ms')(v, decimals) } } export const ListChart = forwardRef( ( { onBrushEnd, data, groupBy, orderBy, timeWindowSize, timeRangeTimestamp }, ref ) => { const { t } = useTranslation() // And we need update all the data at the same time and let the chart refresh only once for a better experience. const [bundle, setBundle] = useState({ data, groupBy, orderBy, timeWindowSize, timeRangeTimestamp }) const { chartData } = useChartData( bundle.data, bundle.groupBy, bundle.orderBy ) const { digestMap } = useDigestMap(bundle.data, bundle.groupBy) useChange(() => { setBundle({ data, groupBy, orderBy, timeWindowSize, timeRangeTimestamp }) }, [data, groupBy, orderBy]) return ( getAxisTickFormatter(bundle.orderBy)(v, 1)} ticks={5} /> {Object.keys(chartData).map((originText) => { const sql = digestMap?.[originText] || '' let text = sql if (!originText) { text = t('topsql.table.others') // is unknown text } else if (!sql) { if (isQueryAggLevel(bundle.groupBy)) { // cannot find the sql text, but we agg by sql text = `(SQL ${originText.slice(0, 8)})` } else { text = originText } } else { // text too long, show a part of it text = sql.length > 50 ? `${sql.slice(0, 50)}...` : sql } return ( ) })} {Object.keys(chartData).length === 0 && ( // When there is no data, supply an empty one to preserve the axis. )} ) } ) function useDigestMap(seriesDataO: any[] = [], groupBy: string) { const digestMap = useMemo(() => { if (!seriesDataO) { return {} } if (!isQueryAggLevel(groupBy)) { return {} } let seriesData = seriesDataO as TopsqlSummaryItem[] if (!seriesData) { return {} } return seriesData.reduce((prev, { sql_digest, sql_text }) => { prev[sql_digest!] = sql_text return prev }, {} as { [digest: string]: string | undefined }) }, [seriesDataO, groupBy]) return { digestMap } } function useChartData(seriesDataO: any[], groupBy: string, orderBy: OrderBy) { let chartData: Record> = {} chartData = useMemo(() => { if (isQueryAggLevel(groupBy)) { if (!seriesDataO) { return {} } let seriesData = seriesDataO as TopsqlSummaryItem[] // Group by SQL digest + timestamp and sum their values const valuesByDigestAndTs: Record> = {} const sumValueByDigest: Record = {} // Get the value getter function based on orderBy const getValue = (values: any, i: number): number => { switch (orderBy) { case OrderBy.NetworkBytes: return values.network_bytes?.[i] || 0 case OrderBy.LogicalIoBytes: return values.logical_io_bytes?.[i] || 0 case OrderBy.CpuTime: default: return values.cpu_time_ms?.[i] || 0 } } seriesData.forEach((series) => { const seriesDigest = series.sql_digest! if (!valuesByDigestAndTs[seriesDigest]) { valuesByDigestAndTs[seriesDigest] = {} } const map = valuesByDigestAndTs[seriesDigest] let sum = 0 series.plans?.forEach((values) => { values.timestamp_sec?.forEach((t, i) => { const value = getValue(values, i) if (!map[t]) { map[t] = value } else { map[t] += value } sum += value }) }) if (!sumValueByDigest[seriesDigest]) { sumValueByDigest[seriesDigest] = 0 } sumValueByDigest[seriesDigest] += sum }) // Order by digest const orderedDigests = lodashOrderBy( toPairs(sumValueByDigest), ['1'], ['desc'] ) .filter((v) => v[1] > 0) .map((v) => v[0]) const datumByDigest: Record> = {} for (const digest of orderedDigests) { const datum: Array<[number, number]> = [] const valuesByTs = valuesByDigestAndTs[digest] for (const ts in valuesByTs) { const value = valuesByTs[ts] datum.push([Number(ts), value]) } datumByDigest[digest] = datum } return datumByDigest } else { if (!seriesDataO) { return {} } let seriesData = seriesDataO as TopsqlSummaryByItem[] const datumBy: Record> = {} // Get the value getter function based on orderBy const getValue = (series: any, i: number): number => { switch (orderBy) { case OrderBy.NetworkBytes: return series.network_bytes?.[i] || 0 case OrderBy.LogicalIoBytes: return series.logical_io_bytes?.[i] || 0 case OrderBy.CpuTime: default: return series.cpu_time_ms?.[i] || 0 } } seriesData.forEach((series) => { const key = series.text! if (!datumBy[key]) { datumBy[key] = [] } series.timestamp_sec?.forEach((t, i) => { const value = getValue(series, i) if (value > 0) { datumBy[key].push([t, value]) } }) }) return datumBy } }, [seriesDataO, groupBy, orderBy]) return { chartData } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListDetail/ListDetail.tsx ================================================ import React from 'react' import { useTranslation } from 'react-i18next' import { Head } from '@lib/components' import { InstanceType, ListDetailTable } from './ListDetailTable' import type { SQLRecord } from '../ListTable' import { OrderBy } from '../List' interface ListDetailProps { record: SQLRecord capacity: number instanceType: InstanceType orderBy: OrderBy } export function ListDetail({ record, capacity, instanceType, orderBy }: ListDetailProps) { const { t } = useTranslation() return ( <> ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListDetail/ListDetailContent.tsx ================================================ import React, { useState } from 'react' import { Space } from 'antd' import formatSql from '@lib/utils/sqlFormatter' import { Descriptions, Expand, Pre, HighlightSQL, TextWithInfo, Card, CopyLink } from '@lib/components' import type { PlanRecord } from './ListDetailTable' import type { SQLRecord } from '../ListTable' import { isNoPlanRecord, isOverallRecord } from '@lib/apps/TopSQL/utils/specialRecord' interface ListDetailContentProps { sqlRecord: SQLRecord planRecord?: PlanRecord } export function ListDetailContent({ sqlRecord, planRecord }: ListDetailContentProps) { const [sqlExpanded, setSqlExpanded] = useState(false) const toggleSqlExpanded = () => setSqlExpanded((prev) => !prev) const [planExpanded, setPlanExpanded] = useState(false) const togglePlanExpanded = () => setPlanExpanded((prev) => !prev) return ( { // avoid page hang when sql is too long sqlRecord.sql_text!.length < 10000 && ( ) } } > } > } > {sqlRecord.sql_digest} {!!planRecord?.plan_digest && !isOverallRecord(planRecord) && !isNoPlanRecord(planRecord) ? ( } > {planRecord.plan_digest} ) : null} {!!planRecord?.plan_text ? ( } >
{planRecord.plan_text}
) : null}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListDetail/ListDetailTable.tsx ================================================ import React, { useCallback, useMemo } from 'react' import { SelectionMode, IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { Tooltip } from 'antd' import { getValueFormat } from '@baurine/grafana-value-formats' import { useTranslation } from 'react-i18next' import { QuestionCircleOutlined } from '@ant-design/icons' import { CSVLink } from 'react-csv' import { Bar, TextWrap, CardTable, Card } from '@lib/components' import { TopsqlSummaryPlanItem } from '@lib/client' import type { SQLRecord } from '../ListTable' import { ListDetailContent } from './ListDetailContent' import { useRecordSelection } from '../../../utils/useRecordSelection' import { convertNoPlanRecord, createOverallRecord, isNoPlanRecord, isOverallRecord } from '@lib/apps/TopSQL/utils/specialRecord' import { telemetry } from '@lib/apps/TopSQL/utils/telemetry' import { OrderBy } from '../List' export type InstanceType = 'tidb' | 'tikv' interface ListDetailTableProps { record: SQLRecord capacity: number instanceType: InstanceType orderBy: OrderBy } const UNKNOWN_LABEL = 'Unknown' const formatZero = (v: number) => { if (v.toFixed(1) === '0.0') { return 0 } return v } const shortFormat = (v: number = 0) => { return v && v < 0.1 ? '<0.1' : getValueFormat('short')(formatZero(v), 1) } const msFormat = (v: number = 0) => { return getValueFormat('ms')(formatZero(v), 1) } export function ListDetailTable({ record: sqlRecord, capacity, instanceType, orderBy }: ListDetailTableProps) { const { records: planRecords, isMultiPlans, detailCapacity } = usePlanRecord(sqlRecord, orderBy) const { t } = useTranslation() // Use detailCapacity if available, otherwise fall back to capacity from parent const effectiveCapacity = detailCapacity > 0 ? detailCapacity : capacity // Get column title and value based on orderBy const getColumnTitle = () => { switch (orderBy) { case OrderBy.NetworkBytes: return t('topsql.detail.fields.network_bytes') || 'Network Bytes' case OrderBy.LogicalIoBytes: return t('topsql.detail.fields.logical_io_bytes') || 'Logical IO Bytes' case OrderBy.CpuTime: default: return t('topsql.detail.fields.cpu_time') } } const getColumnValue = (rec: PlanRecord): number => { switch (orderBy) { case OrderBy.NetworkBytes: return rec.networkBytes || 0 case OrderBy.LogicalIoBytes: return rec.logicalIoBytes || 0 case OrderBy.CpuTime: default: return rec.cpuTime || 0 } } const getValueFormatter = () => { switch (orderBy) { case OrderBy.NetworkBytes: case OrderBy.LogicalIoBytes: return (v: number) => getValueFormat('bytes')(v, 2) case OrderBy.CpuTime: default: return (v: number) => getValueFormat('ms')(v, 2) } } const formatter = getValueFormatter() const tableColumns = useMemo( () => [ { name: getColumnTitle(), key: orderBy === OrderBy.NetworkBytes ? 'networkBytes' : orderBy === OrderBy.LogicalIoBytes ? 'logicalIoBytes' : 'cpuTime', minWidth: 150, maxWidth: 250, onRender: (rec: PlanRecord) => { const value = getColumnValue(rec) return ( {formatter(value)} ) } }, { name: t('topsql.detail.fields.plan'), key: 'plan_digest', minWidth: 150, maxWidth: 150, onRender: (rec: PlanRecord) => { return isOverallRecord(rec) ? ( {t('topsql.detail.overall')} ) : isNoPlanRecord(rec) ? ( {t('topsql.detail.no_plan')} ) : (
                {rec.plan_digest?.slice(0, 8) || UNKNOWN_LABEL}
              
) } }, { name: t('topsql.detail.fields.exec_count_per_sec'), key: 'exec_count_per_sec', minWidth: 50, maxWidth: 150, onRender: (rec: PlanRecord) => ( {shortFormat(rec.exec_count_per_sec)} ) }, instanceType === 'tikv' && { name: t('topsql.detail.fields.scan_records_per_sec'), key: 'scan_records_per_sec', minWidth: 50, maxWidth: 150, onRender: (rec: PlanRecord) => ( {shortFormat(rec.scan_records_per_sec)} ) }, instanceType === 'tikv' && { name: t('topsql.detail.fields.scan_indexes_per_sec'), key: 'scan_indexes_per_sec', minWidth: 50, maxWidth: 150, onRender: (rec: PlanRecord) => ( {shortFormat(rec.scan_indexes_per_sec)} ) }, instanceType === 'tidb' && { name: t('topsql.detail.fields.duration_per_exec_ms'), key: 'duration_per_exec_ms', minWidth: 50, maxWidth: 150, onRender: (rec: PlanRecord) => ( {msFormat(rec.duration_per_exec_ms)} ) } ].filter((c) => !!c) as IColumn[], [effectiveCapacity, instanceType, orderBy, t] ) const csvHeaders = tableColumns.map((c) => ({ label: c.name, key: c.key })) const getKey = useCallback((r: PlanRecord) => r?.plan_digest!, []) const { selectedRecord, selection } = useRecordSelection({ storageKey: 'topsql.list_detail_table_selected_key', selections: planRecords, options: { getKey, canSelectItem: (r) => !isNoPlanRecord(r) && !isOverallRecord(r) } }) const planRecord = useMemo(() => { if (isMultiPlans) { return selectedRecord } return planRecords[0] }, [planRecords, isMultiPlans, selectedRecord]) return ( <> Download to CSV { const index = planRecords .filter((r) => !isOverallRecord(r) && !isNoPlanRecord(r)) .findIndex((r) => r.plan_digest === item.plan_digest) if (index > -1) { telemetry.clickPlan(index) } }} selection={selection} /> {!sqlRecord.is_other && ( )} ) } export type PlanRecord = { cpuTime: number networkBytes?: number logicalIoBytes?: number } & TopsqlSummaryPlanItem const usePlanRecord = ( record: SQLRecord, orderBy: OrderBy ): { isMultiPlans: boolean; records: PlanRecord[]; detailCapacity: number } => { return useMemo(() => { if (!record?.plans?.length) { return { isMultiPlans: false, records: [], detailCapacity: 0 } } const isMultiPlans = record.plans.length > 1 const plans = [...record.plans] let detailCapacity = 0 const records: PlanRecord[] = plans .map((p) => { const cpuTime = p.cpu_time_ms?.reduce((pt, t) => pt + t, 0) || 0 const networkBytes = p.network_bytes?.reduce((pt, t) => pt + t, 0) || 0 const logicalIoBytes = p.logical_io_bytes?.reduce((pt, t) => pt + t, 0) || 0 // Calculate capacity based on the selected orderBy dimension let value = 0 switch (orderBy) { case OrderBy.NetworkBytes: value = networkBytes break case OrderBy.LogicalIoBytes: value = logicalIoBytes break case OrderBy.CpuTime: default: value = cpuTime break } if (detailCapacity < value) { detailCapacity = value } return { ...p, cpuTime, networkBytes, logicalIoBytes } }) .sort((a, b) => { // Sort based on the selected orderBy dimension let aValue = 0 let bValue = 0 switch (orderBy) { case OrderBy.NetworkBytes: aValue = a.networkBytes || 0 bValue = b.networkBytes || 0 break case OrderBy.LogicalIoBytes: aValue = a.logicalIoBytes || 0 bValue = b.logicalIoBytes || 0 break case OrderBy.CpuTime: default: aValue = a.cpuTime bValue = b.cpuTime break } return bValue - aValue }) .map(convertNoPlanRecord) // add overall record to the first if (isMultiPlans) { const overallRecord = createOverallRecord(record, orderBy) records.unshift(overallRecord) // Update capacity if overall record has larger value const overallValue = getOverallValue(overallRecord, orderBy) if (detailCapacity < overallValue) { detailCapacity = overallValue } } return { isMultiPlans, records, detailCapacity } }, [record, orderBy]) } const getOverallValue = (rec: PlanRecord, orderBy: OrderBy): number => { switch (orderBy) { case OrderBy.NetworkBytes: return rec.networkBytes || 0 case OrderBy.LogicalIoBytes: return rec.logicalIoBytes || 0 case OrderBy.CpuTime: default: return rec.cpuTime || 0 } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListDetail/index.ts ================================================ export * from './ListDetail' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListTable.tsx ================================================ import React, { useContext, useMemo } from 'react' import { Tooltip, Typography } from 'antd' import { getValueFormat } from '@baurine/grafana-value-formats' import { useTranslation } from 'react-i18next' import { SelectionMode, DetailsRow } from 'office-ui-fabric-react/lib/DetailsList' import { QuestionCircleOutlined } from '@ant-design/icons' import { CSVLink } from 'react-csv' import { TopsqlSummaryItem, TopsqlSummaryByItem } from '@lib/client' import { Card, CardTable, Bar, TextWrap, HighlightSQL, AppearAnimate, TimeRange, toTimeRangeValue } from '@lib/components' import { useRecordSelection } from '../../utils/useRecordSelection' import { ListDetail } from './ListDetail' import { isOthersRecord, isSummaryByRecord, isUnknownSQLRecord } from '../../utils/specialRecord' import { InstanceType } from './ListDetail/ListDetailTable' import { useMemoizedFn } from 'ahooks' import { telemetry } from '../../utils/telemetry' import openLink from '@lib/utils/openLink' import { useNavigate } from 'react-router-dom' import { TopSQLContext } from '../../context' import { AggLevel, OrderBy } from './List' interface ListTableProps { data: any[] groupBy: string orderBy: OrderBy topN: number instanceType: InstanceType timeRange: TimeRange onRowOver: (key: string) => void onRowLeave: () => void } const emptyFn = () => {} export type SQLRecord = TopsqlSummaryItem & TopsqlSummaryByItem & { cpuTime: number networkBytes?: number logicalIoBytes?: number } function isConvertNumber(value: string): boolean { let res = !isNaN(Number(value)) return res } export function ListTable({ data, groupBy, orderBy, topN, instanceType, timeRange, onRowLeave, onRowOver }: ListTableProps) { const { t } = useTranslation() const { data: tableRecords, capacity } = useTableData(data, orderBy) const navigate = useNavigate() const ctx = useContext(TopSQLContext) const tableColumns = useMemo(() => { function goDetail(ev: React.MouseEvent, record: SQLRecord) { const sv = sessionStorage.getItem('statement.query_options') if (sv) { const queryOptions = JSON.parse(sv) queryOptions.searchText = record.sql_digest sessionStorage.setItem( 'statement.query_options', JSON.stringify(queryOptions) ) } const tv = toTimeRangeValue(timeRange) openLink(`/statement?from=${tv[0]}&to=${tv[1]}`, ev, navigate) } // Get column title and value based on orderBy const getColumnTitle = () => { switch (orderBy) { case OrderBy.NetworkBytes: return t('topsql.table.fields.network_bytes') || 'Network Bytes' case OrderBy.LogicalIoBytes: return t('topsql.table.fields.logical_io_bytes') || 'Logical IO Bytes' case OrderBy.CpuTime: default: return t('topsql.table.fields.cpu_time') } } const getColumnValue = (rec: SQLRecord): number => { switch (orderBy) { case OrderBy.NetworkBytes: return rec.networkBytes || 0 case OrderBy.LogicalIoBytes: return rec.logicalIoBytes || 0 case OrderBy.CpuTime: default: return rec.cpuTime || 0 } } const getValueFormatter = () => { switch (orderBy) { case OrderBy.NetworkBytes: case OrderBy.LogicalIoBytes: return (v: number) => getValueFormat('bytes')(v, 2) case OrderBy.CpuTime: default: return (v: number) => getValueFormat('ms')(v, 2) } } const formatter = getValueFormatter() let cols = [ { name: getColumnTitle(), key: orderBy === OrderBy.NetworkBytes ? 'networkBytes' : orderBy === OrderBy.LogicalIoBytes ? 'logicalIoBytes' : 'cpuTime', minWidth: 150, maxWidth: 250, onRender: (rec: SQLRecord) => { const value = getColumnValue(rec) return ( {formatter(value)} ) } }, { name: groupBy === AggLevel.Table ? t('topsql.table.fields.table') : groupBy === AggLevel.Schema ? t('topsql.table.fields.db') : groupBy === AggLevel.Region ? 'RegionID' : t('topsql.table.fields.sql'), key: groupBy === AggLevel.Table || groupBy === AggLevel.Schema || groupBy === AggLevel.Region ? 'text' : 'sql_text', minWidth: 250, maxWidth: 550, onRender: (rec: SQLRecord) => { let text = rec.text ? rec.text : isUnknownSQLRecord(rec) ? `(SQL ${rec.sql_digest?.slice(0, 8)})` : rec.sql_text! // parser the table name if the text is like "tableId-tableName" text = text.includes('-') && text.split('-').length > 1 ? isConvertNumber(text.split('-')[0]) ? text.split('-')[1] + ' ( tid =' + text.split('-')[0] + ' )' : text : text return isOthersRecord(rec) ? ( {t('topsql.table.others')} ) : ( } placement="right" > ) } }, { name: '', key: 'actions', minWidth: 200, // maxWidth: 200, onRender: (rec) => { if (!isOthersRecord(rec) && !isSummaryByRecord(rec)) { return ( goDetail(ev, rec)}> {t('topsql.table.actions.search_in_statements')} ) } return null } } ] if (ctx?.cfg.showSearchInStatements === false) { cols = cols.filter((c) => c.key !== 'actions') } return cols }, [ capacity, t, topN, groupBy, orderBy, navigate, timeRange, ctx?.cfg.showSearchInStatements ]) const csvHeaders = tableColumns .slice(0, 2) .map((c) => ({ label: c.name, key: c.key })) const getKey = useMemoizedFn((r: SQLRecord) => r?.sql_digest ?? r?.text ?? '') const { selectedRecord, selection } = useRecordSelection({ storageKey: 'topsql.list_table_selected_key', selections: tableRecords, options: { getKey, canSelectItem: (r) => !isSummaryByRecord(r) } }) const onRenderRow = useMemoizedFn((props: any) => (
onRowOver(props.item?.sql_digest ?? props.item?.text)} onMouseLeave={onRowLeave} onClick={() => telemetry.clickStatement(props.itemIndex, props.itemIndex === topN) } >
)) return tableRecords.length ? ( <>
{t('topsql.table.description', { topN })}{' '} Download to CSV
{selectedRecord && groupBy !== AggLevel.Table && groupBy !== AggLevel.Schema && groupBy !== AggLevel.Region && ( )} ) : null } function useTableData(records: any[], orderBy: OrderBy) { const tableData: { data: SQLRecord[]; capacity: number } = useMemo(() => { if (!records) { return { data: [], capacity: 0 } } const sum = (arr?: Array): number => (arr ?? []).reduce((acc, v) => acc + (v || 0), 0) let capacity = 0 const d = records .map((r) => { let cpuTime = 0 let networkBytes = 0 let logicalIoBytes = 0 r.plans?.forEach((plan: any) => { plan.timestamp_sec?.forEach((t: number, i: number) => { cpuTime += plan.cpu_time_ms?.[i] || 0 // network_bytes and logical_io_bytes might be arrays similar to cpu_time_ms networkBytes += plan.network_bytes?.[i] || 0 logicalIoBytes += plan.logical_io_bytes?.[i] || 0 }) }) // For SummaryByItem (groupBy table / schema / region) // Note: backend may omit unrelated fields depending on orderBy, so avoid using // cpu_time_ms_sum as the "is summary-by" guard. if ((r.text?.length ?? 0) > 0) { cpuTime = r.cpu_time_ms_sum ?? sum(r.cpu_time_ms) networkBytes = r.network_bytes_sum ?? sum(r.network_bytes) logicalIoBytes = r.logical_io_bytes_sum ?? sum(r.logical_io_bytes) } // Calculate capacity based on the selected orderBy dimension let sortValue = 0 switch (orderBy) { case OrderBy.CpuTime: sortValue = cpuTime break case OrderBy.NetworkBytes: sortValue = networkBytes break case OrderBy.LogicalIoBytes: sortValue = logicalIoBytes break } if (capacity < sortValue) { capacity = sortValue } return { ...r, cpuTime, networkBytes, logicalIoBytes, plans: r.plans || [] } }) .filter((r) => { // Filter based on the selected orderBy dimension switch (orderBy) { case OrderBy.CpuTime: return !!r.cpuTime case OrderBy.NetworkBytes: return !!r.networkBytes case OrderBy.LogicalIoBytes: return !!r.logicalIoBytes default: return !!r.cpuTime } }) .sort((a, b) => { // Sort based on the selected orderBy dimension let aValue = 0 let bValue = 0 switch (orderBy) { case OrderBy.CpuTime: aValue = a.cpuTime bValue = b.cpuTime break case OrderBy.NetworkBytes: aValue = a.networkBytes || 0 bValue = b.networkBytes || 0 break case OrderBy.LogicalIoBytes: aValue = a.logicalIoBytes || 0 bValue = b.logicalIoBytes || 0 break } return bValue - aValue }) .sort((a, b) => (b.is_other ? -1 : 0)) return { data: d, capacity } }, [records, orderBy]) return tableData } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/SettingsForm.module.less ================================================ @import 'antd/es/style/themes/default.less'; .partialSwitch { background: linear-gradient( 90deg, @blue-6 0%, @blue-6 50%, @gray-5 50%, @gray-5 100% ) !important; border-color: transparent !important; &:hover:not(:global(.ant-switch-disabled)) { background: linear-gradient( 90deg, @blue-5 0%, @blue-5 50%, @gray-4 50%, @gray-4 100% ) !important; border-color: transparent !important; } :global(.ant-switch-handle) { left: calc(50% - 9px) !important; } :global(.ant-switch-inner) { margin: 0; padding: 0; text-align: center; } } .switchWithStatus { display: inline-flex; align-items: center; gap: 8px; } .switchStatus { color: @text-color-secondary; } .partialResultContent { display: flex; flex-direction: column; gap: 8px; } .successInfoContent { display: flex; flex-direction: column; gap: 4px; } .successInfoEmphasis { font-weight: 600; } .partialResultWarnings { margin: 0; padding: 8px; max-height: 120px; overflow: auto; white-space: pre-wrap; word-break: break-word; background: @background-color-light; border: 1px solid @border-color-base; border-radius: @border-radius-base; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/SettingsForm.tsx ================================================ import React, { useState, useCallback, useContext, useEffect, useMemo } from 'react' import { Form, Skeleton, Switch, Space, Button, Modal } from 'antd' import { ExclamationCircleOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { TopsqlEditableConfig } from '@lib/client' import { useClientRequest } from '@lib/utils/useClientRequest' import { DrawerFooter, ErrorBar } from '@lib/components' import { useIsWriteable } from '@lib/utils/store' import { telemetry } from '../../utils/telemetry' import { TopSQLContext } from '../../context' import styles from './SettingsForm.module.less' interface Props { onClose: () => void onConfigUpdated: () => any } interface FormValues { enable: boolean tikv_network_io_collection: boolean } export function SettingsForm({ onClose, onConfigUpdated }: Props) { const ctx = useContext(TopSQLContext) const [form] = Form.useForm() const [submitting, setSubmitting] = useState(false) const { t } = useTranslation() const isWriteable = useIsWriteable() const { data: initialConfig, isLoading: loading, error } = useClientRequest(ctx!.ds.topsqlConfigGet) const { data: initialTikvNetworkIoCollection, isLoading: loadingTikvNetworkIoCollection, error: errorTikvNetworkIoCollection } = useClientRequest(ctx!.ds.topsqlTikvNetworkIoCollectionGet) const handleSubmit = useCallback( (values: FormValues) => { async function updateConfig(values: FormValues) { const newConfig: TopsqlEditableConfig = { enable: values.enable } try { setSubmitting(true) const shouldCheckTikvCollectionResult = values.tikv_network_io_collection && form.isFieldTouched('tikv_network_io_collection') const [, tikvCollectionUpdateResponse] = await Promise.all([ ctx!.ds.topsqlConfigPost(newConfig), ctx!.ds.topsqlTikvNetworkIoCollectionPost({ enable: values.tikv_network_io_collection }) ]) const tikvCollectionWarningMessages = ( tikvCollectionUpdateResponse.data.warnings ?? [] ) .map((w) => w.message || w.full_text || '') .filter((msg) => !!msg) let tikvCollectionAfterSave: | { enable: boolean; is_multi_value?: boolean } | undefined if (shouldCheckTikvCollectionResult) { try { const resp = await ctx!.ds.topsqlTikvNetworkIoCollectionGet() tikvCollectionAfterSave = resp.data } catch { // Ignore this best-effort check so save flow remains non-blocking. } } telemetry.saveSettings({ ...newConfig, tikv_network_io_collection: values.tikv_network_io_collection }) onClose() onConfigUpdated() if (values.enable && !initialConfig?.enable) { Modal.success({ title: t('topsql.settings.enable_info.title'), content: t('topsql.settings.enable_info.content') }) } if (shouldCheckTikvCollectionResult) { const isPartialAfterSave = tikvCollectionAfterSave?.is_multi_value === true const isAllEnabledAfterSave = tikvCollectionAfterSave?.enable === true if ( !isPartialAfterSave && isAllEnabledAfterSave && tikvCollectionWarningMessages.length === 0 ) { Modal.success({ title: t( 'topsql.settings.tikv_network_io_collection_info.title' ), content: (
{t( 'topsql.settings.tikv_network_io_collection_info.content_prefix' )}
{t( 'topsql.settings.tikv_network_io_collection_info.content_emphasis' )}
) }) } else { Modal.warning({ title: t( 'topsql.settings.tikv_network_io_collection_partial_info.title' ), content: (
{t( 'topsql.settings.tikv_network_io_collection_partial_info.content' )}
{tikvCollectionWarningMessages.length > 0 && (
                        {tikvCollectionWarningMessages.join('\n')}
                      
)} window.open(t('topsql.settings.help_url'), '_blank') } > {t( 'topsql.settings.tikv_network_io_collection_partial_info.action' )}
) }) } } } finally { setSubmitting(false) } } if (!values.enable && (initialConfig?.enable ?? true)) { // warning Modal.confirm({ title: t('topsql.settings.disable_feature'), icon: , content: t('topsql.settings.disable_warning'), okText: t('topsql.settings.actions.close'), cancelText: t('topsql.settings.actions.cancel'), okButtonProps: { danger: true }, onOk: () => updateConfig(values) }) } else { updateConfig(values) } }, [t, onClose, onConfigUpdated, initialConfig, ctx, form] ) const combinedLoading = loading || loadingTikvNetworkIoCollection const combinedError = [error, errorTikvNetworkIoCollection].filter((e) => !!e) const topsqlEnabled = Form.useWatch('enable', form) const tikvNetworkIoCollectionEnabled = Form.useWatch( 'tikv_network_io_collection', form ) const tikvStatusText = useMemo(() => { if (topsqlEnabled === false) { return t('topsql.settings.tikv_network_io_collection_disabled_by_topsql') } if (!initialTikvNetworkIoCollection) { return '' } if (initialTikvNetworkIoCollection.is_multi_value) { return t('topsql.settings.tikv_network_io_collection_status.partial') } return initialTikvNetworkIoCollection.enable ? t('topsql.settings.tikv_network_io_collection_status.on') : t('topsql.settings.tikv_network_io_collection_status.off') }, [topsqlEnabled, initialTikvNetworkIoCollection, t]) const showTikvNetworkIoCollectionPartialState = useMemo(() => { if (topsqlEnabled === false) { return false } if (!initialTikvNetworkIoCollection?.is_multi_value) { return false } if (form.isFieldTouched('tikv_network_io_collection')) { return false } return ( tikvNetworkIoCollectionEnabled === initialTikvNetworkIoCollection.enable ) }, [ topsqlEnabled, initialTikvNetworkIoCollection, tikvNetworkIoCollectionEnabled, form ]) const tikvNetworkIoCollectionTooltip = useMemo(() => { return showTikvNetworkIoCollectionPartialState ? t('topsql.settings.tikv_network_io_collection_tooltip_partial') : t('topsql.settings.tikv_network_io_collection_tooltip') }, [showTikvNetworkIoCollectionPartialState, t]) useEffect(() => { if (topsqlEnabled === false) { form.setFieldsValue({ tikv_network_io_collection: false }) } }, [topsqlEnabled, form]) return ( <> {combinedError.length > 0 && } {combinedLoading && } {!combinedLoading && initialConfig && (
{tikvStatusText && ( {tikvStatusText} )}
)} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/legendAction.ts ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import { LegendPath } from '@elastic/charts' // HACK: Use elastic-charts internal api for the legend hover // sync with https://github.com/elastic/elastic-charts/blob/master/packages/charts/src/state/actions/legend.ts export const ON_LEGEND_ITEM_OVER = 'ON_LEGEND_ITEM_OVER' export const ON_LEGEND_ITEM_OUT = 'ON_LEGEND_ITEM_OUT' interface LegendItemOverAction { type: typeof ON_LEGEND_ITEM_OVER legendPath: LegendPath } interface LegendItemOutAction { type: typeof ON_LEGEND_ITEM_OUT } function onLegendItemOverAction(legendPath: LegendPath): LegendItemOverAction { return { type: ON_LEGEND_ITEM_OVER, legendPath } } function onLegendItemOutAction(): LegendItemOutAction { return { type: ON_LEGEND_ITEM_OUT } } export const onLegendItemOver = (chart: any, key: string) => { const legendItems = chart.chartStore .getState() .internalChartState.getLegendItems(chart.chartStore.getState()) if (!legendItems?.length) { return } const item = legendItems.find((it) => it.seriesIdentifiers[0].specId === key) if (!item) { return } chart.chartStore.dispatch(onLegendItemOverAction(item.path)) } export const onLegendItemOut = (chart) => { chart.chartStore.dispatch(onLegendItemOutAction()) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/en.yaml ================================================ topsql: nav_title: Top SQL alert_header: title: Feature Not Enabled body: Top SQL feature is not enabled. The history data are available. You can modify settings to enable the feature and wait for new data being collected. settings: Settings settings: title: Settings open_settings: Open Settings disable_feature: Disable Top SQL Feature disable_warning: Are you sure want to disable this feature? enable: Enable Feature enable_tooltip: Whether Top SQL feature is enabled. When enabled, there will be small overhead. tikv_network_io_collection: Enable TiKV Network IO collection (multi-dimensional) tikv_network_io_collection_tooltip: When enabled, TiKV network traffic, logical IO, and other dimensions can be collected, but this increases storage and query overhead. Newly added TiKV nodes will not enable collection automatically. To auto-enable for new nodes, update the TiKV default config via TiUP (see docs). tikv_network_io_collection_tooltip_partial: Some TiKV nodes do not have collection enabled. You can click the switch to enable all, or update the TiKV default config via TiUP (see docs). tikv_network_io_collection_disabled_by_topsql: (Disabled when TopSQL is off) tikv_network_io_collection_status: on: '(Current: all enabled)' off: '(Current: all disabled)' partial: '(Current: partially enabled)' disabled_result: title: Feature Not Enabled sub_title: Top SQL feature is not enabled. You can modify settings to enable the feature and wait for new data being collected. actions: save: Save close: Disable cancel: Cancel enable_info: title: Success content: Top SQL is enabled now and is collecting data. You need to wait for about 1 minute to view this data. tikv_network_io_collection_info: title: Success content: TiKV Network IO collection has been enabled and is being applied to all current TiKV nodes. You may need to wait for about 1 minute to see data for related dimensions. Note that this is not applied automatically to newly scaled-out TiKV nodes. To make it effective for new nodes, update the TiKV default config via tiup and enable it there as well. content_prefix: TiKV Network IO collection has been enabled and is being applied to all current TiKV nodes. You may need to wait for about 1 minute to see data for related dimensions. content_emphasis: Note that this is not applied automatically to newly scaled-out TiKV nodes. To make it effective for new nodes, update the TiKV default config via tiup and enable it there as well. tikv_network_io_collection_partial_info: title: TiKV Network IO collection is not fully enabled content: Some TiKV nodes still have collection disabled, so new data may be incomplete. You can retry later, or update the TiKV default config via TiUP (see docs). action: View docs help: Help help_url: https://docs.pingcap.com/tidb/dev/top-sql tikv_network_io_collection_tip: title: TiKV Network IO collection is disabled body: In the current state, historical data will be shown if available. New data requires enabling TiKV network/logical IO collection. body_partial: The selected dimension depends on TiKV network/logical IO collection. Some TiKV nodes are not enabled. Historical data may still be shown, but new data may be incomplete. action: Go to settings refresh: Refresh chart: cpu_time: CPU Time network_bytes: Network Bytes logical_io_bytes: Logical IO Bytes table: description: The following table shows which top {{topN}} queries are contributing the most to load in the current time range. Click one to see details. description_no_recent_data: There is no data currently. You need to wait for about 1 minute for new data being collected. others: Others others_tooltip: All of other non Top {{topN}} Items fields: cpu_time: Total CPU Time network_bytes: Total Network Bytes logical_io_bytes: Total Logical IO Bytes sql: SQL Statement table: Table Name db: Database Name actions: search_in_statements: Search in SQL Statements detail: title: SQL Statement Details by Plan overall: Overall overall_tooltip: The execution details of all plans of this statement no_plan: Plan Not Available no_plan_tooltip: This statement is not a query or the statement plan was being generated fields: cpu_time: Total CPU Time network_bytes: Total Network Bytes logical_io_bytes: Total Logical IO Bytes plan: Plan exec_count_per_sec: Call/sec scan_records_per_sec: Scan Rows/sec scan_indexes_per_sec: Scan Indexes/sec duration_per_exec_ms: Latency/call detail_content: fields: sql_text: Statement Template sql_text_tooltip: Similar queries have same statement template even for different query parameters sql_digest: Query Template ID sql_digest_tooltip: a.k.a. Query digest plan_digest: Plan Template ID plan_digest_tooltip: a.k.a. Plan digest plan: Execution Plan ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/zh.yaml ================================================ topsql: nav_title: Top SQL alert_header: title: 该功能未启用 body: Top SQL 功能未启用,可查看历史数据。您可以修改设置打开该功能后等待新数据收集。 settings: 设置 settings: title: 设置 open_settings: 打开设置 disable_feature: 关闭 Top SQL 功能 disable_warning: 确认要关闭该功能吗? enable: 启用功能 enable_tooltip: 是否启用 Top SQL 功能,关闭后将只能看到历史数据,但能提升少量 {{distro.tidb}} 性能。 tikv_network_io_collection: 开启 TiKV 网络 IO 采集(多维度) tikv_network_io_collection_tooltip: 开启后可采集 TiKV 网络流量,逻辑 IO 等多维度数据,不过会增加一定的存储和查询开销。全新 TiKV 节点不会自动开启采集,若需自动开启请通过 TiUP 修改 TiKV 默认配置(请参考文档)。 tikv_network_io_collection_tooltip_partial: 存在未开启采集的 TiKV 节点,可点击按钮全部开启,或者通过 TiUP 修改 TiKV 默认配置(请参考文档) tikv_network_io_collection_disabled_by_topsql: (TopSQL 未开启时该开关强制关闭) tikv_network_io_collection_status: on: (当前状态:全开) off: (当前状态:全关) partial: (当前状态:部分开) disabled_result: title: 该功能未启用 sub_title: Top SQL 功能未启用。您可以修改设置打开该功能后等待新数据收集。 actions: save: 保存 close: 确认 cancel: 取消 enable_info: title: 成功 content: Top SQL 功能现在已启用,正在收集数据。您需要等待大约 1 分钟时间以便看到该数据。 tikv_network_io_collection_info: title: 成功 content: 已开启 TiKV 网络 IO 采集,正在下发到所有当前 TiKV 节点。您可能需要等待约 1 分钟以便看到对应维度的数据。注意,对新扩容的 TiKV 节点不会自动生效,如需生效请通过 tiup 修改 TiKV 默认配置并同步开启。 content_prefix: 已开启 TiKV 网络 IO 采集,正在下发到所有当前 TiKV 节点。您可能需要等待约 1 分钟以便看到对应维度的数据。 content_emphasis: 注意,对新扩容的 TiKV 节点不会自动生效,如需生效请通过 tiup 修改 TiKV 默认配置并同步开启。 tikv_network_io_collection_partial_info: title: TiKV 网络 IO 采集未完全开启 content: 检测到仍有 TiKV 节点未开启采集,新数据可能不完整。您可以稍后重试,或者通过 TiUP 修改 TiKV 默认配置(请参考文档)。 action: 查看文档 help: 帮助 help_url: https://docs.pingcap.com/zh/tidb/dev/top-sql tikv_network_io_collection_tip: title: TiKV 网络 IO 采集未开启 body: 当前状态,历史数据若有则会展示,新数据需要开启 TiKV 网络/逻辑 IO 采集, body_partial: 当前选择的维度依赖 TiKV 的网络/逻辑 IO 采集,检测到部分 TiKV 节点未开启。历史数据仍可展示,但新数据可能不完整。 action: 去设置 refresh: 刷新 chart: cpu_time: CPU 耗时 network_bytes: 网络字节数 logical_io_bytes: 逻辑 IO 字节数 table: description: 以下表格展示了当前时间范围内消耗负载最多的 {{topN}} 类 SQL 查询,点击后可进一步显示详情。 description_no_recent_data: 当前暂无数据,您需要等待约 1 分钟完成新数据采集。 others: 其他 others_tooltip: 所有其他非 Top {{topN}} 的条目 fields: cpu_time: 累计 CPU 耗时 network_bytes: 累计网络字节数 logical_io_bytes: 累计逻辑 IO 字节数 sql: SQL 语句 table: 表名 db: 数据库名 actions: search_in_statements: 在 SQL 语句分析中搜索 detail: title: SQL 详情 overall: 总计 overall_tooltip: 该语句在所有执行计划上的总计详情 no_plan: 无执行计划 no_plan_tooltip: 该语句不是查询语句,或当时执行计划正在生成中 fields: cpu_time: 累计 CPU 耗时 network_bytes: 累计网络字节数 logical_io_bytes: 累计逻辑 IO 字节数 plan: 执行计划 exec_count_per_sec: Call/sec scan_records_per_sec: Scan Rows/sec scan_indexes_per_sec: Scan Indexes/sec duration_per_exec_ms: Latency/call detail_content: fields: sql_text: SQL 模板 sql_text_tooltip: 相似的 SQL 查询即使查询参数不一样也具有相同的 SQL 模板 sql_digest: SQL 模板 ID sql_digest_tooltip: SQL 模板的唯一标识(SQL 指纹) plan_digest: Plan 模板 ID plan_digest_tooltip: Plan 模板的唯一标识(Plan 指纹) plan: 执行计划 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/utils/specialRecord.ts ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import type { PlanRecord } from '../pages/List/ListDetail/ListDetailTable' import type { SQLRecord } from '../pages/List/ListTable' import { OrderBy } from '../pages/List/List' const OVERALL_IDENTIFIER = '__OVERALL_IDENTIFIER__' export const isOverallRecord = (r: PlanRecord) => { return r.plan_digest === OVERALL_IDENTIFIER } export const createOverallRecord = ( record: SQLRecord, orderBy: OrderBy ): PlanRecord => { return { plan_digest: OVERALL_IDENTIFIER, cpuTime: record.cpuTime || 0, networkBytes: record.networkBytes || 0, logicalIoBytes: record.logicalIoBytes || 0, exec_count_per_sec: record.exec_count_per_sec, scan_records_per_sec: record.scan_records_per_sec, scan_indexes_per_sec: record.scan_indexes_per_sec, duration_per_exec_ms: record.duration_per_exec_ms } } export const isOthersRecord = (r: SQLRecord) => { return r.is_other || r.text === 'other' } export const isSummaryByRecord = (r: SQLRecord) => { return (r.text?.length ?? 0) > 0 } const NO_PLAN_IDENTIFIER = '__NO_PLAN_IDENTIFIER__' export const convertNoPlanRecord = (r: PlanRecord) => { const _r = { ...r } if (isNoPlanRecord(_r)) { _r.plan_digest = NO_PLAN_IDENTIFIER } return _r } export const isNoPlanRecord = (r: PlanRecord) => { return !r.plan_digest || r.plan_digest === NO_PLAN_IDENTIFIER } export const isUnknownSQLRecord = (r: SQLRecord) => { return !r.sql_text && !r.is_other } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/utils/telemetry.ts ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import { mixpanel } from '@lib/utils/telemetry' import { TimeRange } from '@lib/components' export const telemetry = { openSelectInstance() { mixpanel.track('TopSQL: Open Select Instance') }, finishSelectInstance(type: string) { mixpanel.track('TopSQL: Finish Select Instance', { type }) }, openTimeRangePicker() { mixpanel.track('TopSQL: Open Time Range Picker') }, selectTimeRange(v: TimeRange) { mixpanel.track('TopSQL: Select Time Range', v) }, clickZoomOut(timestamps: [number, number]) { mixpanel.track('TopSQL: Click Zoom Out Button', { timestamps }) }, dndZoomIn(timestamps: [number, number]) { mixpanel.track('TopSQL: Drag & Drop Zoom In', { timestamps }) }, clickRefresh() { mixpanel.track('TopSQL: Click Refresh') }, clickAutoRefresh() { mixpanel.track('TopSQL: Click Auto Refresh Dropdown') }, selectAutoRefreshOption(seconds: number) { mixpanel.track('TopSQL: Select Auto Refresh Option', { seconds }) }, clickSettings(type: 'firstTimeTips' | 'settingIcon' | 'bannerTips') { mixpanel.track('TopSQL: Click Settings', { type }) }, saveSettings(settings: Record) { mixpanel.track('TopSQL: Save Settings', { settings }) }, clickStatement(index: number, isOther: boolean) { mixpanel.track('TopSQL: Click Statement', { rank: index + 1, isOther }) }, clickPlan(index: number) { mixpanel.track('TopSQL: Click Plan', { rank: index + 1 }) } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSQL/utils/useRecordSelection.ts ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import { useMemo, useEffect } from 'react' import { Selection, SelectionMode, IObjectWithKey } from 'office-ui-fabric-react/lib/Selection' import { useGetSet, useSessionStorage } from 'react-use' import { useMemoizedFn } from 'ahooks' interface Props { storageKey: string selections: T[] options?: { getKey: (s: T) => string canSelectItem?: (s: T) => boolean } } export function useRecordSelection({ storageKey, selections, options }: Props) { const memoOption = useMemo( () => ({ getKey: options?.getKey || (() => 'key'), canSelectItem: options?.canSelectItem || (() => true) }), // eslint-disable-next-line react-hooks/exhaustive-deps [] ) const [selectedRecordKey, _setSelectedRecordKey] = useSessionStorage< string | null >(storageKey, null) const [getInternalKey, setInternalKey] = useGetSet(selectedRecordKey) const setSelectedRecordKey = useMemoizedFn((k: string) => { _setSelectedRecordKey(k) setInternalKey(k) }) const selectedRecord = useMemo( () => selections.find((r) => memoOption.getKey(r) === selectedRecordKey), [selections, selectedRecordKey, memoOption] ) const selection = useMemo(() => { const s = new Selection({ selectionMode: SelectionMode.single, getKey: memoOption.getKey as | (( item: IObjectWithKey, index?: number | undefined ) => string | number) | undefined, canSelectItem: memoOption.canSelectItem as | ((item: IObjectWithKey, index?: number | undefined) => boolean) | undefined, onSelectionChanged: () => { const r = s.getSelection()[0] as T if (!r) { // A hack to fix the selection zone bug, SelectionZone.tsx L330 // It will clear the selection state when click disabled item. // So we need to reselect the correct target. const internalKey = getInternalKey() const isSelectedItemInSelections = s .getItems() .find((r) => memoOption.getKey(r as T) === internalKey) if (!!internalKey && isSelectedItemInSelections) { s.selectToKey(internalKey) } return } const rk = memoOption.getKey(r) if (rk !== getInternalKey()) { setSelectedRecordKey(rk) } } }) return s }, [memoOption, getInternalKey, setSelectedRecordKey]) useEffect(() => { // Selected record will be cleared by selection itself when update items // So we need manual keep the selected state in the selection const isRecordInSelections = !!selectedRecordKey && selection .getItems() .find((tr) => memoOption.getKey(tr as T) === selectedRecordKey) if (!selection.getSelection().length && isRecordInSelections) { selection.selectToKey(selectedRecordKey!) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selections]) return { selectedRecord, selectedRecordKey, selection } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/context.ts ================================================ import { createContext, useContext } from 'react' interface ISlowQuery {} interface ITimeWindow { begin_time: number end_time: number } export interface ITopSlowQueryConfig { // for clinic orgName?: string clusterName?: string userName?: string } export type TopSlowQueryCtxValue = { // api api: { getAvailableTimeWindows(params: { from: number to: number duration: number }): Promise getMetrics: (params: { start: number end: number }) => Promise<[number, number][]> getDatabaseList(params: { start: number; end: number }): Promise getTopSlowQueries(params: { start: number end: number order: string dbs: string[] internal: string stmtKinds: string[] }): Promise } cfg: ITopSlowQueryConfig } export const TopSlowQueryContext = createContext( null ) export const useTopSlowQueryContext = () => { const context = useContext(TopSlowQueryContext) if (!context) { throw new Error('TopSlowQueryContext must be used within a provider') } return context } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/index.tsx ================================================ import React from 'react' import { HashRouter as Router, Routes, Route } from 'react-router-dom' import { Root } from '@lib/components' import { addTranslations } from '@lib/utils/i18n' import { useLocationChange } from '@lib/hooks/useLocationChange' import translations from './translations' import { TopSlowQueryList } from './pages/List' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' addTranslations(translations) // Create a client const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1 // refetchOnMount: false, // refetchOnReconnect: false, } } }) function AppRoutes() { useLocationChange() return ( } /> ) } export default function () { return ( // Provide the client to your App ) } export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/pages/CountChart.tsx ================================================ import React, { useMemo } from 'react' import { Axis, Chart, Position, ScaleType, Settings, timeFormatter, BrushEvent, BarSeries } from '@elastic/charts' import { DEFAULT_CHART_SETTINGS } from '@lib/utils/charts' import { getValueFormat } from '@baurine/grafana-value-formats' import { TimeRangeValue } from 'metrics-chart' export function CountChart({ data, timeRange, onSelectTimeRange }: { data: [number, number][] timeRange: TimeRangeValue onSelectTimeRange?: (timeRange: TimeRangeValue) => void }) { const convertedData = useMemo(() => { return (data || []).map(([time, count]) => [time * 1000, count]) }, [data]) function onBrushEnd(e: BrushEvent) { if (!e.x) { return } let value: [number, number] const tr = e.x.map((d) => d / 1000) const delta = tr[1] - tr[0] if (delta < 60) { const offset = Math.floor(delta / 2) value = [Math.ceil(tr[0] + offset - 30), Math.floor(tr[1] - offset + 30)] } else { value = [Math.ceil(tr[0]), Math.floor(tr[1])] } onSelectTimeRange?.(value) } return ( getValueFormat('short')(v, 0, 1)} ticks={5} /> ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/pages/List.module.less ================================================ @import 'antd/es/style/themes/default.less'; .container { display: flex; height: 100%; flex-direction: column; .chart_container { margin: 24px 24px 24px 48px; height: 200px; } } .sorted_column_header { background-color: #f3f2f1; } .slow_hint_container { position: relative; } .slow_hint { position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: flex; align-items: center; justify-content: center; span { color: #888; background-color: white; padding: 8px; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/pages/List.tsx ================================================ import React, { useRef, useMemo } from 'react' import { Space, Select, Typography, Button, Tag, Skeleton } from 'antd' import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons' import { Card, MultiSelect, TimeRangeValue, toTimeRangeValue } from '@lib/components' import styles from './List.module.less' import { useTopSlowQueryContext } from '../context' import { Link } from 'react-router-dom' import { useTopSlowQueryUrlState } from '../uilts/url-state' import { DEFAULT_TIME_RANGE, DURATIONS, ORDER_BY, STMT_KINDS } from '../uilts/helpers' import { useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' import { TopSlowQueryListTable } from './ListTable' import { CountChart } from './CountChart' import { telemetry } from '../uilts/telemetry' export function TopSlowQueryList() { const containerRef = useRef(null) return (
Top 10
) } function ClusterInfoHeader() { const ctx = useTopSlowQueryContext() // only for clinic const clusterInfo = useMemo(() => { const infos: string[] = [] if (ctx?.cfg.orgName) { infos.push(`Org: ${ctx?.cfg.orgName}`) } if (ctx?.cfg.clusterName) { infos.push(`Cluster: ${ctx?.cfg.clusterName}`) } return infos.join(' | ') }, [ctx?.cfg.orgName, ctx?.cfg.clusterName]) if (!clusterInfo) return null return (
{clusterInfo} Top SlowQueries beta | telemetry.clickSlowQueryTab()}> Slow Query Logs
) } function useTimeWindows() { const ctx = useTopSlowQueryContext() const { duration, tw, setTw, timeRange } = useTopSlowQueryUrlState() const query = useQuery({ queryKey: [ 'top_slowquery_time_windows', duration, timeRange, ctx.cfg.orgName, ctx.cfg.clusterName ], queryFn: () => { const timeVal = toTimeRangeValue(timeRange) return ctx.api.getAvailableTimeWindows({ from: timeVal[0], to: timeVal[1], duration }) }, onSuccess(data) { if (data.length === 0) { return } if (data.some((d) => d.begin_time === tw[0] && d.end_time === tw[1])) { return } setTw(`${data[0].begin_time}-${data[0].end_time}`) } }) return query } const timezone = dayjs().format('UTCZ') function TimeWindowSelect() { const { duration, setQueryParams, tw, setTw } = useTopSlowQueryUrlState() const { data: availableTimeWindows } = useTimeWindows() function newerTw() { if (!availableTimeWindows) { return } const idx = availableTimeWindows.findIndex( (item) => item.begin_time === tw[0] && item.end_time === tw[1] ) if (idx === -1 || idx === 0) { return } const item = availableTimeWindows[idx - 1] setTw(`${item.begin_time}-${item.end_time}`) } function olderTw() { if (!availableTimeWindows) { return } const idx = availableTimeWindows.findIndex( (item) => item.begin_time === tw[0] && item.end_time === tw[1] ) if (idx === -1 || idx === availableTimeWindows.length - 1) { return } const item = availableTimeWindows[idx + 1] setTw(`${item.begin_time}-${item.end_time}`) } return (
{/*
Time Range:
*/}
Duration:
Time Range: {' '}
Time Zone: {timezone}
) } function useChartData() { const ctx = useTopSlowQueryContext() const { tw } = useTopSlowQueryUrlState() const query = useQuery({ queryKey: [ 'top_slowquery_chart_data', ctx.cfg.orgName, ctx.cfg.clusterName, tw ], queryFn: () => { return ctx.api.getMetrics({ start: tw[0], end: tw[1] }) }, enabled: !!tw[0] }) return query } function SlowQueryCountChart() { const { data: chartData, isLoading } = useChartData() const { tw, setQueryParams } = useTopSlowQueryUrlState() function onSelectTimeRange(timeRange: TimeRangeValue) { const delta = timeRange[1] - timeRange[0] let duration = 60 * 60 if (delta < 60 * 60) { duration = 60 * 60 } else if (delta < 3 * 60 * 60) { duration = 3 * 60 * 60 } else if (delta < 6 * 60 * 60) { duration = 6 * 60 * 60 } else if (delta < 12 * 60 * 60) { duration = 12 * 60 * 60 } else if (delta < 24 * 60 * 60) { duration = 24 * 60 * 60 } else if (delta < 7 * 24 * 60 * 60) { duration = 7 * 24 * 60 * 60 } setQueryParams({ duration, from: timeRange[0], to: timeRange[0] }) } return (
Slow Query Count
) } function useDatabaseList() { const ctx = useTopSlowQueryContext() const { tw } = useTopSlowQueryUrlState() const query = useQuery({ queryKey: [ 'top_slowquery_database_list', ctx.cfg.orgName, ctx.cfg.clusterName, tw ], queryFn: () => { return ctx.api.getDatabaseList({ start: tw[0], end: tw[1] }) }, enabled: !!tw[0] }) return query } function TopSlowQueryFilters() { const { tw, dbs, setDbs, order, setOrder, stmtKinds, setStmtKinds } = useTopSlowQueryUrlState() const { data: databaseList } = useDatabaseList() return (
Databases: {/* this component has a weird bug, sometimes it can't select item after changing the time window, use `key` can fix it */} { telemetry.changeDatabases() setDbs(v) }} items={databaseList || []} />
Statement Kinds: { telemetry.changeStmtKinds() setStmtKinds(v) }} items={STMT_KINDS} />
{/*
Internal:
*/}
Order by:
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/pages/ListTable.tsx ================================================ import React, { useEffect, useMemo, useState } from 'react' import { useTopSlowQueryContext } from '../context' import { useTopSlowQueryUrlState } from '../uilts/url-state' import { useQuery } from '@tanstack/react-query' import { CSVLink } from 'react-csv' import { CardTable, HighlightSQL, TextWrap } from '@lib/components' import { ColumnActionsMode, IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { Tooltip } from 'antd' import { getValueFormat } from '@baurine/grafana-value-formats' import { useMemoizedFn } from 'ahooks' import { useNavigate } from 'react-router-dom' import openLink from '@lib/utils/openLink' import styles from './List.module.less' import { telemetry } from '../uilts/telemetry' function useTopSlowQueryData() { const ctx = useTopSlowQueryContext() const { tw, order, dbs, internal, stmtKinds } = useTopSlowQueryUrlState() const query = useQuery({ queryKey: [ 'top_slowquery_list', ctx.cfg.orgName, ctx.cfg.clusterName, tw, order, dbs, internal, stmtKinds ], queryFn: () => { return ctx.api.getTopSlowQueries({ start: tw[0], end: tw[1], order, dbs, internal, stmtKinds }) }, enabled: !!tw[0] }) return query } export function TopSlowQueryListTable() { const { tw, dbs, order, setOrder } = useTopSlowQueryUrlState() const { isLoading, isFetching, data: slowQueries } = useTopSlowQueryData() const navigate = useNavigate() const [loadSlow, setLoadSlow] = useState(false) useEffect(() => { if (!isFetching) { setLoadSlow(false) return } let timerId = window.setTimeout(() => { setLoadSlow(true) }, 10 * 1000) return () => { window.clearTimeout(timerId) } }, [isFetching]) const handleRowClick = useMemoizedFn( (rec, _idx, ev: React.MouseEvent) => { telemetry.clickTableRow() openLink( `/slow_query?from=${tw[0]}&to=${tw[1]}&digest=${ rec.sql_digest }&dbs=${dbs.join(',')}`, ev, navigate ) } ) const columns: IColumn[] = useMemo(() => { return [ { name: 'Query', key: 'sql_text', minWidth: 100, maxWidth: 500, onRender: (row: any) => { return ( } placement="right" > ) } }, { name: 'SQL Digest', key: 'sql_digest', minWidth: 100, maxWidth: 150, onRender: (row: any) => { return ( {row.sql_digest} ) } }, { name: 'Total Latency', headerClassName: order === 'sum_latency' ? styles.sorted_column_header : '', key: 'sum_latency', fieldName: 'sum_latency', minWidth: 100, maxWidth: 150, columnActionsMode: ColumnActionsMode.clickable, onRender: (row: any) => { return {getValueFormat('s')(row.sum_latency, 1)} } }, { name: 'Max Latency', headerClassName: order === 'max_latency' ? styles.sorted_column_header : '', key: 'max_latency', fieldName: 'max_latency', // fieldName is used to sort minWidth: 100, maxWidth: 150, columnActionsMode: ColumnActionsMode.clickable, onRender: (row: any) => { return {getValueFormat('s')(row.max_latency, 1)} } }, { name: 'Avg Latency', headerClassName: order === 'avg_latency' ? styles.sorted_column_header : '', key: 'avg_latency', fieldName: 'avg_latency', minWidth: 100, maxWidth: 150, columnActionsMode: ColumnActionsMode.clickable, onRender: (row: any) => { return {getValueFormat('s')(row.avg_latency, 1)} } }, { name: 'Total Memory', headerClassName: order === 'sum_memory' ? styles.sorted_column_header : '', key: 'sum_memory', fieldName: 'sum_memory', minWidth: 100, maxWidth: 150, columnActionsMode: ColumnActionsMode.clickable, onRender: (row: any) => { return {getValueFormat('bytes')(row.sum_memory, 1)} } }, { name: 'Max Memory', headerClassName: order === 'max_memory' ? styles.sorted_column_header : '', key: 'max_memory', fieldName: 'max_memory', minWidth: 100, maxWidth: 150, columnActionsMode: ColumnActionsMode.clickable, onRender: (row: any) => { return {getValueFormat('bytes')(row.max_memory, 1)} } }, { name: 'Avg Memory', headerClassName: order === 'avg_memory' ? styles.sorted_column_header : '', key: 'avg_memory', fieldName: 'avg_memory', minWidth: 100, maxWidth: 150, columnActionsMode: ColumnActionsMode.clickable, onRender: (row: any) => { return {getValueFormat('bytes')(row.avg_memory, 1)} } }, { name: 'Total Count', headerClassName: order === 'count' ? styles.sorted_column_header : '', key: 'count', fieldName: 'count', minWidth: 100, maxWidth: 150, columnActionsMode: ColumnActionsMode.clickable, onRender: (row: any) => { return {getValueFormat('short')(row.count, 0, 1)} } }, { name: 'Total Disk', key: 'sum_disk', fieldName: 'sum_disk', minWidth: 100, maxWidth: 120, onRender: (row: any) => { return {getValueFormat('bytes')(row.sum_disk, 1)} } } // { // name: 'Database', // key: 'database', // minWidth: 100, // maxWidth: 150, // onRender: (row: any) => { // return ( // // {row.schema_name} // // ) // } // }, // { // name: 'Table', // key: 'table', // minWidth: 100, // maxWidth: 150, // onRender: (row: any) => { // return ( // // {row.table_names} // // ) // } // } ] }, [order]) const csvHeaders = columns.map((c) => ({ label: c.name, key: c.key })) return (
{slowQueries && ( Download to CSV )} {loadSlow && (
We are working to prepare the data, please be patient.
)}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/translations/en.yaml ================================================ top_slowquery: nav_title: Top SlowQuery ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/translations/zh.yaml ================================================ top_slowquery: nav_title: Top SlowQuery ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/uilts/helpers.ts ================================================ import { TimeRange } from '@lib/components/TimeRangeSelector' export const TIME_RANGE_RECENT_SECONDS = [ 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60, 7 * 24 * 60 * 60, 30 * 24 * 60 * 60 ] export const DEFAULT_TIME_RANGE: TimeRange = { type: 'recent', value: TIME_RANGE_RECENT_SECONDS[6] } export const DURATIONS = [ { label: '1 hour', value: 60 * 60 }, { label: '3 hours', value: 3 * 60 * 60 }, { label: '6 hours', value: 6 * 60 * 60 }, { label: '12 hours', value: 12 * 60 * 60 }, { label: '1 day', value: 24 * 60 * 60 }, { label: '3 days', value: 3 * 24 * 60 * 60 } ] export const STMT_KINDS = [ 'AlterTable', 'AnalyzeTable', 'Begin', 'Change', 'Insert', 'Update', 'Commit', 'Delete', 'Select', 'Show', 'Set', 'Others' ] export const ORDER_BY = [ { label: 'Total Latency', value: 'sum_latency' }, { label: 'Max Latency', value: 'max_latency' }, { label: 'Avg Latency', value: 'avg_latency' }, { label: 'Total Memory', value: 'sum_memory' }, { label: 'Max Memory', value: 'max_memory' }, { label: 'Avg Memory', value: 'avg_memory' }, { label: 'Total Count', value: 'count' } ] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/uilts/telemetry.ts ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import { mixpanel } from '@lib/utils/telemetry' export const telemetry = { clickSlowQueryTab() { mixpanel.track('TopSlowquery: Click Slowquery Tab') }, changeDuration(duration: number) { mixpanel.track('TopSlowquery: Change Duration', { duration }) }, changeTimeRange() { mixpanel.track('TopSlowquery: Change Time Range') }, changeDatabases() { mixpanel.track('TopSlowquery: Change Databases') }, changeStmtKinds() { mixpanel.track('TopSlowquery: Change Statement Kinds') }, changeOrder() { mixpanel.track('TopSlowquery: Change Order') }, clickTableRow() { mixpanel.track('TopSlowquery: Click Table Row') } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/uilts/url-state.ts ================================================ import useUrlState from '@ahooksjs/use-url-state' import { TimeRange, toURLTimeRange, urlToTimeRange } from '@lib/components/TimeRangeSelector' import { useCallback, useMemo } from 'react' import { DEFAULT_TIME_RANGE, DURATIONS, ORDER_BY } from './helpers' // tw: time window (start-end) type UrlState = Partial< Record< | 'from' | 'to' | 'duration' | 'tw' | 'order' | 'dbs' | 'internal' | 'stmt_kinds', string > > export function useTopSlowQueryUrlState() { const [queryParams, setQueryParams] = useUrlState() const timeRange = useMemo(() => { const { from, to } = queryParams if (from && to) { return urlToTimeRange({ from, to }) } return DEFAULT_TIME_RANGE }, [queryParams.from, queryParams.to]) const setTimeRange = useCallback( (newTimeRange: TimeRange) => { setQueryParams({ ...toURLTimeRange(newTimeRange) }) }, [setQueryParams] ) const duration: number = useMemo(() => { const v = parseInt(queryParams.duration) if (isNaN(v)) { return DURATIONS[0].value } if (DURATIONS.some((s) => s.value === v)) { return v } return DURATIONS[0].value }, [queryParams.duration]) const setDuration = useCallback( (v: number) => { setQueryParams({ duration: v + '' }) }, [setQueryParams] ) const tw = useMemo(() => { const arr = queryParams.tw?.split('-') if (arr && arr.length === 2) { const s = parseInt(arr[0]) const e = parseInt(arr[1]) if (!isNaN(s) && !isNaN(e)) { return [s, e] } } return [0, 0] }, [queryParams.tw]) const setTw = useCallback( (v: string) => { // v format: "from-to" setQueryParams({ tw: v }) }, [setQueryParams] ) const order = queryParams.order || ORDER_BY[0].value const setOrder = useCallback( (v: string) => setQueryParams({ order: v }), [setQueryParams] ) // dbs const dbs = useMemo(() => { const dbs = queryParams.dbs return dbs ? dbs.split(',') : [] }, [queryParams.dbs]) const setDbs = useCallback( (v: string[]) => { setQueryParams({ dbs: v.join(',') }) }, [setQueryParams] ) const stmtKinds = useMemo(() => { const stmtTypes = queryParams.stmt_kinds return stmtTypes ? stmtTypes.split(',') : [] }, [queryParams.stmt_kinds]) const setStmtKinds = useCallback( (v: string[]) => { setQueryParams({ stmt_kinds: v.join(',') }) }, [setQueryParams] ) const internal = queryParams.internal || 'no' const setInternal = useCallback( (v: string) => setQueryParams({ internal: v }), [setQueryParams] ) return { timeRange, setTimeRange, duration, setDuration, tw, setTw, order, setOrder, dbs, setDbs, stmtKinds, setStmtKinds, internal, setInternal, queryParams, setQueryParams } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Language.tsx ================================================ import { Form, Select } from 'antd' import React, { useCallback } from 'react' import { DEFAULT_FORM_ITEM_STYLE } from '../utils/helper' import { ALL_LANGUAGES } from '@lib/utils/i18n' import _ from 'lodash' import { useTranslation } from 'react-i18next' export function LanguageForm() { const { t, i18n } = useTranslation() const handleLanguageChange = useCallback( (langKey) => { i18n.changeLanguage(langKey) }, [i18n] ) return (
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.PrometheusAddr.tsx ================================================ import { AnimatedSkeleton, Blink, ErrorBar } from '@lib/components' import { useIsWriteable } from '@lib/utils/store' import { useClientRequest } from '@lib/utils/useClientRequest' import { Button, Form, Input, Radio, Space, Typography } from 'antd' import React, { useContext } from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { DEFAULT_FORM_ITEM_STYLE } from '../utils/helper' import { UserProfileContext } from '../context' export function PrometheusAddressForm() { const ctx = useContext(UserProfileContext) const { t } = useTranslation() const isWriteable = useIsWriteable() const [isChanged, setIsChanged] = useState(false) const [isPosting, setIsPosting] = useState(false) const handleValuesChange = useCallback(() => setIsChanged(true), []) const { error, isLoading, data } = useClientRequest( ctx!.ds.metricsGetPromAddress ) const isInitialLoad = useRef(true) const initialForm = useRef(null) // Used for "Cancel" behaviour const [form] = Form.useForm() useEffect(() => { if (data && isInitialLoad.current) { isInitialLoad.current = false form.setFieldsValue({ sourceType: (data.customized_addr?.length ?? 0) > 0 ? 'custom' : 'deployment', customAddr: data.customized_addr }) initialForm.current = { ...form.getFieldsValue() } } }, [data, form]) const handleFinish = useCallback( async (values) => { let address = '' if (values.sourceType === 'custom') { address = values.customAddr || '' } try { setIsPosting(true) const resp = await ctx!.ds.metricsSetCustomPromAddress({ address }) const customAddr = resp?.data?.normalized_address ?? '' form.setFieldsValue({ customAddr }) initialForm.current = { ...form.getFieldsValue() } setIsChanged(false) } finally { setIsPosting(false) } }, [form, ctx] ) const handleCancel = useCallback(() => { form.setFieldsValue({ ...initialForm.current }) setIsChanged(false) }, [form]) return (
{error && } {t( 'user_profile.service_endpoints.prometheus.form.deployed' )} {(data?.deployed_addr?.length ?? 0) > 0 && `(${data!.deployed_addr})`} {data && data.deployed_addr?.length === 0 && ( ( {t( 'user_profile.service_endpoints.prometheus.form.not_deployed' )} ) )} {t('user_profile.service_endpoints.prometheus.form.custom')} {(f) => f.getFieldValue('sourceType') === 'custom' && ( ) } {isChanged && ( )}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.SSO.tsx ================================================ import { CheckCircleFilled } from '@ant-design/icons' import { SsoSSOImpersonationModel } from '@lib/client' import { AnimatedSkeleton, ErrorBar } from '@lib/components' import { useIsFeatureSupport, useIsWriteable } from '@lib/utils/store' import { useChange } from '@lib/utils/useChange' import { useClientRequest } from '@lib/utils/useClientRequest' import { Alert, Button, Checkbox, Form, Input, Modal, Space, Switch, Typography } from 'antd' import React, { useContext } from 'react' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { DEFAULT_FORM_ITEM_STYLE } from '../utils/helper' import { UserProfileContext } from '../context' interface IUserAuthInputProps { value?: SsoSSOImpersonationModel onChange?: (value: SsoSSOImpersonationModel) => void } function isImpersonationNotFailed(imp?: SsoSSOImpersonationModel) { return Boolean( imp && imp.last_impersonate_status !== 'auth_fail' && imp.last_impersonate_status !== 'insufficient_privileges' ) } function UserAuthInput({ value, onChange }: IUserAuthInputProps) { const ctx = useContext(UserProfileContext) const { t } = useTranslation() const [modalVisible, setModalVisible] = useState(false) const [isPosting, setIsPosting] = useState(false) const isWriteable = useIsWriteable() const handleClose = useCallback(() => { setModalVisible(false) }, []) const handleAuthnClick = useCallback(() => { setModalVisible(true) }, []) const supportNonRootLogin = useIsFeatureSupport('nonRootLogin') const handleFinish = useCallback( async (data) => { setIsPosting(true) try { const resp = await ctx!.ds.userSSOCreateImpersonation({ sql_user: data.user, password: data.password }) setModalVisible(false) onChange?.(resp.data) } finally { setIsPosting(false) } }, [onChange, ctx] ) return ( <> {Boolean(!value) && ( )} {Boolean(value) && ( {value!.sql_user} {isImpersonationNotFailed(value) && ( {' '} {t('user_profile.sso.form.user.authn_status.ok')} )} {value?.last_impersonate_status === 'auth_fail' && ( {' '} {t('user_profile.sso.form.user.authn_status.auth_failed')} )} {value?.last_impersonate_status === 'insufficient_privileges' && ( {' '} {t( 'user_profile.sso.form.user.authn_status.insufficient_privileges' )} )} )}
) } const UserAuthInputMemo = React.memo(UserAuthInput) export function SSOForm() { const ctx = useContext(UserProfileContext) const { t } = useTranslation() const [isChanged, setIsChanged] = useState(false) const [isPosting, setIsPosting] = useState(false) const handleValuesChange = useCallback(() => setIsChanged(true), []) const [form] = Form.useForm() const { error, isLoading, data: config, sendRequest } = useClientRequest(ctx!.ds.userSSOGetConfig) const { error: impError, isLoading: impIsLoading, data: impData, sendRequest: impSendRequest } = useClientRequest(ctx!.ds.userSSOListImpersonations) const initialForm = useRef(null) // Used for "Cancel" behaviour const isWriteable = useIsWriteable() useChange(() => { if (config) { form.setFieldsValue(config) initialForm.current = { ...config } } }, [config]) useChange(() => { if (impData) { let rootImp: SsoSSOImpersonationModel | undefined = impData[0] const update = { user_authenticated: rootImp } form.setFieldsValue(update) initialForm.current = { ...initialForm.current, ...update } } }, [impData]) // TODO: Extract common logic const handleCancel = useCallback(() => { form.setFieldsValue({ ...initialForm.current }) setIsChanged(false) }, [form]) const handleFinish = useCallback( async (data) => { setIsPosting(true) try { await ctx!.ds.userSSOSetConfig({ config: data }) sendRequest() setIsChanged(false) } finally { setIsPosting(false) } }, [sendRequest, ctx] ) const handleAuthStateChange = useCallback(() => { impSendRequest() }, [impSendRequest]) return (
{(error || impError) && } {(f) => f.getFieldValue('enabled') && ( <> { // to compatible with old version config?.client_secret !== undefined && ( ) } ) } {isChanged && ( )} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Session.tsx ================================================ import { CopyToClipboard } from 'react-copy-to-clipboard' import { CheckOutlined, CopyOutlined, LogoutOutlined, QuestionCircleOutlined, RollbackOutlined, ShareAltOutlined } from '@ant-design/icons' import { Alert, Button, DatePicker, Divider, Form, Input, message, Modal, Select, Space, Tooltip } from 'antd' import React, { useContext } from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { Pre } from '@lib/components' import { getValueFormat } from '@baurine/grafana-value-formats' import ReactMarkdown from 'react-markdown' import Checkbox from 'antd/lib/checkbox/Checkbox' import { store } from '@lib/utils/store' import { UserProfileContext } from '../context' import dayjs from 'dayjs' const SHARE_SESSION_EXPIRY_HOURS = [ 0.25, 0.5, 1, 2, 3, 6, 12, 24, 24 * 3, 24 * 7, 24 * 30, 24 * 365 ] function RevokeSessionButton() { const whoAmI = store.useState((s) => s.whoAmI) const { t } = useTranslation() const ctx = useContext(UserProfileContext) function showRevokeConfirm() { Modal.confirm({ title: t('user_profile.revoke_modal.title'), content: t('user_profile.revoke_modal.content'), okText: t('user_profile.revoke_modal.ok'), cancelText: t('user_profile.revoke_modal.cancel'), onOk() { ctx?.ds.userRevokeSession().then(() => { message.success(t('user_profile.revoke_modal.success_message')) }) } }) } let button = ( ) if (whoAmI && !whoAmI.is_shareable) { button = ( {button} ) } return <>{button} } function ShareSessionButton() { const ctx = useContext(UserProfileContext) const { t } = useTranslation() const [visible, setVisible] = useState(false) const [isPosting, setIsPosting] = useState(false) const [code, setCode] = useState(undefined) const [isCopied, setIsCopied] = useState(false) const whoAmI = store.useState((s) => s.whoAmI) const handleOpen = useCallback(() => { setVisible(true) }, []) const handleClose = useCallback(() => { setVisible(false) setCode(undefined) setIsPosting(false) setIsCopied(false) }, []) const handleFinish = useCallback( async (values) => { const expire = values['expire'] const expireCustom = values['expireCustom'] let expireInSec = 0 if (expire === 0) { // expireCustom has value because it is required expireInSec = dayjs.unix(expireCustom.unix()).endOf('day').unix() - dayjs().unix() } else if (expire === -1) { expireInSec = 100 * 365 * 24 * 60 * 60 // 100 years } else { expireInSec = expire * 60 * 60 } try { setIsPosting(true) const r = await ctx!.ds.userShareSession({ expire_in_sec: expireInSec, revoke_write_priv: !!values.read_only }) setCode(r.data.code) } finally { setIsPosting(false) } }, [ctx] ) const handleCopy = useCallback(() => { setIsCopied(true) }, []) let button = ( ) if (whoAmI && !whoAmI.is_shareable) { button = ( {button} ) } return ( <> {button} } visible={!!code} > {code}} type="success" showIcon /> {t('user_profile.share_session.text')}
prev.expire !== cur.expire} > {({ getFieldValue }) => { return ( getFieldValue('expire') === 0 && ( dayjs() .endOf('day') .isAfter(dayjs.unix(date.unix())) } /> ) ) }}
) } export function SessionForm() { const ctx = useContext(UserProfileContext) const { t } = useTranslation() const handleLogout = useCallback(async () => { let signOutURL: string | undefined = undefined try { const resp = await ctx!.ds.userGetSignOutInfo( `${window.location.protocol}//${window.location.host}${window.location.pathname}` ) signOutURL = resp.data.end_session_url } catch (e) { console.error(e) } ctx!.event.logOut() if (signOutURL) { window.location.href = signOutURL } else { window.location.reload() } }, [ctx]) return ( {/* only available for v8.4.0+, v6.5.11+ */} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Version.tsx ================================================ import { CopyLink, Descriptions, TextWithInfo } from '@lib/components' import { store } from '@lib/utils/store' import { Space } from 'antd' import React from 'react' export function VersionForm() { const info = store.useState((s) => s.appInfo) return ( <> {Boolean(info) && ( } > {info!.version?.internal_version} } > {info!.version?.build_git_hash} } > {info!.version?.build_time} } > {info!.version?.standalone} } > {info!.version?.pd_version} )} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/index.ts ================================================ export * from './Form.SSO' export * from './Form.Session' export * from './Form.PrometheusAddr' export * from './Form.Version' export * from './Form.Language' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/context/index.ts ================================================ import { createContext } from 'react' import { AxiosPromise } from 'axios' import { UserSignOutInfo, SsoCreateImpersonationRequest, SsoSSOImpersonationModel, ConfigSSOCoreConfig, SsoSetConfigRequest, CodeShareRequest, CodeShareResponse, MetricsGetPromAddressConfigResponse, MetricsPutCustomPromAddressRequest, MetricsPutCustomPromAddressResponse } from '@lib/client' import { ReqConfig } from '@lib/types' export interface IUserProfileDataSource { userGetSignOutInfo( redirectUrl?: string, options?: ReqConfig ): AxiosPromise userSSOCreateImpersonation( request: SsoCreateImpersonationRequest, options?: ReqConfig ): AxiosPromise userSSOGetConfig(options?: ReqConfig): AxiosPromise userSSOListImpersonations( options?: ReqConfig ): AxiosPromise> userSSOSetConfig( request: SsoSetConfigRequest, options?: ReqConfig ): AxiosPromise userShareSession( request: CodeShareRequest, options?: ReqConfig ): AxiosPromise userRevokeSession(options?: ReqConfig): AxiosPromise metricsGetPromAddress( options?: ReqConfig ): AxiosPromise metricsSetCustomPromAddress( request: MetricsPutCustomPromAddressRequest, options?: ReqConfig ): AxiosPromise } export interface IUserProfileEvent { logOut(): void } export interface IUserProfileContext { ds: IUserProfileDataSource event: IUserProfileEvent } export const UserProfileContext = createContext( null ) export const UserProfileProvider = UserProfileContext.Provider ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/index.tsx ================================================ import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' import { HashRouter as Router, Route, Routes } from 'react-router-dom' import { Card, Root } from '@lib/components' import { SSOForm, SessionForm, PrometheusAddressForm, VersionForm, LanguageForm } from './components' import { addTranslations } from '@lib/utils/i18n' import translations from './translations' import { UserProfileContext } from './context' import { useLocationChange } from '@lib/hooks/useLocationChange' addTranslations(translations) function UserProfile() { const { t } = useTranslation() return ( <> ) } function AppRoutes() { useLocationChange() return ( } /> ) } function App() { const ctx = useContext(UserProfileContext) if (ctx === null) { throw new Error('UserProfileContext must not be null') } return ( ) } export default App export * from './context' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/translations/en.yaml ================================================ user_profile: sso: title: Single Sign-On (SSO) switch: label: Enable to use SSO when sign into {{distro.tidb}} Dashboard extra: OIDC based SSO is supported form: client_id: OIDC Client ID client_secret: OIDC Client Secret client_secret_tooltip: Optional, it is needed when using "Client Secret Post" authentication method, and it is only can be seen when setting. scopes: Additional OIDC Scopes (space-separated) discovery_url: OIDC Discovery URL is_read_only: Sign in as read-only privilege user: label: Impersonate SQL User extra: The SSO signed-in user will be using {{distro.tidb}} Dashboard on behalf of this SQL user and shares its permissions. must_auth: You must authorize to continue authn_button: Authorize Impersonation modify_authn_button: Modify Authorization authn_dialog: title: Authorize Impersonation user: SQL User to Impersonate password: SQL User Password info: The password of the SQL user will be stored encrypted. The impersonation will fail after SQL user changes the password. submit: Authorize and Save close: Cancel authn_status: ok: Authorized auth_failed: 'Cannot impersonate: SQL user password is changed.' insufficient_privileges: 'Cannot impersonate: Has no sufficient privileges to accsss {{distro.tidb}} dashboard.' update: Update cancel: Cancel service_endpoints: title: Service Endpoints prometheus: title: Prometheus Data Source form: deployed: Use deployed address not_deployed: Prometheus is not deployed custom: Use customized address update: Update cancel: Cancel custom_form: address: Customize Prometheus Address i18n: title: Language & Localization language: Language session: title: Session sign_out: Sign Out share: Share Current Session share_unavailable_tooltip: Current session is not allowed to be shared revoke: Revoke Authorization Codes revoke_unavailable_tooltip: You have no permission to revoke the authorization codes share_session: text: > You can invite others to access this {{distro.tidb}} Dashboard by sharing your current session via an **Authorization Code**: - The Authorization Code can be used multiple times. - The shared session has the same privilege as your current session. - The shared session will be invalidated after the expiry time you specified. - The shared session can be revoked in advance by administrator. form: expire: Expire in no_expiration: No expiration custom_expiration: Custom read_only: Share as read-only privilege submit: Generate Authorization Code close: Close success_dialog: title: Authorization Code Generated copy: Copy copied: Copied revoke_modal: title: Are you sure you want to revoke all authorization codes? content: After revoking, all authorization codes that are authorized before can't be used to login again, and this action can't undo. ok: Revoke cancel: Cancel success_message: Revoke authorization codes successfully! version: title: Version Information internal_version: '{{distro.tidb}} Dashboard Internal Version' build_git_hash: '{{distro.tidb}} Dashboard Build Git Hash' build_time: '{{distro.tidb}} Dashboard Build Time' standalone: '{{distro.tidb}} Dashboard Run in Standalone Mode' pd_version: '{{distro.pd}} Version' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/translations/zh.yaml ================================================ user_profile: sso: title: 单点登录 (SSO) switch: label: 允许使用 SSO 登录到 {{distro.tidb}} Dashboard extra: 支持基于 OIDC 的 SSO 登录 form: client_id: OIDC Client ID client_secret: OIDC Client Secret client_secret_tooltip: 可选,使用 "Client Secret Post" 认证方法时需要,且仅在设置时可见。 scopes: 附加 OIDC Scope(空格分隔) discovery_url: OIDC Discovery URL is_read_only: 以只读权限登录 user: label: 实际登录 SQL 用户 extra: SSO 登录成功后将被视为使用该 SQL 用户登录使用 {{distro.tidb}} Dashboard,并具有该用户对应的操作权限。 must_auth: 必须完成授权后才能继续 authn_button: 授权登录 modify_authn_button: 修改授权 authn_dialog: title: SSO 登录授权 user: 实际被登录的 SQL 用户 password: SQL 用户的登录密码 info: 登录密码将被加密存储;在 SQL 用户修改密码后 SSO 登录将失败(可重新进行登录授权)。 submit: 授权并保存 close: 取消 authn_status: ok: 已授权 auth_failed: 授权失败:SQL 用户密码已变更 insufficient_privileges: 授权失败:缺少访问 {{distro.tidb}} Dashboard 所需的权限 update: 更新 cancel: 取消 service_endpoints: title: 服务端点 prometheus: title: Prometheus 数据源 form: deployed: 使用已部署的组件地址 not_deployed: 未部署 Prometheus 组件 custom: 使用自定义地址 update: 更新 cancel: 取消 custom_form: address: 自定义 Prometheus 数据源地址 i18n: title: 语言和本地化 language: 语言 session: title: 会话 sign_out: 登出 share: 分享当前会话 share_unavailable_tooltip: 当前会话被禁止分享 revoke: 撤消授权码 revoke_unavailable_tooltip: 你没有权限撤消授权码 share_session: text: > 您可以生成一个**授权码**来将您当前的会话分享给其他人,邀请他们使用该 {{distro.tidb}} Dashboard: - 授权码可以被重复使用。 - 分享的会话和您当前会话具有相同权限。 - 分享的会话将在您指定的有效时间后过期。 - 分享的会话可以被管理员提前撤消。 form: expire: 有效时间 no_expiration: 无过期 custom_expiration: 自定义 read_only: 以只读权限分享 submit: 生成授权码 close: 关闭 success_dialog: title: 授权码已生成 copy: 复制 copied: 已复制 revoke_modal: title: 你确定你要撤消所有的授权码吗? content: 撤消之后,所有之前授权的授权码都不能再用于登录,而且这个操作不能回滚。 ok: 撤消 cancel: 取消 success_message: 撤消授权码成功! version: title: 版本信息 internal_version: '{{distro.tidb}} Dashboard 内部版本号' build_git_hash: '{{distro.tidb}} Dashboard 编译 Git Hash' build_time: '{{distro.tidb}} Dashboard 编译时间' standalone: '{{distro.tidb}} Dashboard 运行于独立模式' pd_version: '{{distro.pd}} 版本号' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/UserProfile/utils/helper.ts ================================================ export const DEFAULT_FORM_ITEM_STYLE = { width: 200 } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/apps/index.ts ================================================ export { default as OverviewApp } from './Overview' export * from './Overview' export { default as MonitoringApp } from './Monitoring' export * from './Monitoring' export { default as ClusterInfoApp } from './ClusterInfo' export * from './ClusterInfo' export { default as TopSQLApp } from './TopSQL' export * from './TopSQL' export { default as TopSlowQueryApp } from './TopSlowQuery' export * from './TopSlowQuery' export { default as SQLAdvisorAPP } from './SQLAdvisor' export * from './SQLAdvisor' export { default as StatementApp } from './Statement' export * from './Statement' export { default as SlowQueryApp } from './SlowQuery' export * from './SlowQuery' export { default as KeyVizApp } from './KeyViz' export * from './KeyViz' export { default as SystemReportApp } from './SystemReport' export * from './SystemReport' export { default as SearchLogsApp } from './SearchLogs' export * from './SearchLogs' export { default as InstanceProfilingApp } from './InstanceProfiling' export * from './InstanceProfiling' export { default as ConProfilingApp } from './ContinuousProfiling' export * from './ContinuousProfiling' export { default as DebugAPIApp } from './DebugAPI' export * from './DebugAPI' export { default as QueryEditorApp } from './QueryEditor' export * from './QueryEditor' export { default as ConfigurationApp } from './Configuration' export * from './Configuration' export { default as UserProfileApp } from './UserProfile' export * from './UserProfile' export { default as DiagnoseApp } from './Diagnose' export * from './Diagnose' export { default as OptimizerTraceApp } from './OptimizerTrace' export * from './OptimizerTrace' export { default as DeadlockApp } from './Deadlock' export * from './Deadlock' export { default as ResourceManagerApp } from './ResourceManager' export * from './ResourceManager' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/client/clinic-extensions.d.ts ================================================ /** * Type augmentation for fields returned by clinic's NGM proxy that aren't * part of the dashboard's own swagger spec. * * This file is NOT generated. Declarations here merge on top of the * interfaces generated into `./models.ts`, so any future re-generation of * the swagger client does NOT overwrite these fields. * * Background: clinic backend (PR pingcap-inc/clinic#1415) returns additional * RU V2 metrics on slow-query / sql-statement responses. The dashboard's own * Go API server does not yet model these fields, so the auto-generated * TypeScript model files don't declare them. This file fills the gap purely * on the TS side for the clinic-cloud deliverable. * * Visibility of these fields in the UI is independently gated by * `ISlowQueryConfig.showRuV2` / `IStatementConfig.showRuV2`, so other * deliverables (standalone TiDB dashboard, clinic-op) are unaffected. */ import '@lib/client' declare module '@lib/client' { interface RequestUnitV2Metrics { total_ru?: number tidb_ru?: number tikv_ru?: number tiflash_ru?: number txn_cnt?: number plan_cnt?: number plan_derive_stats_paths?: number session_parser_total?: number executor_l1?: number executor_l2?: number executor_l3?: number executor_l5_insert_rows?: number result_chunk_cells?: number resource_manager_read_cnt?: number resource_manager_write_cnt?: number tikv_coprocessor_executor_iterations?: number tikv_coprocessor_response_bytes?: number tikv_coprocessor_executor_work_total?: Record tikv_storage_processed_keys_get?: number tikv_storage_processed_keys_batch_get?: number tikv_kv_engine_cache_miss?: number tikv_raftstore_store_write_trigger_wb_bytes?: number } interface SlowqueryModel { ru_v2?: number ru_v2_detail?: string ru_v2_metrics?: RequestUnitV2Metrics } interface StatementModel { avg_ru_v2?: number sum_ru_v2?: number max_ru_v2?: number } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/client/index.ts ================================================ export * from './models' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/client/models.ts ================================================ /* tslint:disable */ /* eslint-disable */ /* This file is auto generated by tidb-dashboard-client/swagger/gen_api.sh */ /** * * @export * @interface ClusterinfoClusterStatisticsPartial */ export interface ClusterinfoClusterStatisticsPartial { /** * * @type {number} * @memberof ClusterinfoClusterStatisticsPartial */ 'number_of_hosts'?: number; /** * * @type {number} * @memberof ClusterinfoClusterStatisticsPartial */ 'number_of_instances'?: number; /** * * @type {number} * @memberof ClusterinfoClusterStatisticsPartial */ 'total_logical_cores'?: number; /** * * @type {number} * @memberof ClusterinfoClusterStatisticsPartial */ 'total_memory_capacity_bytes'?: number; /** * * @type {number} * @memberof ClusterinfoClusterStatisticsPartial */ 'total_physical_cores'?: number; } /** * * @export * @interface ClusterinfoClusterStatistics */ export interface ClusterinfoClusterStatistics { /** * * @type {number} * @memberof ClusterinfoClusterStatistics */ 'probe_failure_hosts'?: number; /** * * @type {{ [key: string]: ClusterinfoClusterStatisticsPartial; }} * @memberof ClusterinfoClusterStatistics */ 'stats_by_instance_kind'?: { [key: string]: ClusterinfoClusterStatisticsPartial; }; /** * * @type {ClusterinfoClusterStatisticsPartial} * @memberof ClusterinfoClusterStatistics */ 'total_stats'?: ClusterinfoClusterStatisticsPartial; /** * * @type {Array} * @memberof ClusterinfoClusterStatistics */ 'versions'?: Array; } /** * * @export * @interface ClusterinfoGetHostsInfoResponse */ export interface ClusterinfoGetHostsInfoResponse { /** * * @type {Array} * @memberof ClusterinfoGetHostsInfoResponse */ 'hosts'?: Array; /** * * @type {RestErrorResponse} * @memberof ClusterinfoGetHostsInfoResponse */ 'warning'?: RestErrorResponse; } /** * * @export * @interface ClusterinfoStoreTopologyResponse */ export interface ClusterinfoStoreTopologyResponse { /** * * @type {Array} * @memberof ClusterinfoStoreTopologyResponse */ 'tiflash'?: Array; /** * * @type {Array} * @memberof ClusterinfoStoreTopologyResponse */ 'tikv'?: Array; } /** * * @export * @interface CodeShareRequest */ export interface CodeShareRequest { /** * * @type {number} * @memberof CodeShareRequest */ 'expire_in_sec'?: number; /** * * @type {boolean} * @memberof CodeShareRequest */ 'revoke_write_priv'?: boolean; } /** * * @export * @interface CodeShareResponse */ export interface CodeShareResponse { /** * * @type {string} * @memberof CodeShareResponse */ 'code'?: string; } /** * * @export * @interface ConfigKeyVisualConfig */ export interface ConfigKeyVisualConfig { /** * * @type {boolean} * @memberof ConfigKeyVisualConfig */ 'auto_collection_disabled'?: boolean; /** * * @type {string} * @memberof ConfigKeyVisualConfig */ 'policy'?: string; /** * * @type {string} * @memberof ConfigKeyVisualConfig */ 'policy_kv_separator'?: string; } /** * * @export * @interface ConfigProfilingConfig */ export interface ConfigProfilingConfig { /** * * @type {number} * @memberof ConfigProfilingConfig */ 'auto_collection_duration_secs'?: number; /** * * @type {number} * @memberof ConfigProfilingConfig */ 'auto_collection_interval_secs'?: number; /** * * @type {Array} * @memberof ConfigProfilingConfig */ 'auto_collection_targets'?: Array; } /** * * @export * @interface ConfigSSOCoreConfig */ export interface ConfigSSOCoreConfig { /** * * @type {string} * @memberof ConfigSSOCoreConfig */ 'client_id'?: string; /** * * @type {string} * @memberof ConfigSSOCoreConfig */ 'client_secret'?: string; /** * * @type {string} * @memberof ConfigSSOCoreConfig */ 'discovery_url'?: string; /** * * @type {boolean} * @memberof ConfigSSOCoreConfig */ 'enabled'?: boolean; /** * * @type {boolean} * @memberof ConfigSSOCoreConfig */ 'is_read_only'?: boolean; /** * * @type {string} * @memberof ConfigSSOCoreConfig */ 'scopes'?: string; } /** * * @export * @interface ConfigurationAllConfigItems */ export interface ConfigurationAllConfigItems { /** * * @type {Array} * @memberof ConfigurationAllConfigItems */ 'errors'?: Array; /** * * @type {{ [key: string]: Array; }} * @memberof ConfigurationAllConfigItems */ 'items'?: { [key: string]: Array; }; } /** * * @export * @interface ConfigurationEditRequest */ export interface ConfigurationEditRequest { /** * * @type {string} * @memberof ConfigurationEditRequest */ 'id'?: string; /** * * @type {string} * @memberof ConfigurationEditRequest */ 'kind'?: string; /** * * @type {object} * @memberof ConfigurationEditRequest */ 'new_value'?: object; } /** * * @export * @interface ConfigurationEditResponse */ export interface ConfigurationEditResponse { /** * * @type {Array} * @memberof ConfigurationEditResponse */ 'warnings'?: Array; } /** * * @export * @interface ConfigurationItem */ export interface ConfigurationItem { /** * * @type {string} * @memberof ConfigurationItem */ 'id'?: string; /** * * @type {boolean} * @memberof ConfigurationItem */ 'is_editable'?: boolean; /** * TODO: Support per-instance config * @type {boolean} * @memberof ConfigurationItem */ 'is_multi_value'?: boolean; /** * When multi value present, this contains one of the value * @type {object} * @memberof ConfigurationItem */ 'value'?: object; } /** * * @export * @interface ConprofComponentNum */ export interface ConprofComponentNum { /** * * @type {number} * @memberof ConprofComponentNum */ 'pd'?: number; /** * * @type {number} * @memberof ConprofComponentNum */ 'ticdc'?: number; /** * * @type {number} * @memberof ConprofComponentNum */ 'tidb'?: number; /** * * @type {number} * @memberof ConprofComponentNum */ 'tiflash'?: number; /** * * @type {number} * @memberof ConprofComponentNum */ 'tikv'?: number; } /** * * @export * @interface ConprofComponent */ export interface ConprofComponent { /** * * @type {string} * @memberof ConprofComponent */ 'ip'?: string; /** * * @type {string} * @memberof ConprofComponent */ 'name'?: string; /** * * @type {number} * @memberof ConprofComponent */ 'port'?: number; /** * * @type {number} * @memberof ConprofComponent */ 'status_port'?: number; } /** * * @export * @interface ConprofContinuousProfilingConfig */ export interface ConprofContinuousProfilingConfig { /** * * @type {number} * @memberof ConprofContinuousProfilingConfig */ 'data_retention_seconds'?: number; /** * * @type {boolean} * @memberof ConprofContinuousProfilingConfig */ 'enable'?: boolean; /** * * @type {number} * @memberof ConprofContinuousProfilingConfig */ 'interval_seconds'?: number; /** * * @type {number} * @memberof ConprofContinuousProfilingConfig */ 'profile_seconds'?: number; /** * * @type {number} * @memberof ConprofContinuousProfilingConfig */ 'timeout_seconds'?: number; } /** * * @export * @interface ConprofEstimateSizeRes */ export interface ConprofEstimateSizeRes { /** * * @type {number} * @memberof ConprofEstimateSizeRes */ 'instance_count'?: number; /** * * @type {number} * @memberof ConprofEstimateSizeRes */ 'profile_size'?: number; } /** * * @export * @interface ConprofGroupProfileDetail */ export interface ConprofGroupProfileDetail { /** * * @type {number} * @memberof ConprofGroupProfileDetail */ 'profile_duration_secs'?: number; /** * * @type {string} * @memberof ConprofGroupProfileDetail */ 'state'?: string; /** * * @type {Array} * @memberof ConprofGroupProfileDetail */ 'target_profiles'?: Array; /** * * @type {number} * @memberof ConprofGroupProfileDetail */ 'ts'?: number; } /** * * @export * @interface ConprofGroupProfiles */ export interface ConprofGroupProfiles { /** * * @type {ConprofComponentNum} * @memberof ConprofGroupProfiles */ 'component_num'?: ConprofComponentNum; /** * * @type {number} * @memberof ConprofGroupProfiles */ 'profile_duration_secs'?: number; /** * * @type {string} * @memberof ConprofGroupProfiles */ 'state'?: string; /** * * @type {number} * @memberof ConprofGroupProfiles */ 'ts'?: number; } /** * * @export * @interface ConprofNgMonitoringConfig */ export interface ConprofNgMonitoringConfig { /** * * @type {ConprofContinuousProfilingConfig} * @memberof ConprofNgMonitoringConfig */ 'continuous_profiling'?: ConprofContinuousProfilingConfig; } /** * * @export * @interface ConprofProfileDetail */ export interface ConprofProfileDetail { /** * * @type {string} * @memberof ConprofProfileDetail */ 'error'?: string; /** * * @type {string} * @memberof ConprofProfileDetail */ 'profile_type'?: string; /** * * @type {string} * @memberof ConprofProfileDetail */ 'state'?: string; /** * * @type {ConprofTarget} * @memberof ConprofProfileDetail */ 'target'?: ConprofTarget; } /** * * @export * @interface ConprofTarget */ export interface ConprofTarget { /** * * @type {string} * @memberof ConprofTarget */ 'address'?: string; /** * * @type {string} * @memberof ConprofTarget */ 'component'?: string; } /** * * @export * @interface DeadlockModel */ export interface DeadlockModel { /** * * @type {string} * @memberof DeadlockModel */ 'current_sql'?: string; /** * * @type {number} * @memberof DeadlockModel */ 'id'?: number; /** * * @type {string} * @memberof DeadlockModel */ 'instance'?: string; /** * * @type {string} * @memberof DeadlockModel */ 'key'?: string; /** * * @type {string} * @memberof DeadlockModel */ 'key_info'?: string; /** * * @type {string} * @memberof DeadlockModel */ 'occur_time'?: string; /** * * @type {boolean} * @memberof DeadlockModel */ 'retryable'?: boolean; /** * * @type {number} * @memberof DeadlockModel */ 'trx_holding_lock'?: number; /** * * @type {number} * @memberof DeadlockModel */ 'try_lock_trx_id'?: number; } /** * * @export * @interface DecoratorLabelKey */ export interface DecoratorLabelKey { /** * * @type {string} * @memberof DecoratorLabelKey */ 'key': string; /** * * @type {Array} * @memberof DecoratorLabelKey */ 'labels': Array; } /** * * @export * @interface DiagnoseGenDiagnosisReportRequest */ export interface DiagnoseGenDiagnosisReportRequest { /** * * @type {number} * @memberof DiagnoseGenDiagnosisReportRequest */ 'end_time'?: number; /** * values: config, error, performance * @type {string} * @memberof DiagnoseGenDiagnosisReportRequest */ 'kind'?: string; /** * * @type {number} * @memberof DiagnoseGenDiagnosisReportRequest */ 'start_time'?: number; } /** * * @export * @interface DiagnoseGenerateMetricsRelationRequest */ export interface DiagnoseGenerateMetricsRelationRequest { /** * * @type {number} * @memberof DiagnoseGenerateMetricsRelationRequest */ 'end_time'?: number; /** * * @type {number} * @memberof DiagnoseGenerateMetricsRelationRequest */ 'start_time'?: number; /** * * @type {string} * @memberof DiagnoseGenerateMetricsRelationRequest */ 'type'?: string; } /** * * @export * @interface DiagnoseGenerateReportRequest */ export interface DiagnoseGenerateReportRequest { /** * * @type {number} * @memberof DiagnoseGenerateReportRequest */ 'compare_end_time'?: number; /** * * @type {number} * @memberof DiagnoseGenerateReportRequest */ 'compare_start_time'?: number; /** * * @type {number} * @memberof DiagnoseGenerateReportRequest */ 'end_time'?: number; /** * * @type {number} * @memberof DiagnoseGenerateReportRequest */ 'start_time'?: number; } /** * * @export * @interface DiagnoseReport */ export interface DiagnoseReport { /** * * @type {string} * @memberof DiagnoseReport */ 'compare_end_time'?: string; /** * * @type {string} * @memberof DiagnoseReport */ 'compare_start_time'?: string; /** * * @type {string} * @memberof DiagnoseReport */ 'content'?: string; /** * * @type {string} * @memberof DiagnoseReport */ 'created_at'?: string; /** * * @type {string} * @memberof DiagnoseReport */ 'end_time'?: string; /** * * @type {string} * @memberof DiagnoseReport */ 'id'?: string; /** * 0~100 * @type {number} * @memberof DiagnoseReport */ 'progress'?: number; /** * * @type {string} * @memberof DiagnoseReport */ 'start_time'?: string; } /** * * @export * @interface DiagnoseTableDef */ export interface DiagnoseTableDef { /** * The category of the table, such as [TiDB] * @type {Array} * @memberof DiagnoseTableDef */ 'category'?: Array; /** * * @type {Array} * @memberof DiagnoseTableDef */ 'column'?: Array; /** * * @type {string} * @memberof DiagnoseTableDef */ 'comment'?: string; /** * * @type {Array} * @memberof DiagnoseTableDef */ 'rows'?: Array; /** * * @type {string} * @memberof DiagnoseTableDef */ 'title'?: string; } /** * * @export * @interface DiagnoseTableRowDef */ export interface DiagnoseTableRowDef { /** * * @type {string} * @memberof DiagnoseTableRowDef */ 'comment'?: string; /** * SubValues need fold default. * @type {Array>} * @memberof DiagnoseTableRowDef */ 'sub_values'?: Array>; /** * * @type {Array} * @memberof DiagnoseTableRowDef */ 'values'?: Array; } /** * * @export * @interface EndpointAPIDefinition */ export interface EndpointAPIDefinition { /** * * @type {string} * @memberof EndpointAPIDefinition */ 'component'?: string; /** * * @type {string} * @memberof EndpointAPIDefinition */ 'id'?: string; /** * * @type {string} * @memberof EndpointAPIDefinition */ 'method'?: string; /** * * @type {string} * @memberof EndpointAPIDefinition */ 'path'?: string; /** * e.g. /stats/dump/{db}/{table} -> db, table * @type {Array} * @memberof EndpointAPIDefinition */ 'path_params'?: Array; /** * e.g. /debug/pprof?seconds=1 -> seconds * @type {Array} * @memberof EndpointAPIDefinition */ 'query_params'?: Array; } /** * * @export * @interface EndpointAPIParamDefinition */ export interface EndpointAPIParamDefinition { /** * * @type {string} * @memberof EndpointAPIParamDefinition */ 'name'?: string; /** * * @type {boolean} * @memberof EndpointAPIParamDefinition */ 'required'?: boolean; /** * * @type {string} * @memberof EndpointAPIParamDefinition */ 'ui_kind'?: string; /** * varies by different ui kinds * @type {object} * @memberof EndpointAPIParamDefinition */ 'ui_props'?: object; } /** * * @export * @interface EndpointRequestPayload */ export interface EndpointRequestPayload { /** * * @type {string} * @memberof EndpointRequestPayload */ 'api_id'?: string; /** * * @type {string} * @memberof EndpointRequestPayload */ 'host'?: string; /** * * @type {{ [key: string]: string; }} * @memberof EndpointRequestPayload */ 'param_values'?: { [key: string]: string; }; /** * * @type {number} * @memberof EndpointRequestPayload */ 'port'?: number; } /** * * @export * @interface HostinfoCPUInfo */ export interface HostinfoCPUInfo { /** * * @type {string} * @memberof HostinfoCPUInfo */ 'arch'?: string; /** * * @type {number} * @memberof HostinfoCPUInfo */ 'logical_cores'?: number; /** * * @type {number} * @memberof HostinfoCPUInfo */ 'physical_cores'?: number; } /** * * @export * @interface HostinfoCPUUsageInfo */ export interface HostinfoCPUUsageInfo { /** * * @type {number} * @memberof HostinfoCPUUsageInfo */ 'idle'?: number; /** * * @type {number} * @memberof HostinfoCPUUsageInfo */ 'system'?: number; } /** * * @export * @interface HostinfoInfo */ export interface HostinfoInfo { /** * * @type {HostinfoCPUInfo} * @memberof HostinfoInfo */ 'cpu_info'?: HostinfoCPUInfo; /** * * @type {HostinfoCPUUsageInfo} * @memberof HostinfoInfo */ 'cpu_usage'?: HostinfoCPUUsageInfo; /** * * @type {string} * @memberof HostinfoInfo */ 'host'?: string; /** * Instances in the current host. The key is instance address * @type {{ [key: string]: HostinfoInstanceInfo; }} * @memberof HostinfoInfo */ 'instances'?: { [key: string]: HostinfoInstanceInfo; }; /** * * @type {HostinfoMemoryUsageInfo} * @memberof HostinfoInfo */ 'memory_usage'?: HostinfoMemoryUsageInfo; /** * 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. * @type {{ [key: string]: HostinfoPartitionInfo; }} * @memberof HostinfoInfo */ 'partitions'?: { [key: string]: HostinfoPartitionInfo; }; } /** * * @export * @interface HostinfoInstanceInfo */ export interface HostinfoInstanceInfo { /** * * @type {string} * @memberof HostinfoInstanceInfo */ 'partition_path_lower'?: string; /** * * @type {string} * @memberof HostinfoInstanceInfo */ 'type'?: string; } /** * * @export * @interface HostinfoMemoryUsageInfo */ export interface HostinfoMemoryUsageInfo { /** * * @type {number} * @memberof HostinfoMemoryUsageInfo */ 'total'?: number; /** * * @type {number} * @memberof HostinfoMemoryUsageInfo */ 'used'?: number; } /** * * @export * @interface HostinfoPartitionInfo */ export interface HostinfoPartitionInfo { /** * * @type {number} * @memberof HostinfoPartitionInfo */ 'free'?: number; /** * * @type {string} * @memberof HostinfoPartitionInfo */ 'fstype'?: string; /** * * @type {string} * @memberof HostinfoPartitionInfo */ 'path'?: string; /** * * @type {number} * @memberof HostinfoPartitionInfo */ 'total'?: number; } /** * * @export * @interface InfoInfoResponse */ export interface InfoInfoResponse { /** * * @type {boolean} * @memberof InfoInfoResponse */ 'enable_experimental'?: boolean; /** * * @type {boolean} * @memberof InfoInfoResponse */ 'enable_telemetry'?: boolean; /** * * @type {string} * @memberof InfoInfoResponse */ 'ngm_state'?: string; /** * * @type {Array} * @memberof InfoInfoResponse */ 'supported_features'?: Array; /** * * @type {VersionInfo} * @memberof InfoInfoResponse */ 'version'?: VersionInfo; } /** * * @export * @interface InfoTableSchema */ export interface InfoTableSchema { /** * * @type {string} * @memberof InfoTableSchema */ 'table_id'?: string; /** * * @type {string} * @memberof InfoTableSchema */ 'table_name'?: string; } /** * * @export * @interface InfoWhoAmIResponse */ export interface InfoWhoAmIResponse { /** * * @type {string} * @memberof InfoWhoAmIResponse */ 'display_name'?: string; /** * * @type {boolean} * @memberof InfoWhoAmIResponse */ 'is_shareable'?: boolean; /** * * @type {boolean} * @memberof InfoWhoAmIResponse */ 'is_writeable'?: boolean; } /** * * @export * @interface LogsearchCreateTaskGroupRequest */ export interface LogsearchCreateTaskGroupRequest { /** * * @type {LogsearchSearchLogRequest} * @memberof LogsearchCreateTaskGroupRequest */ 'request': LogsearchSearchLogRequest; /** * * @type {Array} * @memberof LogsearchCreateTaskGroupRequest */ 'targets': Array; } /** * * @export * @interface LogsearchPreviewModel */ export interface LogsearchPreviewModel { /** * * @type {number} * @memberof LogsearchPreviewModel */ 'id'?: number; /** * * @type {number} * @memberof LogsearchPreviewModel */ 'level'?: number; /** * * @type {string} * @memberof LogsearchPreviewModel */ 'message'?: string; /** * * @type {number} * @memberof LogsearchPreviewModel */ 'task_group_id'?: number; /** * * @type {number} * @memberof LogsearchPreviewModel */ 'task_id'?: number; /** * * @type {number} * @memberof LogsearchPreviewModel */ 'time'?: number; } /** * * @export * @interface LogsearchSearchLogRequest */ export interface LogsearchSearchLogRequest { /** * * @type {number} * @memberof LogsearchSearchLogRequest */ 'end_time'?: number; /** * * @type {number} * @memberof LogsearchSearchLogRequest */ 'min_level'?: number; /** * 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) * @type {Array} * @memberof LogsearchSearchLogRequest */ 'patterns'?: Array; /** * * @type {number} * @memberof LogsearchSearchLogRequest */ 'start_time'?: number; } /** * * @export * @interface LogsearchTaskGroupModel */ export interface LogsearchTaskGroupModel { /** * * @type {number} * @memberof LogsearchTaskGroupModel */ 'id'?: number; /** * * @type {string} * @memberof LogsearchTaskGroupModel */ 'log_store_dir'?: string; /** * * @type {LogsearchSearchLogRequest} * @memberof LogsearchTaskGroupModel */ 'search_request'?: LogsearchSearchLogRequest; /** * * @type {number} * @memberof LogsearchTaskGroupModel */ 'state'?: number; /** * * @type {ModelRequestTargetStatistics} * @memberof LogsearchTaskGroupModel */ 'target_stats'?: ModelRequestTargetStatistics; } /** * * @export * @interface LogsearchTaskGroupResponse */ export interface LogsearchTaskGroupResponse { /** * * @type {LogsearchTaskGroupModel} * @memberof LogsearchTaskGroupResponse */ 'task_group'?: LogsearchTaskGroupModel; /** * * @type {Array} * @memberof LogsearchTaskGroupResponse */ 'tasks'?: Array; } /** * * @export * @interface LogsearchTaskModel */ export interface LogsearchTaskModel { /** * * @type {string} * @memberof LogsearchTaskModel */ 'error'?: string; /** * * @type {number} * @memberof LogsearchTaskModel */ 'id'?: number; /** * * @type {string} * @memberof LogsearchTaskModel */ 'log_store_path'?: string; /** * * @type {number} * @memberof LogsearchTaskModel */ 'size'?: number; /** * * @type {string} * @memberof LogsearchTaskModel */ 'slow_log_store_path'?: string; /** * * @type {number} * @memberof LogsearchTaskModel */ 'state'?: number; /** * * @type {ModelRequestTargetNode} * @memberof LogsearchTaskModel */ 'target'?: ModelRequestTargetNode; /** * * @type {number} * @memberof LogsearchTaskModel */ 'task_group_id'?: number; } /** * * @export * @interface MatrixMatrix */ export interface MatrixMatrix { /** * * @type {{ [key: string]: Array>; }} * @memberof MatrixMatrix */ 'data': { [key: string]: Array>; }; /** * * @type {Array} * @memberof MatrixMatrix */ 'keyAxis': Array; /** * * @type {Array} * @memberof MatrixMatrix */ 'timeAxis': Array; } /** * * @export * @interface MetricsGetPromAddressConfigResponse */ export interface MetricsGetPromAddressConfigResponse { /** * * @type {string} * @memberof MetricsGetPromAddressConfigResponse */ 'customized_addr'?: string; /** * * @type {string} * @memberof MetricsGetPromAddressConfigResponse */ 'deployed_addr'?: string; } /** * * @export * @interface MetricsPutCustomPromAddressRequest */ export interface MetricsPutCustomPromAddressRequest { /** * * @type {string} * @memberof MetricsPutCustomPromAddressRequest */ 'address'?: string; } /** * * @export * @interface MetricsPutCustomPromAddressResponse */ export interface MetricsPutCustomPromAddressResponse { /** * * @type {string} * @memberof MetricsPutCustomPromAddressResponse */ 'normalized_address'?: string; } /** * * @export * @interface MetricsQueryResponse */ export interface MetricsQueryResponse { /** * * @type {object} * @memberof MetricsQueryResponse */ 'data'?: object; /** * * @type {string} * @memberof MetricsQueryResponse */ 'status'?: string; } /** * * @export * @interface ModelRequestTargetNode */ export interface ModelRequestTargetNode { /** * * @type {string} * @memberof ModelRequestTargetNode */ 'display_name'?: string; /** * * @type {string} * @memberof ModelRequestTargetNode */ 'ip'?: string; /** * * @type {string} * @memberof ModelRequestTargetNode */ 'kind'?: string; /** * * @type {number} * @memberof ModelRequestTargetNode */ 'port'?: number; } /** * * @export * @interface ModelRequestTargetStatistics */ export interface ModelRequestTargetStatistics { /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_pd_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_scheduling_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_ticdc_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_tidb_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_tiflash_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_tikv_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_tiproxy_nodes'?: number; /** * * @type {number} * @memberof ModelRequestTargetStatistics */ 'num_tso_nodes'?: number; } /** * * @export * @interface ProfilingGroupDetailResponse */ export interface ProfilingGroupDetailResponse { /** * * @type {number} * @memberof ProfilingGroupDetailResponse */ 'server_time'?: number; /** * * @type {ProfilingTaskGroupModel} * @memberof ProfilingGroupDetailResponse */ 'task_group_status'?: ProfilingTaskGroupModel; /** * * @type {Array} * @memberof ProfilingGroupDetailResponse */ 'tasks_status'?: Array; } /** * * @export * @interface ProfilingStartRequest */ export interface ProfilingStartRequest { /** * * @type {number} * @memberof ProfilingStartRequest */ 'duration_secs'?: number; /** * * @type {Array} * @memberof ProfilingStartRequest */ 'requsted_profiling_types'?: Array; /** * * @type {Array} * @memberof ProfilingStartRequest */ 'targets'?: Array; } /** * * @export * @interface ProfilingTaskGroupModel */ export interface ProfilingTaskGroupModel { /** * * @type {number} * @memberof ProfilingTaskGroupModel */ 'id'?: number; /** * * @type {number} * @memberof ProfilingTaskGroupModel */ 'profile_duration_secs'?: number; /** * * @type {Array} * @memberof ProfilingTaskGroupModel */ 'requsted_profiling_types'?: Array; /** * * @type {number} * @memberof ProfilingTaskGroupModel */ 'started_at'?: number; /** * * @type {number} * @memberof ProfilingTaskGroupModel */ 'state'?: number; /** * * @type {ModelRequestTargetStatistics} * @memberof ProfilingTaskGroupModel */ 'target_stats'?: ModelRequestTargetStatistics; } /** * * @export * @interface ProfilingTaskModel */ export interface ProfilingTaskModel { /** * * @type {string} * @memberof ProfilingTaskModel */ 'error'?: string; /** * * @type {number} * @memberof ProfilingTaskModel */ 'id'?: number; /** * * @type {string} * @memberof ProfilingTaskModel */ 'profiling_type'?: string; /** * * @type {string} * @memberof ProfilingTaskModel */ 'raw_data_type'?: string; /** * The start running time, reset when retry. Used to estimate approximate profiling progress. * @type {number} * @memberof ProfilingTaskModel */ 'started_at'?: number; /** * * @type {number} * @memberof ProfilingTaskModel */ 'state'?: number; /** * * @type {ModelRequestTargetNode} * @memberof ProfilingTaskModel */ 'target'?: ModelRequestTargetNode; /** * * @type {number} * @memberof ProfilingTaskModel */ 'task_group_id'?: number; } /** * * @export * @interface QueryeditorRunRequest */ export interface QueryeditorRunRequest { /** * * @type {number} * @memberof QueryeditorRunRequest */ 'max_rows'?: number; /** * * @type {string} * @memberof QueryeditorRunRequest */ 'statements'?: string; } /** * * @export * @interface QueryeditorRunResponse */ export interface QueryeditorRunResponse { /** * * @type {number} * @memberof QueryeditorRunResponse */ 'actual_rows'?: number; /** * * @type {Array} * @memberof QueryeditorRunResponse */ 'column_names'?: Array; /** * * @type {string} * @memberof QueryeditorRunResponse */ 'error_msg'?: string; /** * * @type {number} * @memberof QueryeditorRunResponse */ 'execution_ms'?: number; /** * * @type {Array>} * @memberof QueryeditorRunResponse */ 'rows'?: Array>; } /** * * @export * @interface ResourcemanagerCalibrateResponse */ export interface ResourcemanagerCalibrateResponse { /** * * @type {number} * @memberof ResourcemanagerCalibrateResponse */ 'estimated_capacity'?: number; } /** * * @export * @interface ResourcemanagerGetConfigResponse */ export interface ResourcemanagerGetConfigResponse { /** * * @type {boolean} * @memberof ResourcemanagerGetConfigResponse */ 'enable'?: boolean; } /** * * @export * @interface ResourcemanagerResourceInfoRowDef */ export interface ResourcemanagerResourceInfoRowDef { /** * * @type {string} * @memberof ResourcemanagerResourceInfoRowDef */ 'burstable'?: string; /** * * @type {string} * @memberof ResourcemanagerResourceInfoRowDef */ 'name'?: string; /** * * @type {string} * @memberof ResourcemanagerResourceInfoRowDef */ 'priority'?: string; /** * * @type {string} * @memberof ResourcemanagerResourceInfoRowDef */ 'ru_per_sec'?: string; } /** * * @export * @interface RestErrorResponse */ export interface RestErrorResponse { /** * * @type {string} * @memberof RestErrorResponse */ 'code'?: string; /** * * @type {boolean} * @memberof RestErrorResponse */ 'error'?: boolean; /** * * @type {string} * @memberof RestErrorResponse */ 'full_text'?: string; /** * * @type {string} * @memberof RestErrorResponse */ 'message'?: string; } /** * * @export * @interface SlowqueryGetListRequest */ export interface SlowqueryGetListRequest { /** * * @type {number} * @memberof SlowqueryGetListRequest */ 'begin_time'?: number; /** * * @type {Array} * @memberof SlowqueryGetListRequest */ 'db'?: Array; /** * * @type {boolean} * @memberof SlowqueryGetListRequest */ 'desc'?: boolean; /** * * @type {string} * @memberof SlowqueryGetListRequest */ 'digest'?: string; /** * * @type {number} * @memberof SlowqueryGetListRequest */ 'end_time'?: number; /** * example: \"Query,Digest\" * @type {string} * @memberof SlowqueryGetListRequest */ 'fields'?: string; /** * * @type {number} * @memberof SlowqueryGetListRequest */ 'limit'?: number; /** * * @type {string} * @memberof SlowqueryGetListRequest */ 'orderBy'?: string; /** * for showing slow queries in the statement detail page * @type {Array} * @memberof SlowqueryGetListRequest */ 'plans'?: Array; /** * * @type {Array} * @memberof SlowqueryGetListRequest */ 'resource_group'?: Array; /** * * @type {string} * @memberof SlowqueryGetListRequest */ 'text'?: string; } /** * * @export * @interface SlowqueryModel */ export interface SlowqueryModel { /** * * @type {number} * @memberof SlowqueryModel */ 'backoff_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'backoff_types'?: string; /** * * @type {string} * @memberof SlowqueryModel */ 'binary_plan'?: string; /** * Computed fields * @type {string} * @memberof SlowqueryModel */ 'binary_plan_json'?: string; /** * binary plan plain text * @type {string} * @memberof SlowqueryModel */ 'binary_plan_text'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'commit_backoff_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'commit_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'compile_time'?: number; /** * TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. * @type {string} * @memberof SlowqueryModel */ 'connection_id'?: string; /** * * @type {string} * @memberof SlowqueryModel */ 'cop_proc_addr'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_proc_avg'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_proc_max'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_proc_p90'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'cop_wait_addr'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_wait_avg'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_wait_max'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'cop_wait_p90'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'db'?: string; /** * * @type {string} * @memberof SlowqueryModel */ 'digest'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'disk_max'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'exec_retry_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'get_commit_ts_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'host'?: string; /** * IA remote read * @type {number} * @memberof SlowqueryModel */ 'ia_remote_read_segment_size'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'ia_remote_read_segment_wait_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'index_names'?: string; /** * * @type {string} * @memberof SlowqueryModel */ 'instance'?: string; /** * Basic * @type {number} * @memberof SlowqueryModel */ 'is_internal'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'local_latch_wait_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'lock_keys_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'mem_arbitration'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'memory_max'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'optimize_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'parse_time'?: number; /** * deprecated, replaced by BinaryPlanText * @type {string} * @memberof SlowqueryModel */ 'plan'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'plan_from_binding'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'plan_from_cache'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'prepared'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'preproc_subqueries_time'?: number; /** * Detail * @type {string} * @memberof SlowqueryModel */ 'prev_stmt'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'prewrite_region'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'prewrite_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'process_keys'?: number; /** * Time * @type {number} * @memberof SlowqueryModel */ 'process_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'query'?: string; /** * latency * @type {number} * @memberof SlowqueryModel */ 'query_time'?: number; /** * Coprocessor * @type {number} * @memberof SlowqueryModel */ 'request_count'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'resolve_lock_time'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'resource_group'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'rewrite_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'rocksdb_block_cache_hit_count'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'rocksdb_block_read_byte'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'rocksdb_block_read_count'?: number; /** * RocksDB * @type {number} * @memberof SlowqueryModel */ 'rocksdb_delete_skipped_count'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'rocksdb_key_skipped_count'?: number; /** * Resource Control * @type {number} * @memberof SlowqueryModel */ 'ru'?: number; /** * * @type {string} * @memberof SlowqueryModel */ 'stats'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'success'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'time_queued_by_rc'?: number; /** * finish time * @type {number} * @memberof SlowqueryModel */ 'timestamp'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'total_keys'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'txn_retry'?: number; /** * TODO: Switch back to uint64 when modern browser as well as Swagger handles BigInt well. * @type {string} * @memberof SlowqueryModel */ 'txn_start_ts'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_received_tiflash_cross_zone'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_received_tiflash_total'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_received_tikv_cross_zone'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_received_tikv_total'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_sent_tiflash_cross_zone'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_sent_tiflash_total'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_sent_tikv_cross_zone'?: number; /** * Network fields * @type {number} * @memberof SlowqueryModel */ 'unpacked_bytes_sent_tikv_total'?: number; /** * Connection * @type {string} * @memberof SlowqueryModel */ 'user'?: string; /** * * @type {number} * @memberof SlowqueryModel */ 'wait_prewrite_binlog_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'wait_time'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'wait_ts'?: number; /** * * @type {Array} * @memberof SlowqueryModel */ 'warnings'?: Array; /** * Transaction * @type {number} * @memberof SlowqueryModel */ 'write_keys'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'write_size'?: number; /** * * @type {number} * @memberof SlowqueryModel */ 'write_sql_response_total'?: number; } /** * * @export * @interface SsoCreateImpersonationRequest */ export interface SsoCreateImpersonationRequest { /** * * @type {string} * @memberof SsoCreateImpersonationRequest */ 'password'?: string; /** * * @type {string} * @memberof SsoCreateImpersonationRequest */ 'sql_user'?: string; } /** * * @export * @interface SsoSetConfigRequest */ export interface SsoSetConfigRequest { /** * * @type {ConfigSSOCoreConfig} * @memberof SsoSetConfigRequest */ 'config'?: ConfigSSOCoreConfig; } /** * * @export * @interface SsoSSOImpersonationModel */ export interface SsoSSOImpersonationModel { /** * * @type {string} * @memberof SsoSSOImpersonationModel */ 'last_impersonate_status'?: string; /** * * @type {string} * @memberof SsoSSOImpersonationModel */ 'sql_user'?: string; } /** * * @export * @interface StatementBinding */ export interface StatementBinding { /** * * @type {string} * @memberof StatementBinding */ 'plan_digest'?: string; /** * * @type {string} * @memberof StatementBinding */ 'source'?: StatementBindingSourceEnum; /** * * @type {string} * @memberof StatementBinding */ 'status'?: StatementBindingStatusEnum; } export const StatementBindingSourceEnum = { manual: 'manual', history: 'history', capture: 'capture', evolve: 'evolve' } as const; export type StatementBindingSourceEnum = typeof StatementBindingSourceEnum[keyof typeof StatementBindingSourceEnum]; export const StatementBindingStatusEnum = { enabled: 'enabled', using: 'using', disabled: 'disabled', deleted: 'deleted', invalid: 'invalid', rejected: 'rejected', pending_verify: 'pending verify' } as const; export type StatementBindingStatusEnum = typeof StatementBindingStatusEnum[keyof typeof StatementBindingStatusEnum]; /** * * @export * @interface StatementEditableConfig */ export interface StatementEditableConfig { /** * * @type {boolean} * @memberof StatementEditableConfig */ 'enable'?: boolean; /** * * @type {number} * @memberof StatementEditableConfig */ 'history_size'?: number; /** * * @type {boolean} * @memberof StatementEditableConfig */ 'internal_query'?: boolean; /** * * @type {number} * @memberof StatementEditableConfig */ 'max_size'?: number; /** * * @type {number} * @memberof StatementEditableConfig */ 'refresh_interval'?: number; } /** * * @export * @interface StatementGetStatementsRequest */ export interface StatementGetStatementsRequest { /** * * @type {number} * @memberof StatementGetStatementsRequest */ 'begin_time'?: number; /** * * @type {number} * @memberof StatementGetStatementsRequest */ 'end_time'?: number; /** * * @type {string} * @memberof StatementGetStatementsRequest */ 'fields'?: string; /** * * @type {Array} * @memberof StatementGetStatementsRequest */ 'resource_groups'?: Array; /** * * @type {Array} * @memberof StatementGetStatementsRequest */ 'schemas'?: Array; /** * * @type {Array} * @memberof StatementGetStatementsRequest */ 'stmt_types'?: Array; /** * * @type {string} * @memberof StatementGetStatementsRequest */ 'text'?: string; } /** * * @export * @interface StatementModel */ export interface StatementModel { /** * * @type {number} * @memberof StatementModel */ 'avg_affected_rows'?: number; /** * avg total back off time per sql * @type {number} * @memberof StatementModel */ 'avg_backoff_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_commit_backoff_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_commit_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_compile_latency'?: number; /** * avg process time per copr task * @type {number} * @memberof StatementModel */ 'avg_cop_process_time'?: number; /** * avg wait time per copr task * @type {number} * @memberof StatementModel */ 'avg_cop_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_disk'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_get_commit_ts_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_ia_read_segment_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_ia_remote_read_segment_size'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_ia_remote_read_segment_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_latency'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_local_latch_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_mem'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_mem_arbitration'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_parse_latency'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_prewrite_regions'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_prewrite_time'?: number; /** * avg total process time per sql * @type {number} * @memberof StatementModel */ 'avg_process_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_processed_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_resolve_lock_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_rocksdb_block_cache_hit_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_rocksdb_block_read_byte'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_rocksdb_block_read_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_rocksdb_delete_skipped_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_rocksdb_key_skipped_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_ru'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_time_queued_by_rc'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_total_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_txn_retry'?: number; /** * avg total wait time per sql * @type {number} * @memberof StatementModel */ 'avg_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_write_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'avg_write_size'?: number; /** * * @type {string} * @memberof StatementModel */ 'binary_plan'?: string; /** * * @type {string} * @memberof StatementModel */ 'binary_plan_json'?: string; /** * * @type {string} * @memberof StatementModel */ 'binary_plan_text'?: string; /** * * @type {string} * @memberof StatementModel */ 'digest'?: string; /** * * @type {string} * @memberof StatementModel */ 'digest_text'?: string; /** * * @type {number} * @memberof StatementModel */ 'exec_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'first_seen'?: number; /** * * @type {string} * @memberof StatementModel */ 'index_names'?: string; /** * * @type {number} * @memberof StatementModel */ 'last_seen'?: number; /** * max back off time per sql * @type {number} * @memberof StatementModel */ 'max_backoff_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_commit_backoff_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_commit_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_compile_latency'?: number; /** * max process time per copr task * @type {number} * @memberof StatementModel */ 'max_cop_process_time'?: number; /** * max wait time per copr task * @type {number} * @memberof StatementModel */ 'max_cop_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_disk'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_get_commit_ts_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_ia_read_segment_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_ia_remote_read_segment_size'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_ia_remote_read_segment_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_latency'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_local_latch_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_mem'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_mem_arbitration'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_parse_latency'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_prewrite_regions'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_prewrite_time'?: number; /** * max process time per sql * @type {number} * @memberof StatementModel */ 'max_process_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_processed_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_resolve_lock_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_rocksdb_block_cache_hit_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_rocksdb_block_read_byte'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_rocksdb_block_read_count'?: number; /** * RocksDB * @type {number} * @memberof StatementModel */ 'max_rocksdb_delete_skipped_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_rocksdb_key_skipped_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_ru'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_time_queued_by_rc'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_total_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_txn_retry'?: number; /** * max wait time per sql * @type {number} * @memberof StatementModel */ 'max_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_write_keys'?: number; /** * * @type {number} * @memberof StatementModel */ 'max_write_size'?: number; /** * * @type {number} * @memberof StatementModel */ 'min_latency'?: number; /** * deprecated, replaced by BinaryPlanText * @type {string} * @memberof StatementModel */ 'plan'?: string; /** * * @type {number} * @memberof StatementModel */ 'plan_cache_hits'?: number; /** * * @type {boolean} * @memberof StatementModel */ 'plan_can_be_bound'?: boolean; /** * * @type {number} * @memberof StatementModel */ 'plan_count'?: number; /** * * @type {string} * @memberof StatementModel */ 'plan_digest'?: string; /** * * @type {string} * @memberof StatementModel */ 'plan_hint'?: string; /** * * @type {string} * @memberof StatementModel */ 'prev_sample_text'?: string; /** * * @type {string} * @memberof StatementModel */ 'query_sample_text'?: string; /** * Computed fields * @type {string} * @memberof StatementModel */ 'related_schemas'?: string; /** * Resource Control * @type {string} * @memberof StatementModel */ 'resource_group'?: string; /** * * @type {string} * @memberof StatementModel */ 'sample_user'?: string; /** * * @type {string} * @memberof StatementModel */ 'schema_name'?: string; /** * * @type {string} * @memberof StatementModel */ 'stmt_type'?: string; /** * * @type {number} * @memberof StatementModel */ 'sum_backoff_times'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_cop_task_num'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_errors'?: number; /** * IA remote read segment metrics * @type {number} * @memberof StatementModel */ 'sum_ia_read_segment_count'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_ia_remote_read_segment_size'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_ia_remote_read_segment_wait_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_latency'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_ru'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_received_tiflash_cross_zone'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_received_tiflash_total'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_received_tikv_cross_zone'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_received_tikv_total'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_sent_tiflash_cross_zone'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_sent_tiflash_total'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_sent_tikv_cross_zone'?: number; /** * Network Fields * @type {number} * @memberof StatementModel */ 'sum_unpacked_bytes_sent_tikv_total'?: number; /** * * @type {number} * @memberof StatementModel */ 'sum_warnings'?: number; /** * * @type {number} * @memberof StatementModel */ 'summary_begin_time'?: number; /** * * @type {number} * @memberof StatementModel */ 'summary_end_time'?: number; /** * * @type {string} * @memberof StatementModel */ 'table_names'?: string; } /** * * @export * @interface StatementTimeRange */ export interface StatementTimeRange { /** * * @type {number} * @memberof StatementTimeRange */ begin_time?: number /** * * @type {number} * @memberof StatementTimeRange */ end_time?: number } /** * * @export * @interface TopologyAlertManagerInfo */ export interface TopologyAlertManagerInfo { /** * * @type {string} * @memberof TopologyAlertManagerInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyAlertManagerInfo */ 'port'?: number; } /** * * @export * @interface TopologyGrafanaInfo */ export interface TopologyGrafanaInfo { /** * * @type {string} * @memberof TopologyGrafanaInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyGrafanaInfo */ 'port'?: number; } /** * * @export * @interface TopologyPDInfo */ export interface TopologyPDInfo { /** * * @type {string} * @memberof TopologyPDInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyPDInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyPDInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyPDInfo */ 'port'?: number; /** * Ts = 0 means unknown * @type {number} * @memberof TopologyPDInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyPDInfo */ 'status'?: number; /** * * @type {string} * @memberof TopologyPDInfo */ 'version'?: string; } /** * * @export * @interface TopologySchedulingInfo */ export interface TopologySchedulingInfo { /** * * @type {string} * @memberof TopologySchedulingInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologySchedulingInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologySchedulingInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologySchedulingInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologySchedulingInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologySchedulingInfo */ 'status'?: number; /** * * @type {string} * @memberof TopologySchedulingInfo */ 'version'?: string; } /** * * @export * @interface TopologyStoreInfo */ export interface TopologyStoreInfo { /** * * @type {string} * @memberof TopologyStoreInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyStoreInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyStoreInfo */ 'ip'?: string; /** * * @type {{ [key: string]: string; }} * @memberof TopologyStoreInfo */ 'labels'?: { [key: string]: string; }; /** * * @type {number} * @memberof TopologyStoreInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologyStoreInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyStoreInfo */ 'status'?: number; /** * * @type {number} * @memberof TopologyStoreInfo */ 'status_port'?: number; /** * * @type {string} * @memberof TopologyStoreInfo */ 'version'?: string; } /** * * @export * @interface TopologyStoreLabels */ export interface TopologyStoreLabels { /** * * @type {string} * @memberof TopologyStoreLabels */ 'address'?: string; /** * * @type {{ [key: string]: string; }} * @memberof TopologyStoreLabels */ 'labels'?: { [key: string]: string; }; } /** * * @export * @interface TopologyStoreLocation */ export interface TopologyStoreLocation { /** * * @type {Array} * @memberof TopologyStoreLocation */ 'location_labels'?: Array; /** * * @type {Array} * @memberof TopologyStoreLocation */ 'stores'?: Array; } /** * * @export * @interface TopologyTiCDCInfo */ export interface TopologyTiCDCInfo { /** * * @type {string} * @memberof TopologyTiCDCInfo */ 'cluster_name'?: string; /** * * @type {string} * @memberof TopologyTiCDCInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyTiCDCInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyTiCDCInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyTiCDCInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologyTiCDCInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyTiCDCInfo */ 'status'?: number; /** * * @type {number} * @memberof TopologyTiCDCInfo */ 'status_port'?: number; /** * * @type {string} * @memberof TopologyTiCDCInfo */ 'version'?: string; } /** * * @export * @interface TopologyTiDBInfo */ export interface TopologyTiDBInfo { /** * * @type {string} * @memberof TopologyTiDBInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyTiDBInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyTiDBInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyTiDBInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologyTiDBInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyTiDBInfo */ 'status'?: number; /** * * @type {number} * @memberof TopologyTiDBInfo */ 'status_port'?: number; /** * * @type {string} * @memberof TopologyTiDBInfo */ 'version'?: string; } /** * * @export * @interface TopologyTiProxyInfo */ export interface TopologyTiProxyInfo { /** * * @type {string} * @memberof TopologyTiProxyInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyTiProxyInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyTiProxyInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyTiProxyInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologyTiProxyInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyTiProxyInfo */ 'status'?: number; /** * * @type {number} * @memberof TopologyTiProxyInfo */ 'status_port'?: number; /** * * @type {string} * @memberof TopologyTiProxyInfo */ 'version'?: string; } /** * * @export * @interface TopologyTSOInfo */ export interface TopologyTSOInfo { /** * * @type {string} * @memberof TopologyTSOInfo */ 'deploy_path'?: string; /** * * @type {string} * @memberof TopologyTSOInfo */ 'git_hash'?: string; /** * * @type {string} * @memberof TopologyTSOInfo */ 'ip'?: string; /** * * @type {number} * @memberof TopologyTSOInfo */ 'port'?: number; /** * * @type {number} * @memberof TopologyTSOInfo */ 'start_timestamp'?: number; /** * * @type {number} * @memberof TopologyTSOInfo */ 'status'?: number; /** * * @type {string} * @memberof TopologyTSOInfo */ 'version'?: string; } /** * * @export * @interface TopsqlEditableConfig */ export interface TopsqlEditableConfig { /** * * @type {boolean} * @memberof TopsqlEditableConfig */ 'enable'?: boolean; } /** * * @export * @interface TopsqlInstanceItem */ export interface TopsqlInstanceItem { /** * * @type {string} * @memberof TopsqlInstanceItem */ 'instance'?: string; /** * * @type {string} * @memberof TopsqlInstanceItem */ 'instance_type'?: string; } /** * * @export * @interface TopsqlInstanceResponse */ export interface TopsqlInstanceResponse { /** * * @type {Array} * @memberof TopsqlInstanceResponse */ 'data'?: Array; } /** * * @export * @interface TopsqlSummaryByItem */ export interface TopsqlSummaryByItem { /** * * @type {Array} * @memberof TopsqlSummaryByItem */ 'cpu_time_ms'?: Array; /** * * @type {number} * @memberof TopsqlSummaryByItem */ 'cpu_time_ms_sum'?: number; /** * * @type {Array} * @memberof TopsqlSummaryByItem */ 'logical_io_bytes'?: Array; /** * * @type {number} * @memberof TopsqlSummaryByItem */ 'logical_io_bytes_sum'?: number; /** * * @type {Array} * @memberof TopsqlSummaryByItem */ 'network_bytes'?: Array; /** * * @type {number} * @memberof TopsqlSummaryByItem */ 'network_bytes_sum'?: number; /** * * @type {string} * @memberof TopsqlSummaryByItem */ 'text'?: string; /** * * @type {Array} * @memberof TopsqlSummaryByItem */ 'timestamp_sec'?: Array; } /** * * @export * @interface TopsqlSummaryItem */ export interface TopsqlSummaryItem { /** * * @type {number} * @memberof TopsqlSummaryItem */ 'cpu_time_ms'?: number; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'duration_per_exec_ms'?: number; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'exec_count_per_sec'?: number; /** * * @type {boolean} * @memberof TopsqlSummaryItem */ 'is_other'?: boolean; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'logical_io_bytes'?: number; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'network_bytes'?: number; /** * * @type {Array} * @memberof TopsqlSummaryItem */ 'plans'?: Array; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'scan_indexes_per_sec'?: number; /** * * @type {number} * @memberof TopsqlSummaryItem */ 'scan_records_per_sec'?: number; /** * * @type {string} * @memberof TopsqlSummaryItem */ 'sql_digest'?: string; /** * * @type {string} * @memberof TopsqlSummaryItem */ 'sql_text'?: string; } /** * * @export * @interface TopsqlSummaryPlanItem */ export interface TopsqlSummaryPlanItem { /** * * @type {Array} * @memberof TopsqlSummaryPlanItem */ 'cpu_time_ms'?: Array; /** * * @type {number} * @memberof TopsqlSummaryPlanItem */ 'duration_per_exec_ms'?: number; /** * * @type {number} * @memberof TopsqlSummaryPlanItem */ 'exec_count_per_sec'?: number; /** * * @type {Array} * @memberof TopsqlSummaryPlanItem */ 'logical_io_bytes'?: Array; /** * * @type {Array} * @memberof TopsqlSummaryPlanItem */ 'network_bytes'?: Array; /** * * @type {string} * @memberof TopsqlSummaryPlanItem */ 'plan_digest'?: string; /** * * @type {string} * @memberof TopsqlSummaryPlanItem */ 'plan_text'?: string; /** * * @type {number} * @memberof TopsqlSummaryPlanItem */ 'scan_indexes_per_sec'?: number; /** * * @type {number} * @memberof TopsqlSummaryPlanItem */ 'scan_records_per_sec'?: number; /** * * @type {Array} * @memberof TopsqlSummaryPlanItem */ 'timestamp_sec'?: Array; } /** * * @export * @interface TopsqlSummaryResponse */ export interface TopsqlSummaryResponse { /** * * @type {Array} * @memberof TopsqlSummaryResponse */ 'data'?: Array; /** * * @type {Array} * @memberof TopsqlSummaryResponse */ 'data_by'?: Array; } /** * * @export * @interface TopsqlTikvNetworkIoCollectionConfig */ export interface TopsqlTikvNetworkIoCollectionConfig { /** * * @type {boolean} * @memberof TopsqlTikvNetworkIoCollectionConfig */ 'enable'?: boolean; /** * * @type {boolean} * @memberof TopsqlTikvNetworkIoCollectionConfig */ 'is_multi_value'?: boolean; } /** * * @export * @interface TopsqlUpdateTikvNetworkIoCollectionResponse */ export interface TopsqlUpdateTikvNetworkIoCollectionResponse { /** * * @type {Array} * @memberof TopsqlUpdateTikvNetworkIoCollectionResponse */ 'warnings'?: Array; } /** * * @export * @interface UserAuthenticateForm */ export interface UserAuthenticateForm { /** * FIXME: Use strong type * @type {string} * @memberof UserAuthenticateForm */ 'extra'?: string; /** * * @type {string} * @memberof UserAuthenticateForm */ 'password'?: string; /** * * @type {number} * @memberof UserAuthenticateForm */ 'type'?: number; /** * Does not present for AuthTypeSharingCode * @type {string} * @memberof UserAuthenticateForm */ 'username'?: string; } /** * * @export * @interface UserGetLoginInfoResponse */ export interface UserGetLoginInfoResponse { /** * * @type {string} * @memberof UserGetLoginInfoResponse */ 'sql_auth_public_key'?: string; /** * * @type {Array} * @memberof UserGetLoginInfoResponse */ 'supported_auth_types'?: Array; } /** * * @export * @interface UserSignOutInfo */ export interface UserSignOutInfo { /** * * @type {string} * @memberof UserSignOutInfo */ 'end_session_url'?: string; } /** * * @export * @interface UserTokenResponse */ export interface UserTokenResponse { /** * * @type {string} * @memberof UserTokenResponse */ 'expire'?: string; /** * * @type {string} * @memberof UserTokenResponse */ 'token'?: string; } /** * * @export * @interface VersionInfo */ export interface VersionInfo { /** * * @type {string} * @memberof VersionInfo */ 'build_git_hash'?: string; /** * * @type {string} * @memberof VersionInfo */ 'build_time'?: string; /** * * @type {string} * @memberof VersionInfo */ 'internal_version'?: string; /** * * @type {string} * @memberof VersionInfo */ 'pd_version'?: string; /** * * @type {string} * @memberof VersionInfo */ 'standalone'?: string; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/AnimatedSkeleton/index.module.less ================================================ @import 'antd/es/style/mixins/motion.less'; .container :global { .skeletonAnimationFirstTime { animation: 0.5s linear 0.5s antFadeIn; animation-fill-mode: both; animation-iteration-count: 1; } .skeletonAnimationNotFirstTime { animation: 0.5s linear 0 antFadeIn; animation-fill-mode: both; animation-iteration-count: 1; } .contentAnimation { animation: 0.2s linear 0s antFadeIn; animation-fill-mode: both; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/AnimatedSkeleton/index.tsx ================================================ import React, { useEffect, useState } from 'react' import cx from 'classnames' import { Skeleton } from 'antd' import { SkeletonProps } from 'antd/lib/skeleton' import { AppearAnimate } from '..' import styles from './index.module.less' export interface IAnimatedSkeletonProps extends SkeletonProps { showSkeleton?: boolean children?: React.ReactNode style?: React.CSSProperties } function AnimatedSkeleton({ showSkeleton, children, style, ...restProps }: IAnimatedSkeletonProps) { const [skeletonAppears, setSkeletonAppears] = useState(0) useEffect(() => { if (showSkeleton) { setSkeletonAppears((v) => v + 1) } }, [showSkeleton]) return (
{showSkeleton && (
1 })} >
)} {!showSkeleton && ( {children} )}
) } export default React.memo(AnimatedSkeleton) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/AppearAnimate/index.tsx ================================================ import cx from 'classnames' import React, { useState, useCallback, useRef } from 'react' import { useEventListener } from 'ahooks' export interface IAppearAnimateProps extends React.HTMLAttributes { motionName: string } // A component similar to CSSMotion but is simpler, and avoids some edge case bugs. // It simply removes the animation class after animation completes. function AppearAnimate({ className, motionName, children }: IAppearAnimateProps) { const [isFirst, setIsFirst] = useState(true) const handleAnimationEnd = useCallback(() => { setIsFirst(false) }, []) const ref = useRef(null) useEventListener('animationend', handleAnimationEnd, { target: ref }) return (
{children}
) } export default React.memo(AppearAnimate) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/AutoRefreshButton/AutoRefreshButton.tsx ================================================ import React, { useEffect, useRef, useState } from 'react' import { DownOutlined, SyncOutlined } from '@ant-design/icons' import { Dropdown, Menu } from 'antd' import { useSpring, animated } from 'react-spring' import { getValueFormat } from '@baurine/grafana-value-formats' import { useTranslation } from 'react-i18next' import { addTranslationResource } from '@lib/utils/i18n' import styles from './index.module.less' import { useChange } from '@lib/utils/useChange' import { useControllableValue, useMemoizedFn } from 'ahooks' export const DEFAULT_AUTO_REFRESH_OPTIONS = [ 30, 60, 5 * 60, 15 * 60, 30 * 60, 1 * 60 * 60, 2 * 60 * 60 ] export interface IAutoRefreshButtonProps { options?: number[] // set to 0 will stop the auto refresh defaultValue?: number value?: number onChange?: (number) => void onRefresh?: () => void // set to false will pause the auto refresh disabled?: boolean } const translations = { en: { refresh: 'Refresh', auto_refresh: { title: 'Auto Refresh', off: 'Off' } }, zh: { refresh: '刷新', auto_refresh: { title: '自动刷新', off: '关闭' } } } for (const key in translations) { addTranslationResource(key, { component: { autoRefreshButton: translations[key] } }) } export function AutoRefreshButton({ options = DEFAULT_AUTO_REFRESH_OPTIONS, onRefresh, disabled = false, ...props }: IAutoRefreshButtonProps) { const { t } = useTranslation() const [interval, setInterval] = useControllableValue(props, { defaultValue: 60 }) const [remaining, setRemaining] = useState(0) const autoRefreshMenu = ( setInterval(parseInt(key as string))} selectedKeys={[String(interval || 0)]} > {t('component.autoRefreshButton.auto_refresh.off')} {options.map((sec) => { return ( {getValueFormat('s')(sec, 0)} ) })} ) const timer = useRef | undefined>(undefined) const resetTimer = useMemoizedFn(() => { clearTimeout(timer.current!) timer.current = undefined setRemaining(interval) }) useChange(() => { clearTimeout(timer.current!) timer.current = undefined if ( // If remaining seconds is less than the new interval, keep the current remaining seconds. // Otherwise, set remaining seconds to new interval. remaining > interval || remaining === 0 ) { setRemaining(interval) } }, [interval]) const handleRefresh = useMemoizedFn(async () => { if (disabled) { return } resetTimer() onRefresh?.() }) useEffect(() => { // stop or pause auto refresh need to clear timer if (!interval || disabled) { if (!!timer.current) { clearTimeout(timer.current) timer.current = undefined } return } timer.current = setTimeout(() => { if (remaining === 0) { setRemaining(interval) handleRefresh() } else { setRemaining((r) => r - 1) } }, 1000) return () => clearTimeout(timer.current!) }, [interval, disabled, remaining, /* unchange */ handleRefresh]) return ( {Boolean(interval) && ( {getValueFormat('s')(interval, 0)} )} } > {Boolean(interval) ? ( ) : ( )} {t('component.autoRefreshButton.refresh')} ) } function RefreshProgress(props) { const { value } = props const r = 50 const totalLength = 2 * Math.PI * r const [springProps, setSpringProps] = useSpring(() => ({ value: 0 })) useEffect(() => { setSpringProps({ value }) }, [setSpringProps, value]) return ( ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/AutoRefreshButton/index.module.less ================================================ .auto_refresh_btn { :global { .ant-dropdown-trigger { width: auto; padding: 0 10px; display: flex; align-items: center; } } .auto_refresh_secs { font-size: 14px; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/AutoRefreshButton/index.ts ================================================ import { AutoRefreshButton } from './AutoRefreshButton' export default AutoRefreshButton export * from './AutoRefreshButton' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Bar/Bar.module.less ================================================ @import 'antd/es/style/themes/default.less'; @bar-height: 8px; @bar-color: lighten(@primary-color, 10%); @bar-stack-color: @gray-3; @error-bar-height: 6px; @error-bar-color: @gold-5; @error-line-width: 2px; .container { display: flex; flex-direction: row; align-items: center; min-height: 1em; height: unit(@line-height-base, em); } .text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 5px; } .bar_container { flex-grow: 1; position: relative; height: @bar-height; background: @bar-stack-color; } .bar { position: absolute; top: 0; height: 100%; background-color: @bar-color; } .error_bar { position: absolute; top: 50%; height: @error-line-width; margin-top: -(@error-line-width / 2); // https://github.com/less/less.js/issues/3608#issuecomment-811086683 background-color: @error-bar-color; &::before { content: ''; position: absolute; height: @error-bar-height; width: @error-line-width; margin-top: -(@error-bar-height / 2); background-color: @error-bar-color; top: 50%; } &.min_bar { &::before { left: 0; } } &.max_bar { &::before { right: 0; } } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Bar/Bar.tsx ================================================ import React, { useMemo } from 'react' import cx from 'classnames' import clamp from 'lodash/clamp' import sum from 'lodash/sum' import styles from './Bar.module.less' export interface IBarProps { value: number[] | number colors?: (string | null)[] capacity: number min?: number max?: number className?: string children?: React.ReactNode textWidth?: number | string } function Bar({ value, colors, capacity, min, max, className, children, textWidth, ...rest }: IBarProps) { const clampedValues = useMemo(() => { if (value instanceof Array) { const r: [number, number][] = [] let sum = 0 value.forEach((value) => { let v: number if (sum + value <= capacity) { v = value } else if (sum < capacity) { v = capacity - sum } else { v = 0 } r.push([sum, v]) sum += v }) return r } else { return [[0, clamp(value, 0, capacity)]] } }, [value, capacity]) const valuesSum = useMemo( () => sum(clampedValues.map(([_s, v]) => v)), [clampedValues] ) if (min != null) { min = clamp(min, 0, valuesSum) if ((valuesSum - min) / capacity < 0.01) { min = undefined } } if (max != null) { max = clamp(max, valuesSum, capacity) if ((max - valuesSum) / capacity < 0.01) { max = undefined } } return (
{children && (
{children}
)}
{clampedValues.map(([offset, value], idx) => (
))} {min != null && (
)} {max != null && (
)}
) } export default Bar ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Bar/index.tsx ================================================ import Bar from './Bar' export * from './Bar' export default Bar ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/BaseSelect/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; @import 'antd/es/style/mixins/index'; @import 'antd/es/input/style/mixin'; @import 'antd/es/select/style/index.less'; // Exist index.less and index.js both, we need to declare it explictly else it will find index.js .baseSelect { .reset-component; position: relative; display: inline-block; } .baseSelectInner { position: relative; background-color: @select-background; border: @border-width-base @border-style-base @select-border-color; border-radius: @border-radius-base; transition: all 0.3s @ease-in-out; display: flex; width: 100%; height: @input-height-base; padding: 0 @input-padding-horizontal-base; cursor: pointer; color: @text-color; &.focused { .active(); } &.disabled { color: @disabled-color; background: @input-disabled-bg; cursor: not-allowed; .baseSelectInput { cursor: not-allowed; } } &:not(.disabled):hover { .hover(); } } :global(.ant-form-item-has-error) { .baseSelectInner { border-color: @error-color !important; &.focused { .active(@error-color); } } } .baseSelectInput { opacity: 0; position: absolute; top: 0; left: 0; background: transparent; border: none; outline: none; cursor: pointer; width: 100%; height: @select-height-without-border; } .baseSelectValueDisplay { position: relative; display: block; padding-right: @selection-item-padding; font-weight: normal; font-size: @select-dropdown-font-size; line-height: @select-height-without-border; transition: all 0.3s; pointer-events: none; width: 100%; &.isPlaceholder { opacity: 0.4; } } .baseSelectArrow { position: absolute; top: 53%; // The same as Ant-design's select right: @input-padding-horizontal-base; width: @font-size-sm; height: @font-size-sm; margin-top: -(@font-size-sm / 2); color: @disabled-color; font-size: @font-size-sm; line-height: 1; text-align: center; pointer-events: none; } .baseSelectOverlay { background-color: @select-dropdown-bg; border-radius: @border-radius-base; outline: none; box-shadow: @box-shadow-base; box-sizing: border-box; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/BaseSelect/index.stories.tsx ================================================ import React from 'react' import { Button } from 'antd' import BaseSelect from '.' export default { title: 'Select/Base Select' } export const shortContent = () => (
Content
} valueRender={() => Short} /> ) export const longContent = () => (
Content
} valueRender={() => Very Lonnnnnnnnng Value} /> ) export const disabled = () => (
Content
} valueRender={() => Disabled} /> ) export const antdButton = () => ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/BaseSelect/index.tsx ================================================ import React, { useState, useCallback, useRef, useMemo } from 'react' import cx from 'classnames' import { useEventListener } from 'ahooks' import { DownOutlined } from '@ant-design/icons' import Trigger from 'rc-trigger' import KeyCode from 'rc-util/lib/KeyCode' import _ from 'lodash' import { TextWrap } from '..' import styles from './index.module.less' export interface IBaseSelectProps extends Omit< React.HTMLAttributes, 'onChange' | 'placeholder' > { dropdownRender: () => React.ReactElement value?: T valueRender: (value?: T) => React.ReactNode placeholder?: React.ReactNode overlayClassName?: string disabled?: boolean tabIndex?: number autoFocus?: boolean onOpen?: () => void onOpened?: () => void onClose?: () => void onClosed?: () => void } const builtinPlacements = { bottomLeft: { ignoreShake: true, points: ['tl', 'bl'], offset: [0, 4], overflow: { adjustX: 0, adjustY: 0 } } } function BaseSelect({ dropdownRender, value, valueRender, placeholder, disabled, tabIndex, autoFocus, className, overlayClassName, onFocus, onBlur, onKeyDown, onOpen, onOpened, onClose, onClosed, ...restProps }: IBaseSelectProps) { const [dropdownVisible, setDropdownVisible] = useState(false) const toggleDropdownVisible = useCallback(() => { if (disabled) { return } setDropdownVisible((v) => !v) }, [disabled]) const [isFocused, setFocused] = useState(false) const handleDebouncedContainerFocus = useCallback( (ev: React.FocusEvent) => { setFocused(true) onFocus && onFocus(ev) }, [onFocus] ) const handleDebouncedContainerBlur = useCallback( (ev: React.FocusEvent) => { setDropdownVisible(false) setFocused(false) onBlur && onBlur(ev) }, [onBlur] ) const debouncedFocusOrBlur = useMemo(() => { return _.debounce( (isFocus: boolean, ev: React.FocusEvent) => { if (isFocus) { handleDebouncedContainerFocus(ev) } else { handleDebouncedContainerBlur(ev) } }, 50 ) }, [handleDebouncedContainerFocus, handleDebouncedContainerBlur]) const handleContainerFocus = useCallback( (ev) => { debouncedFocusOrBlur(true, ev) }, [debouncedFocusOrBlur] ) const handleContainerBlur = useCallback( (ev) => { debouncedFocusOrBlur(false, ev) }, [debouncedFocusOrBlur] ) const handleContainerKeyDown = useCallback( (ev: React.KeyboardEvent) => { if (ev.which === KeyCode.ENTER) { toggleDropdownVisible() } else if (ev.which === KeyCode.ESC) { setDropdownVisible(false) } onKeyDown && onKeyDown(ev) }, [toggleDropdownVisible, onKeyDown] ) const handleSelectorMouseDown = useCallback(() => { toggleDropdownVisible() }, [toggleDropdownVisible]) const handleOverlayMouseDown = useCallback( (ev: React.MouseEvent) => { // Prevent dropdown container blur event ev.preventDefault() }, [] ) const handlePopupVisibleChange = useCallback( (visible: boolean) => { if (visible) { onOpen?.() } else { onClose?.() } }, [onOpen, onClose] ) const handleAfterPopupVisibleChange = useCallback( (visible: boolean) => { if (visible) { onOpened?.() } else { onClosed?.() } }, [onOpened, onClosed] ) const dropdownOverlayRef = useRef(null) const containerRef = useRef(null) const overlay = useMemo(() => { return (
{dropdownRender()}
) }, [dropdownRender, overlayClassName, handleOverlayMouseDown]) useEventListener('mousedown', (ev: MouseEvent) => { // Close the dropdown if click outside if (!dropdownVisible) { return } const hitElements = [dropdownOverlayRef.current, containerRef.current] if ( hitElements.every( (e) => !e || !ev.target || (!e.contains(ev.target as HTMLElement) && e !== ev.target) ) ) { setDropdownVisible(false) } }) // Close dropdown when disabled change React.useEffect(() => { setDropdownVisible((v) => { if (v && !disabled) { return false } return v }) }, [disabled]) const renderedValue = valueRender(value) const displayAsPlaceholder = renderedValue == null return (
{displayAsPlaceholder ? placeholder : renderedValue}
) } export default React.memo(BaseSelect) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/BinaryPlanTable/BinaryPlanColsSelector.tsx ================================================ import React from 'react' import { Checkbox, Popover, Space } from 'antd' import { DownOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { addTranslationResource } from '@lib/utils/i18n' const translations = { en: { trigger_text: 'Columns' }, zh: { trigger_text: '选择列' } } for (const key in translations) { addTranslationResource(key, { component: { binaryPlanColsSelector: translations[key] } }) } export interface IColumnKeys { [key: string]: boolean } export interface IBinaryPlanColsSelectorProps { columns: IColumn[] visibleColumnKeys: IColumnKeys onChange?: (visibleKeys: IColumnKeys) => void } export function BinaryPlanColsSelector({ columns, visibleColumnKeys, onChange }: IBinaryPlanColsSelectorProps) { const { t } = useTranslation() function handleCheckChange(e, column: IColumn) { const checked = e.target.checked const newVisibleKeys = { ...visibleColumnKeys, [column.key]: checked } onChange && onChange(newVisibleKeys) } const content = (
{columns.map((column) => ( handleCheckChange(e, column)} > {column['extra']} ))}
) return ( {t('component.binaryPlanColsSelector.trigger_text')} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/BinaryPlanTable/index.module.less ================================================ .colHeader { span { opacity: 0; } &:hover { span { opacity: 1; } } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/BinaryPlanTable/index.tsx ================================================ import { IColumn } from 'office-ui-fabric-react' import React, { useMemo } from 'react' import CardTable from '../CardTable' import { Tooltip } from 'antd' import { CopyLink, TxtDownloadLink } from '@lib/components' import { BinaryPlanColsSelector } from './BinaryPlanColsSelector' import { EyeOutlined } from '@ant-design/icons' import styles from './index.module.less' const COLUM_KEYS = [ 'id', 'estRows', 'estCost', 'actRows', 'task', 'accessObject', 'executionInfo', 'operatorInfo', 'memory', 'disk' ] as const type COLUM_KEYS_UNION = typeof COLUM_KEYS[number] type BinaryPlanItem = Record type BinaryPlanFiledPosition = Record< COLUM_KEYS_UNION, { start: number len: number } > type BinaryPlanTableProps = { data: string downloadFileName: string } // see binary_plan_text example from sample-data/detail-res.json function convertBinaryPlanTextToArray( binaryPlanText: string ): BinaryPlanItem[] { const result: BinaryPlanItem[] = [] let positions: BinaryPlanFiledPosition | null = null // we can't simply split by '\n', because operator info column may contain '\n' // for example, execution plan for "select `tidb_decode_binary_plan` ( ? ) as `binary_plan_text`;" // const lines = binaryPlanText.split('\n') const headerEndPos = binaryPlanText.indexOf('\n', 1) const headerLine = binaryPlanText.slice(1, headerEndPos) if (!headerLine.startsWith('| id')) { console.error('invalid binary plan text format') return result } const headerLineLen = headerLine.length const headers = headerLine.split('|') // 0: "" // 1: " id " // 2: " estRows " // 3: " estCost " // 4: " actRows " // 5: " task " // 6: " access object " // 7: " execution info " // 8: " operator info " // 9: " memory " // 10: " disk " // 11: "" if (headers.length !== 12) { console.error('invalid binary plan text format') return result } positions = { id: { start: 0, len: headers[1].length }, estRows: { start: 0, len: headers[2].length }, estCost: { start: 0, len: headers[3].length }, actRows: { start: 0, len: headers[4].length }, task: { start: 0, len: headers[5].length }, accessObject: { start: 0, len: headers[6].length }, executionInfo: { start: 0, len: headers[7].length }, operatorInfo: { start: 0, len: headers[8].length }, memory: { start: 0, len: headers[9].length }, disk: { start: 0, len: headers[10].length } } positions.id.start = 1 positions.estRows.start = positions.id.start + positions.id.len + 1 positions.estCost.start = positions.estRows.start + positions.estRows.len + 1 positions.actRows.start = positions.estCost.start + positions.estCost.len + 1 positions.task.start = positions.actRows.start + positions.actRows.len + 1 positions.accessObject.start = positions.task.start + positions.task.len + 1 positions.executionInfo.start = positions.accessObject.start + positions.accessObject.len + 1 positions.operatorInfo.start = positions.executionInfo.start + positions.executionInfo.len + 1 positions.memory.start = positions.operatorInfo.start + positions.operatorInfo.len + 1 positions.disk.start = positions.memory.start + positions.memory.len + 1 let lineIdx = 1 while (true) { const lineStart = 1 + (headerLineLen + 1) * lineIdx const lineEnd = 1 + (headerLineLen + 1) * (lineIdx + 1) if (lineEnd > binaryPlanText.length) { break } lineIdx++ const line = binaryPlanText.slice(lineStart, lineEnd) const item: BinaryPlanItem = { id: line .slice(positions.id.start + 1, positions.id.start + positions.id.len) .trimEnd(), // start+1 for removing the leading white space estRows: line .slice( positions.estRows.start, positions.estRows.start + positions.estRows.len ) .trim(), estCost: line .slice( positions.estCost.start, positions.estCost.start + positions.estCost.len ) .trim(), actRows: line .slice( positions.actRows.start, positions.actRows.start + positions.actRows.len ) .trim(), task: line .slice(positions.task.start, positions.task.start + positions.task.len) .trim(), accessObject: line .slice( positions.accessObject.start, positions.accessObject.start + positions.accessObject.len ) .trim(), executionInfo: line .slice( positions.executionInfo.start, positions.executionInfo.start + positions.executionInfo.len ) .trim(), operatorInfo: line .slice( positions.operatorInfo.start, positions.operatorInfo.start + positions.operatorInfo.len ) .trim(), memory: line .slice( positions.memory.start, positions.memory.start + positions.memory.len ) .trim(), disk: line .slice(positions.disk.start, positions.disk.start + positions.disk.len) .trim() } result.push(item) } return result } export const BinaryPlanTable: React.FC = ({ data, downloadFileName }) => { const arr = useMemo(() => convertBinaryPlanTextToArray(data), [data]) function hideColumn(columnKey: COLUM_KEYS_UNION) { setVisibleColumnKeys((prev) => { return { ...prev, [columnKey]: false } }) } const columns: IColumn[] = useMemo(() => { return [ { name: (
id hideColumn('id')} />
) as any, extra: 'id', key: 'id', minWidth: 200, maxWidth: 600, onRender: (row: BinaryPlanItem) => { return ( {row.id} ) } }, { name: (
estRows hideColumn('estRows')} />
) as any, extra: 'estRows', key: 'estRows', minWidth: 100, maxWidth: 120, onRender: (row: BinaryPlanItem) => { return row.estRows } }, { name: (
estCost hideColumn('estCost')} />
) as any, extra: 'estCost', key: 'estCost', minWidth: 100, maxWidth: 120, onRender: (row: BinaryPlanItem) => { return row.estCost } }, { name: (
actRows hideColumn('actRows')} />
) as any, extra: 'actRows', key: 'actRows', minWidth: 100, maxWidth: 120, onRender: (row: BinaryPlanItem) => { return row.actRows } }, { name: (
task hideColumn('task')} />
) as any, extra: 'task', key: 'task', minWidth: 60, maxWidth: 100, onRender: (row: BinaryPlanItem) => { return row.task } }, { name: (
access object{' '} hideColumn('accessObject')} />
) as any, extra: 'access object', key: 'accessObject', minWidth: 120, maxWidth: 140, onRender: (row: BinaryPlanItem) => { return {row.accessObject} } }, { name: (
execution info{' '} hideColumn('executionInfo')} />
) as any, extra: 'execution info', key: 'executionInfo', minWidth: 120, maxWidth: 300, onRender: (row: BinaryPlanItem) => { return ( {row.executionInfo} ) } }, { name: (
operator info{' '} hideColumn('operatorInfo')} />
) as any, extra: 'operator info', key: 'operatorInfo', minWidth: 120, maxWidth: 300, onRender: (row: BinaryPlanItem) => { // truncate the string if it's too long // operation info may be super super long const truncateLength = 100 let truncatedStr = row.operatorInfo ?? '' if (truncatedStr.length > truncateLength) { truncatedStr = row.operatorInfo.slice(0, truncateLength) + '...' } const truncateTooltipLen = 2000 let truncatedTooltipStr = row.operatorInfo ?? '' if (truncatedTooltipStr.length > truncateTooltipLen) { truncatedTooltipStr = row.operatorInfo.slice(0, truncateTooltipLen) + '...(too long to show, copy or download to analyze)' } return {truncatedStr} } }, { name: (
memory hideColumn('memory')} />
) as any, extra: 'memory', key: 'memory', minWidth: 80, maxWidth: 100, onRender: (row: BinaryPlanItem) => { return row.memory } }, { name: (
disk hideColumn('disk')} />
) as any, extra: 'disk', key: 'disk', minWidth: 60, maxWidth: 100, onRender: (row: BinaryPlanItem) => { return row.disk } } ] }, []) const [visibleColumnKeys, setVisibleColumnKeys] = React.useState(() => { return COLUM_KEYS.reduce((acc, cur) => { acc[cur] = true return acc }, {}) }) const filteredColumns = useMemo(() => { return columns.filter((c) => visibleColumnKeys[c.key as COLUM_KEYS_UNION]) }, [columns, visibleColumnKeys]) if (arr.length > 0) { return ( <>
) } return (
Parse plan text failed, original content:
{data}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/BinaryPlanTable/sample-data/binary-plan.json ================================================ { "discardedDueToTooLong": false, "main": { "accessObjects": [], "actRows": 12, "children": [ { "accessObjects": [], "actRows": 12, "children": [ { "accessObjects": [], "actRows": 12, "children": [ { "accessObjects": [], "actRows": 69, "children": [ { "accessObjects": [ { "scanObject": { "database": "INFORMATION_SCHEMA", "table": "CLUSTER_LOAD" } } ], "actRows": 1193, "copExecInfo": {}, "diagnosis": [], "diskBytes": "N/A", "duration": "1.51s", "estRows": 10000, "labels": [], "memoryBytes": "N/A", "name": "MemTableScan_12", "rootBasicExecInfo": { "loops": "3", "time": "1.51s" }, "rootGroupExecInfo": [], "storeType": "tidb", "taskType": "root", "__node_attrs": { "id": "5", "collapsed": false, "collapsiable": false, "isNodeDetailVisible": false, "nodeFlexSize": { "width": 280, "height": 230 } } } ], "copExecInfo": {}, "cost": 499000, "diagnosis": ["high_est_error"], "diskBytes": "N/A", "duration": "1.51s", "estRows": 8000, "labels": [], "memoryBytes": "138400", "name": "Selection_11", "operatorInfo": "in(Column#3, \"memory\", \"cpu\")", "rootBasicExecInfo": { "loops": "2", "time": "1.51s" }, "rootGroupExecInfo": [], "storeType": "tidb", "taskType": "root", "__node_attrs": { "id": "4", "collapsed": false, "collapsiable": true, "isNodeDetailVisible": false, "nodeFlexSize": { "width": 280, "height": 200 } } } ], "copExecInfo": {}, "cost": 1772970.6, "diagnosis": [], "diskBytes": "N/A", "duration": "1.51s", "estRows": 8000, "labels": [], "memoryBytes": "48580", "name": "HashAgg_10", "operatorInfo": "group by:Column#1, Column#2, Column#3, Column#4, funcs:json_objectagg(Column#5, Column#6)->Column#7, funcs:firstrow(Column#1)->Column#1, funcs:firstrow(Column#2)->Column#2, funcs:firstrow(Column#3)->Column#3, funcs:firstrow(Column#4)->Column#4", "rootBasicExecInfo": { "loops": "6", "time": "1.51s" }, "rootGroupExecInfo": [ { "final_worker": { "concurrency": "5", "max": "1.513316486s", "p95": "1.513316486s", "task_num": "5", "tot_exec": "223.043µs", "tot_time": "7.566371614s", "tot_wait": "7.566142195s", "wall_time": "1.5133523s" }, "partial_worker": { "concurrency": "5", "max": "1.513153823s", "p95": "1.513153823s", "task_num": "1", "tot_exec": "149.691µs", "tot_time": "7.56536502s", "tot_wait": "7.565194284s", "wall_time": "1.513240352s" } } ], "storeType": "tidb", "taskType": "root", "__node_attrs": { "id": "3", "collapsed": false, "collapsiable": true, "isNodeDetailVisible": false, "nodeFlexSize": { "width": 280, "height": 200 } } } ], "copExecInfo": {}, "cost": 1856802.6, "diagnosis": [], "diskBytes": "N/A", "duration": "1.51s", "estRows": 8000, "labels": [], "memoryBytes": "34596", "name": "Projection_9", "operatorInfo": "Column#1, Column#2, Column#3, Column#4, Column#7, field(lower(Column#1), tiflash, tikv, pd, tidb)->Column#8", "rootBasicExecInfo": { "loops": "6", "time": "1.51s" }, "rootGroupExecInfo": [ { "Concurrency": "5" } ], "storeType": "tidb", "taskType": "root", "__node_attrs": { "id": "2", "collapsed": false, "collapsiable": true, "isNodeDetailVisible": false, "nodeFlexSize": { "width": 280, "height": 200 } } } ], "copExecInfo": {}, "cost": 7403943.686437106, "diagnosis": [], "diskBytes": "N/A", "duration": "1.51s", "estRows": 8000, "labels": [], "memoryBytes": "18792", "name": "Sort_7", "operatorInfo": "Column#8:desc, Column#2, Column#3, Column#4", "rootBasicExecInfo": { "loops": "2", "time": "1.51s" }, "rootGroupExecInfo": [], "storeType": "tidb", "taskType": "root" }, "withRuntimeStats": true } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/BinaryPlanTable/sample-data/detail-res.json ================================================ { "digest": "877ddf60c6084ae30353cc484f375e5159c231f0d9363213d1d1cc2ffbd272ce", "query": "SELECT *, FIELD(LOWER(A.TYPE), 'tiflash', 'tikv', 'pd', 'tidb') AS _ORDER FROM ( SELECT TYPE, INSTANCE, DEVICE_TYPE, DEVICE_NAME, JSON_OBJECTAGG(NAME, VALUE) AS JSON_VALUE FROM INFORMATION_SCHEMA.CLUSTER_LOAD WHERE DEVICE_TYPE IN (?,?) GROUP BY TYPE, INSTANCE, DEVICE_TYPE, DEVICE_NAME ) AS A ORDER BY _ORDER DESC, INSTANCE, DEVICE_TYPE, DEVICE_NAME [arguments: (\"memory\", \"cpu\")];", "instance": "127.0.0.1:10080", "db": "", "connection_id": "5414958427354957035", "success": 1, "timestamp": 1690272708.823209, "query_time": 1.51515176, "parse_time": 0, "compile_time": 0.00103503, "rewrite_time": 0.000332248, "preproc_subqueries_time": 0, "optimize_time": 0.000354698, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0.000068678, "exec_retry_time": 0, "memory_max": 220680, "disk_max": 0, "txn_start_ts": "0", "prev_stmt": "", "plan": "\tid \ttask\testRows\toperator info \tactRows\texecution info \tmemory \tdisk\n\tSort_7 \troot\t8000 \tColumn#8:desc, Column#2, Column#3, Column#4 \t12 \ttime:1.51s, loops:2 \t18.4 KB \t0 Bytes\n\t└─Projection_9 \troot\t8000 \tColumn#1, Column#2, Column#3, Column#4, Column#7, field(lower(Column#1), tiflash, tikv, pd, tidb)-\u003eColumn#8 \t12 \ttime:1.51s, loops:6, Concurrency:5 \t33.8 KB \tN/A\n\t └─HashAgg_10 \troot\t8000 \tgroup by:Column#1, Column#2, Column#3, Column#4, funcs:json_objectagg(Column#5, Column#6)-\u003eColumn#7, funcs:firstrow(Column#1)-\u003eColumn#1, funcs:firstrow(Column#2)-\u003eColumn#2, funcs:firstrow(Column#3)-\u003eColumn#3, funcs:firstrow(Column#4)-\u003eColumn#4\t12 \ttime:1.51s, loops:6, partial_worker:{wall_time:1.513240352s, concurrency:5, task_num:1, tot_wait:7.565194284s, tot_exec:149.691µs, tot_time:7.56536502s, max:1.513153823s, p95:1.513153823s}, final_worker:{wall_time:1.5133523s, concurrency:5, task_num:5, tot_wait:7.566142195s, tot_exec:223.043µs, tot_time:7.566371614s, max:1.513316486s, p95:1.513316486s}\t47.4 KB \tN/A\n\t └─Selection_11 \troot\t8000 \tin(Column#3, \"memory\", \"cpu\") \t69 \ttime:1.51s, loops:2 \t135.2 KB\tN/A\n\t └─MemTableScan_12\troot\t10000 \ttable:CLUSTER_LOAD \t1193 \ttime:1.51s, loops:3 \tN/A \tN/A", "binary_plan": "{\"discardedDueToTooLong\":false,\"main\":{\"accessObjects\":[],\"actRows\":12,\"children\":[{\"accessObjects\":[],\"actRows\":12,\"children\":[{\"accessObjects\":[],\"actRows\":12,\"children\":[{\"accessObjects\":[],\"actRows\":69,\"children\":[{\"accessObjects\":[{\"scanObject\":{\"database\":\"INFORMATION_SCHEMA\",\"table\":\"CLUSTER_LOAD\"}}],\"actRows\":1193,\"copExecInfo\":{},\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"1.51s\",\"estRows\":10000,\"labels\":[],\"memoryBytes\":\"N/A\",\"name\":\"MemTableScan_12\",\"rootBasicExecInfo\":{\"loops\":\"3\",\"time\":\"1.51s\"},\"rootGroupExecInfo\":[],\"storeType\":\"tidb\",\"taskType\":\"root\"}],\"copExecInfo\":{},\"cost\":499000,\"diagnosis\":[\"high_est_error\"],\"diskBytes\":\"N/A\",\"duration\":\"1.51s\",\"estRows\":8000,\"labels\":[],\"memoryBytes\":\"138400\",\"name\":\"Selection_11\",\"operatorInfo\":\"in(Column#3, \\\"memory\\\", \\\"cpu\\\")\",\"rootBasicExecInfo\":{\"loops\":\"2\",\"time\":\"1.51s\"},\"rootGroupExecInfo\":[],\"storeType\":\"tidb\",\"taskType\":\"root\"}],\"copExecInfo\":{},\"cost\":1772970.6,\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"1.51s\",\"estRows\":8000,\"labels\":[],\"memoryBytes\":\"48580\",\"name\":\"HashAgg_10\",\"operatorInfo\":\"group by:Column#1, Column#2, Column#3, Column#4, funcs:json_objectagg(Column#5, Column#6)-\\u003eColumn#7, funcs:firstrow(Column#1)-\\u003eColumn#1, funcs:firstrow(Column#2)-\\u003eColumn#2, funcs:firstrow(Column#3)-\\u003eColumn#3, funcs:firstrow(Column#4)-\\u003eColumn#4\",\"rootBasicExecInfo\":{\"loops\":\"6\",\"time\":\"1.51s\"},\"rootGroupExecInfo\":[{\"final_worker\":{\"concurrency\":\"5\",\"max\":\"1.513316486s\",\"p95\":\"1.513316486s\",\"task_num\":\"5\",\"tot_exec\":\"223.043µs\",\"tot_time\":\"7.566371614s\",\"tot_wait\":\"7.566142195s\",\"wall_time\":\"1.5133523s\"},\"partial_worker\":{\"concurrency\":\"5\",\"max\":\"1.513153823s\",\"p95\":\"1.513153823s\",\"task_num\":\"1\",\"tot_exec\":\"149.691µs\",\"tot_time\":\"7.56536502s\",\"tot_wait\":\"7.565194284s\",\"wall_time\":\"1.513240352s\"}}],\"storeType\":\"tidb\",\"taskType\":\"root\"}],\"copExecInfo\":{},\"cost\":1856802.6,\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"1.51s\",\"estRows\":8000,\"labels\":[],\"memoryBytes\":\"34596\",\"name\":\"Projection_9\",\"operatorInfo\":\"Column#1, Column#2, Column#3, Column#4, Column#7, field(lower(Column#1), tiflash, tikv, pd, tidb)-\\u003eColumn#8\",\"rootBasicExecInfo\":{\"loops\":\"6\",\"time\":\"1.51s\"},\"rootGroupExecInfo\":[{\"Concurrency\":\"5\"}],\"storeType\":\"tidb\",\"taskType\":\"root\"}],\"copExecInfo\":{},\"cost\":7403943.686437106,\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"1.51s\",\"estRows\":8000,\"labels\":[],\"memoryBytes\":\"18792\",\"name\":\"Sort_7\",\"operatorInfo\":\"Column#8:desc, Column#2, Column#3, Column#4\",\"rootBasicExecInfo\":{\"loops\":\"2\",\"time\":\"1.51s\"},\"rootGroupExecInfo\":[],\"storeType\":\"tidb\",\"taskType\":\"root\"},\"withRuntimeStats\":true}", "binary_plan_text": "\n| id | estRows | estCost | actRows | task | access object | execution info | operator info | memory | disk |\n| Sort_7 | 8000.00 | 7403943.69 | 12 | root | | time:1.51s, loops:2 | Column#8:desc, Column#2, Column#3, Column#4 | 18.4 KB | 0 Bytes |\n| └─Projection_9 | 8000.00 | 1856802.60 | 12 | root | | time:1.51s, loops:6, Concurrency:5 | Column#1, Column#2, Column#3, Column#4, Column#7, field(lower(Column#1), tiflash, tikv, pd, tidb)-\u003eColumn#8 | 33.8 KB | N/A |\n| └─HashAgg_10 | 8000.00 | 1772970.60 | 12 | root | | time:1.51s, loops:6, partial_worker:{wall_time:1.513240352s, concurrency:5, task_num:1, tot_wait:7.565194284s, tot_exec:149.691µs, tot_time:7.56536502s, max:1.513153823s, p95:1.513153823s}, final_worker:{wall_time:1.5133523s, concurrency:5, task_num:5, tot_wait:7.566142195s, tot_exec:223.043µs, tot_time:7.566371614s, max:1.513316486s, p95:1.513316486s} | group by:Column#1, Column#2, Column#3, Column#4, funcs:json_objectagg(Column#5, Column#6)-\u003eColumn#7, funcs:firstrow(Column#1)-\u003eColumn#1, funcs:firstrow(Column#2)-\u003eColumn#2, funcs:firstrow(Column#3)-\u003eColumn#3, funcs:firstrow(Column#4)-\u003eColumn#4 | 47.4 KB | N/A |\n| └─Selection_11 | 8000.00 | 499000.00 | 69 | root | | time:1.51s, loops:2 | in(Column#3, \"memory\", \"cpu\") | 135.2 KB | N/A |\n| └─MemTableScan_12 | 10000.00 | 0.00 | 1193 | root | table:CLUSTER_LOAD | time:1.51s, loops:3 | | N/A | N/A |\n", "warnings": [ { "Level": "Warning", "Message": "skip prepared plan-cache: PhysicalMemTable plan is un-cacheable" } ], "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 1, "plan_from_cache": 0, "plan_from_binding": 0, "user": "root", "host": "127.0.0.1", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0 } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Blink/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .blinkActive { animation: blink 0.7s 2 ease-in-out; } @keyframes blink { 0% { background-color: transparent; } 50% { background-color: rgba(@gold-5, 0.4); } 100% { background-color: transparent; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Blink/index.tsx ================================================ import useQueryParams from '@lib/utils/useQueryParams' import React from 'react' import cx from 'classnames' import styles from './index.module.less' export interface IBlinkProps extends React.HTMLAttributes { activeId: string } export default function Blink({ activeId, children, className, ...restProps }: IBlinkProps) { const { blink } = useQueryParams() return (
{children}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Card/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .cardContainer { // &:before, // &:after { // // Handle margin collapse // content: ' '; // display: table; // } } .cardInner { margin: @padding-page; &.noMargin { margin: 0; } &.noMarginTop { margin-top: 0; } &.noMarginBottom { margin-bottom: 0; } &.noMarginLeft { margin-left: 0; } &.noMarginRight { margin-right: 0; } } .cardTitleSection { margin: @padding-lg 0; display: flex; flex-direction: row; align-items: center; } .cardTitle { color: @heading-color; font-size: @heading-4-size; line-height: 32px; margin-right: @padding-md; } .cardSubTitle { margin-left: @padding-md; margin-right: @padding-md; } .cardTitleSpacer { flex-grow: 1; } .hasTitle > .cardContent { margin-top: @padding-lg; } .cardContainer.flexGrow { display: flex; flex-grow: 1; flex-direction: column; .cardInner, .cardContent { display: flex; flex-grow: 1; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Card/index.tsx ================================================ import React, { ReactNode } from 'react' import cx from 'classnames' import styles from './index.module.less' export interface ICardProps extends Omit, 'title'> { title?: ReactNode subTitle?: ReactNode extra?: ReactNode noMargin?: boolean noMarginTop?: boolean noMarginBottom?: boolean noMarginLeft?: boolean noMarginRight?: boolean flexGrow?: boolean } export default function Card({ title, subTitle, extra, className, noMargin, noMarginTop, noMarginBottom, noMarginLeft, noMarginRight, flexGrow, children, ...rest }: ICardProps) { return (
{(title || subTitle || extra) && (
{title &&
{title}
} {subTitle &&
{subTitle}
}
{extra &&
{extra}
}
)} {children &&
{children}
}
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/CardTable/GroupHeader.tsx ================================================ // FIXME: This is mostly a clone from https://github.com/microsoft/fluentui/blob/master/packages/office-ui-fabric-react/src/components/GroupedList/GroupHeader.base.tsx, but replaced with Ant'd Checkbox // Drop it after https://github.com/microsoft/fluentui/issues/13144 is resolved import React from 'react' import { classNamesFunction, styled } from 'office-ui-fabric-react/lib/Utilities' import { IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupHeaderProps, GroupSpacer } from 'office-ui-fabric-react/lib/GroupedList' import { FocusZone, FocusZoneDirection } from 'office-ui-fabric-react/lib/FocusZone' import { getStyles } from 'office-ui-fabric-react/lib/components/GroupedList/GroupHeader.styles' import { Icon } from 'office-ui-fabric-react/lib/Icon' import { Checkbox } from 'antd' import { useMemoizedFn } from 'ahooks' const getClassNames = classNamesFunction< IGroupHeaderStyleProps, IGroupHeaderStyles >() function BaseAntCheckboxGroupHeader(props: IGroupHeaderProps) { const _classNames = getClassNames(props.styles, { theme: props.theme!, className: props.className, selected: props.selected, isCollapsed: props.group?.isCollapsed, compact: props.compact }) const _onHeaderClick = useMemoizedFn(() => { if (props.onToggleSelectGroup) { props.onToggleSelectGroup(props.group!) } }) const _onToggleSelectGroupClick = useMemoizedFn( (ev: React.MouseEvent) => { if (props.onToggleSelectGroup) { props.onToggleSelectGroup(props.group!) } ev.preventDefault() ev.stopPropagation() } ) const _onToggleCollapse = useMemoizedFn( (ev: React.MouseEvent) => { if (props.onToggleCollapse) { props.onToggleCollapse(props.group!) } ev.stopPropagation() ev.preventDefault() } ) return (
{props.group?.name}
) } export const AntCheckboxGroupHeader: React.FunctionComponent = styled( BaseAntCheckboxGroupHeader, getStyles, undefined, { scope: 'GroupHeader' } ) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/CardTable/index.module.less ================================================ .cardTable { :global { .ms-DetailsRow { .ms-DetailsRow-fields { font-size: 0.9rem; } > :first-child.ms-DetailsRow-fields { > :first-child { padding-left: @padding-page; } } > .ms-DetailsRow-cellCheck + .ms-DetailsRow-fields { > :first-child { padding-left: 0; } } } } } .cardTable.contentExtended { :global { .ms-DetailsRow { > :first-child.ms-DetailsRow-fields { > :last-child { padding-right: @padding-page; } } > .ms-DetailsRow-cellCheck + .ms-DetailsRow-fields { > :first-child { padding-left: 0; } } } } } .tableHeader { :global { .ms-DetailsHeader { padding-top: 0; > :first-child .ms-DetailsHeader-cellTitle { padding-left: @padding-page; } > :first-child.ms-DetailsHeader-cellIsCheck + .ms-DetailsHeader-cell .ms-DetailsHeader-cellTitle { padding-left: 0; } // FIXME: For sticky headers, when there is `.contentExtended`, we // need to add padding right. // > :nth-last-child(2) .ms-DetailsHeader-cellTitle { // padding-right: @padding-page; // } // &.is-resizingColumn > :nth-last-child(3) .ms-DetailsHeader-cellTitle { // // FIXME: This is highly magical // padding-right: @padding-page; // } } } } .clickableTableRow { cursor: pointer; } .highlightRow { border: 1px solid; } .cardTableContent { margin-left: -@padding-page; margin-right: -@padding-page; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/CardTable/index.tsx ================================================ import { IRenderFunction } from '@uifabric/utilities' import { useMemoizedFn } from 'ahooks' import { Checkbox } from 'antd' import cx from 'classnames' import { ColumnActionsMode, ConstrainMode, DetailsList, DetailsListLayoutMode, IColumn, IDetailsList, IDetailsListProps, IDetailsRowProps, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList' import { ScrollToMode } from 'office-ui-fabric-react/lib/List' import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky' import React, { useCallback, useLayoutEffect, useMemo, useRef } from 'react' import AnimatedSkeleton from '../AnimatedSkeleton' import Card from '../Card' import ErrorBar from '../ErrorBar' import styles from './index.module.less' export { AntCheckboxGroupHeader } from './GroupHeader' DetailsList['whyDidYouRender'] = { customName: 'DetailsList' } function renderStickyHeader(props, defaultRender) { if (!props) { return null } return (
{defaultRender!(props)}
) } function renderCheckbox(props) { return } export function ImprovedDetailsList(props: IDetailsListProps) { return ( ) } ImprovedDetailsList.whyDidYouRender = true export const MemoDetailsList = React.memo(ImprovedDetailsList) function copyAndSort( items: T[], columnKey: string, isSortedDescending?: boolean ): T[] { const key = columnKey as keyof T return items .slice(0) .sort((a: T, b: T) => (isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1 ) } export interface ICardTableProps extends IDetailsListProps { title?: React.ReactNode subTitle?: React.ReactNode className?: string style?: object loading?: boolean hideLoadingWhenNotEmpty?: boolean // Whether loading animation should not show when data is not empty errors?: any[] cardExtra?: React.ReactNode cardNoMargin?: boolean cardNoMarginTop?: boolean cardNoMarginBottom?: boolean extendLastColumn?: boolean // The keys of visible columns. If null, all columns will be shown. visibleColumnKeys?: { [key: string]: boolean } visibleItemsCount?: number // Handle sort orderBy?: string desc?: boolean onChangeOrder?: (orderBy: string, desc: boolean) => void // Event triggered when a row is clicked. onRowClicked?: ( item: any, itemIndex: number, ev: React.MouseEvent ) => void clickedRowIndex?: number } function useRenderClickableRow( onRowClicked, clickedRowIdx, customRender?: IRenderFunction | undefined ) { return useCallback( (props, defaultRender) => { if (!props) { return null } return (
onRowClicked?.(props.item, props.itemIndex, ev)} > {customRender ? customRender(props) : defaultRender!(props)}
) }, [onRowClicked, clickedRowIdx, customRender] ) } function dummyColumn(): IColumn { return { name: '', key: 'dummy', minWidth: 28, maxWidth: 28, onRender: (_rec) => null } } export default function CardTable(props: ICardTableProps) { const { title, subTitle, className, style, loading = false, hideLoadingWhenNotEmpty, errors = [], cardExtra, cardNoMargin, cardNoMarginTop, cardNoMarginBottom, extendLastColumn, visibleColumnKeys, visibleItemsCount, orderBy, desc = true, onChangeOrder, onRowClicked, clickedRowIndex, columns, items, onRenderRow, selectionMode = SelectionMode.none, ...restProps } = props const renderClickableRow = useRenderClickableRow( onRowClicked, clickedRowIndex || -1, onRenderRow ) const activeIdx = useRef(-1) const handleActiveItemChange = useCallback((_, index?: number) => { activeIdx.current = index ?? -1 }, []) const onColumnClick = useMemoizedFn( (_ev: React.MouseEvent, column: IColumn) => { if (!onChangeOrder) { return } if (column.key === orderBy) { onChangeOrder(orderBy, !desc) } else { onChangeOrder(column.key, true) } } ) const finalColumns = useMemo(() => { let newColumns: IColumn[] = columns || [] if (visibleColumnKeys != null) { newColumns = newColumns.filter((c) => visibleColumnKeys[c.key]) } newColumns = newColumns.map((c) => ({ ...c, isResizable: c.isResizable ?? true, isSorted: c.key === orderBy, isSortedDescending: desc, onColumnClick, columnActionsMode: c.columnActionsMode || ColumnActionsMode.disabled })) if (!extendLastColumn) { newColumns.push(dummyColumn()) } return newColumns }, [ onColumnClick, columns, visibleColumnKeys, orderBy, desc, extendLastColumn ]) const finalItems = useMemo(() => { let newItems = items || [] const curColumn = finalColumns.find((col) => col.key === orderBy) if (curColumn) { newItems = copyAndSort( newItems, curColumn.fieldName!, curColumn.isSortedDescending ) } if (visibleItemsCount != null) { newItems = newItems.slice(0, visibleItemsCount) } return newItems }, [visibleItemsCount, items, orderBy, finalColumns]) const tableRef = useRef(null) useLayoutEffect(() => { if (activeIdx.current === -1 && (clickedRowIndex ?? -1) >= 0) { setTimeout(() => { tableRef.current?.focusIndex( clickedRowIndex!, true, undefined, ScrollToMode.center ) }, 50) } }, [clickedRowIndex, finalItems]) return (
) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/CardTabs/index.module.less ================================================ .tabs { margin-left: -@padding-page; margin-right: -@padding-page; .card_tab_navs { padding-left: @padding-page; padding-right: @padding-page; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/CardTabs/index.tsx ================================================ import React, { useState } from 'react' import { Tabs } from 'antd' import cx from 'classnames' import styles from './index.module.less' import { TabsProps } from 'antd/es/tabs' type Tab = { key: string title: string content: () => React.ReactNode } export interface ICardTabsProps extends TabsProps { className?: string tabs: Tab[] } function renderCardTabBar(props, DefaultTabBar) { return } function CardTabs({ className, tabs, defaultActiveKey, onChange, renderTabBar, ...restProps }: ICardTabsProps) { const [tabKey, setTabKey] = useState(defaultActiveKey || tabs[0].key) const c = cx(styles.tabs, className) const selectedTab = tabs.find((tab) => tab.key === tabKey) function changeTab(tabKey) { setTabKey(tabKey) onChange && onChange(tabKey) } return ( <> {tabs.map((tab) => ( ))} {selectedTab?.content()} ) } export default CardTabs ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/ColumnsSelector/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .title_container { display: flex; align-items: center; justify-content: space-between; height: 32px; } .foot_container { border-top: 1px solid @border-color-split; margin: 0 -@popover-padding-horizontal; padding: 8px @popover-padding-horizontal 0; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/ColumnsSelector/index.tsx ================================================ import React, { ReactNode, useMemo, useState, useEffect } from 'react' import { Checkbox, Popover, Space, Button } from 'antd' import { DownOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { addTranslationResource } from '@lib/utils/i18n' import styles from './index.module.less' const translations = { en: { trigger_text: 'Columns', select: 'Select', reset: 'Reset' }, zh: { trigger_text: '选择列', select: '选择', reset: '重置' } } for (const key in translations) { addTranslationResource(key, { component: { columnsSelector: translations[key] } }) } export interface IColumnKeys { [key: string]: boolean } export interface IColumnsSelectorProps { columns: IColumn[] visibleColumnKeys?: IColumnKeys defaultVisibleColumnKeys?: IColumnKeys onChange?: (visibleKeys: IColumnKeys) => void foot?: ReactNode } export default function ColumnsSelector({ columns, visibleColumnKeys, defaultVisibleColumnKeys, onChange, foot }: IColumnsSelectorProps) { const { t } = useTranslation() const [indeterminate, setIndeterminate] = useState(true) const [checkedAll, setCheckedAll] = useState(false) const filteredColumns = useMemo( () => columns.filter((c) => c.key !== 'dummy'), [columns] ) const visibleKeys = useMemo(() => { if (visibleColumnKeys) { return visibleColumnKeys } return columns.reduce((acc, cur) => { acc[cur.key] = true return acc }, {}) }, [visibleColumnKeys, columns]) useEffect(() => { function updateCheckAllStatus(columnKeys) { const checkedKeysCount = Object.keys(columnKeys).filter( (k) => columnKeys[k] && k !== 'dummy' ).length setIndeterminate( checkedKeysCount > 0 && checkedKeysCount < filteredColumns.length ) setCheckedAll(checkedKeysCount === filteredColumns.length) } updateCheckAllStatus(visibleKeys) }, [visibleKeys, filteredColumns]) function handleCheckAllChange(e) { const checked = e.target.checked const newVisibleKeys = columns.reduce((acc, cur) => { acc[cur.key] = checked return acc }, {}) onChange && onChange(newVisibleKeys) } function handleCheckChange(e, column: IColumn) { const checked = e.target.checked const newVisibleKeys = { ...visibleKeys, [column.key]: checked } onChange && onChange(newVisibleKeys) } const title = (
{t('component.columnsSelector.select')} {defaultVisibleColumnKeys && ( )}
) const content = (
{filteredColumns.map((column) => ( handleCheckChange(e, column)} > {column.name} ))} {foot &&
{foot}
}
) return ( {t('component.columnsSelector.trigger_text')} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/CopyLink/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .copiedText { color: @success-color; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/CopyLink/index.tsx ================================================ import React, { useState } from 'react' import { CopyToClipboard } from 'react-copy-to-clipboard' import { useTranslation } from 'react-i18next' import { useTimeoutFn } from 'react-use' import { CheckOutlined, CopyOutlined } from '@ant-design/icons' import { addTranslationResource } from '@lib/utils/i18n' import styles from './index.module.less' type DisplayVariant = 'default' | 'original_sql' | 'formatted_sql' const transKeys: { [K in DisplayVariant]: string } = { default: 'copy', original_sql: 'copyOriginal', formatted_sql: 'copyFormatted' } export interface ICopyLinkProps extends React.DetailedHTMLProps< React.HTMLAttributes, HTMLSpanElement > { data?: string displayVariant?: DisplayVariant } const translations = { en: { copy: 'Copy', copyOriginal: 'Copy Original', copyFormatted: 'Copy Formatted', success: 'Copied' }, zh: { copy: '复制', copyOriginal: '复制原始 SQL', copyFormatted: '复制格式化 SQL', success: '已复制' } } for (const key in translations) { addTranslationResource(key, { component: { copyLink: translations[key] } }) } function CopyLink({ data, displayVariant = 'default', ...otherProps }: ICopyLinkProps) { const { t } = useTranslation() const [showCopied, setShowCopied] = useState(false) const reset = useTimeoutFn(() => { setShowCopied(false) }, 1500)[2] const handleCopy = () => { setShowCopied(true) reset() } return ( {!showCopied && ( {t(`component.copyLink.${transKeys[displayVariant]}`)}{' '} )} {showCopied && ( {t('component.copyLink.success')} )} ) } export default React.memo(CopyLink) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/DatePicker/index.tsx ================================================ import { Dayjs } from 'dayjs' import dayjsGenerateConfig from 'rc-picker/es/generate/dayjs' import generatePicker from 'antd/es/date-picker/generatePicker' import 'antd/es/date-picker/style/index' const DatePicker = generatePicker(dayjsGenerateConfig) export default DatePicker ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/DateTime/Calendar.tsx ================================================ import React from 'react' import { Tooltip } from 'antd' import dayjs from 'dayjs' import { useTranslation } from 'react-i18next' import { addTranslationResource } from '@lib/utils/i18n' import i18next from 'i18next' import { format as longFormat } from './Long' import { IDateTimeProps } from '.' import calendar from './calendarPlugin' import weekOfYear from 'dayjs/plugin/weekOfYear' import localizedFormat from 'dayjs/plugin/localizedFormat' import tz from '@lib/utils/timezone' dayjs.extend(calendar) dayjs.extend(weekOfYear) dayjs.extend(localizedFormat) const translations = { en: { sameDay: '[Today at] h:mm A (UTCZ)', sameWeek: 'dddd h:mm A (UTCZ)', nextDay: '[Tomorrow] h:mm A (UTCZ)', nextWeek: '[Next] dddd h:mm A (UTCZ)', lastDay: '[Yesterday] h:mm A (UTCZ)', lastWeek: '[Last] dddd h:mm A (UTCZ)', sameElse: 'lll (UTCZ)' }, zh: { sameDay: '[今天] HH:mm (UTCZ)', sameWeek: 'dddd HH:mm (UTCZ)', nextDay: '[明天] HH:mm (UTCZ)', nextWeek: '[下]dddd HH:mm (UTCZ)', lastDay: '[昨天] HH:mm (UTCZ)', lastWeek: '[上]dddd HH:mm (UTCZ)', sameElse: 'lll (UTCZ)' } } for (const key in translations) { addTranslationResource(key, { component: { dateTime: { calendar: translations[key] } } }) } function Calendar({ unixTimestampMs, ...rest }: IDateTimeProps) { useTranslation() // Re-render when language changes return ( {format(unixTimestampMs)} ) } export function format(unixTimestampMs: number) { return dayjs(unixTimestampMs) .utcOffset(tz.getTimeZone()) .calendar(undefined, { sameDay: i18next.t('component.dateTime.calendar.sameDay'), sameWeek: i18next.t('component.dateTime.calendar.sameWeek'), nextDay: i18next.t('component.dateTime.calendar.nextDay'), nextWeek: i18next.t('component.dateTime.calendar.nextWeek'), lastDay: i18next.t('component.dateTime.calendar.lastDay'), lastWeek: i18next.t('component.dateTime.calendar.lastWeek'), sameElse: i18next.t('component.dateTime.calendar.sameElse') }) } export default React.memo(Calendar) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/DateTime/Long.tsx ================================================ import React from 'react' import { Tooltip } from 'antd' import dayjs from 'dayjs' import { useTranslation } from 'react-i18next' import localizedFormat from 'dayjs/plugin/localizedFormat' import tz from '@lib/utils/timezone' import { IDateTimeProps } from '.' dayjs.extend(localizedFormat) function Long({ unixTimestampMs, ...rest }: IDateTimeProps) { useTranslation() // Re-render when language changes return ( {format(unixTimestampMs)} ) } export function format(unixTimestampMs: number) { return dayjs(unixTimestampMs) .utcOffset(tz.getTimeZone()) .format('ll LTS (UTCZ)') } export default React.memo(Long) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/DateTime/calendarPlugin.ts ================================================ // Copyright 2024 PingCAP, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Inspired by: // https://github.com/iamkun/dayjs/issues/1226#issuecomment-768796249 // https://github.com/iamkun/dayjs/blob/dev/src/plugin/calendar/index.js declare module 'dayjs' { interface Dayjs { calendar(refTime?: any, formats?: object): string } } export default (o, c, d) => { const LT = 'h:mm A' const L = 'MM/DD/YYYY' const calendarFormat = { lastDay: `[Yesterday at] ${LT}`, sameDay: `[Today at] ${LT}`, nextDay: `[Tomorrow at] ${LT}`, sameWeek: `dddd [at] ${LT}`, nextWeek: `[Next] dddd [at] ${LT}`, lastWeek: `[Last] dddd [at] ${LT}`, sameElse: L } const proto = c.prototype proto.calendar = function (refTime, formats) { const format = formats || this.$locale().calendar || calendarFormat const refDayStart = d(refTime || undefined).startOf('d') let retVal = '' const dayDiff = this.diff(refDayStart, 'd', true) if (dayDiff < -14 || dayDiff > 14) { retVal = 'sameElse' } else if (dayDiff < 0 && dayDiff >= -1) { retVal = 'lastDay' } else if (dayDiff >= 0 && dayDiff < 1) { retVal = 'sameDay' } else if (dayDiff >= 1 && dayDiff < 2) { retVal = 'nextDay' } else if (dayDiff < -1) { // -14 ~ -1 if (this.startOf('week').unix() === refDayStart.startOf('week').unix()) { retVal = 'sameWeek' } else { const pass1Week = this.add(1, 'week') if ( pass1Week.startOf('week').unix() === refDayStart.startOf('week').unix() ) { retVal = 'lastWeek' } } } else if (dayDiff >= 2) { // 2 ~ 14 if (this.startOf('week').unix() === refDayStart.startOf('week').unix()) { retVal = 'sameWeek' } else { const back1Week = this.subtract(1, 'week') if ( back1Week.startOf('week').unix() === refDayStart.startOf('week').unix() ) { retVal = 'nextWeek' } } } if (retVal === '') { retVal = 'sameElse' } /* eslint-enable no-nested-ternary */ const currentFormat = format[retVal] || calendarFormat[retVal] if (typeof currentFormat === 'function') { return currentFormat.call(this, d()) } return this.format(currentFormat) } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/DateTime/index.tsx ================================================ import Calendar from './Calendar' import Long from './Long' export interface IDateTimeProps { unixTimestampMs: number } export default { Calendar, Long } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Descriptions/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .descriptions { :global { .ant-descriptions-row { line-height: 1; } .ant-descriptions-row > th { padding: 0; padding-bottom: @padding-xs; } .ant-descriptions-row > td { padding: 0; padding-bottom: @padding-md; } .ant-descriptions-item-label { color: @text-color-secondary; } .ant-descriptions-item-content { color: @text-color; } } } .item { // nothing for now } .itemSingleline { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; > span { display: inline; } pre { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } } .itemMultiline { overflow-wrap: break-word; white-space: normal; text-overflow: inherit; overflow: auto; :global { .ant-descriptions-item-container { overflow: auto; } } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Descriptions/index.tsx ================================================ import React from 'react' import { Descriptions as AntDescriptions } from 'antd' import type { DescriptionsItemProps } from 'antd/es/descriptions/Item' import cx from 'classnames' import styles from './index.module.less' export interface IDescriptionsProps { className?: string children?: | (React.ReactElement | null | undefined)[] | React.ReactElement column?: number onClick?: () => void } export interface IDescriptionsItemProps extends DescriptionsItemProps { className?: string children: React.ReactNode multiline?: boolean onClick?: () => void } // FIXME: This logic duplicates to function mapItem(item: React.ReactElement) { const { props } = item const { multiline, className, children, ...restProps } = props const c = cx(className, styles.item, { [styles.itemMultiline]: multiline, [styles.itemSingleline]: !multiline }) return ( {children} ) } function Descriptions({ className, children, column, ...restProps }: IDescriptionsProps) { const c = cx(className, styles.descriptions) let realChildren if (children) { if (Array.isArray(children)) { realChildren = children.filter((v) => v != null).map((v) => mapItem(v!)) } else { realChildren = mapItem(children) } } return ( {realChildren} ) } Descriptions.Item = AntDescriptions.Item as React.FC export default Descriptions ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/DrawerFooter/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .container { position: sticky; bottom: -@drawer-body-padding; margin: -@drawer-body-padding; background: @drawer-bg; padding: @drawer-body-padding; transition: box-shadow 0.1s linear; &.withShadow { box-shadow: 0 -5px 10px rgba(#000, 0.1); } } .mark { height: 1px; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/DrawerFooter/index.tsx ================================================ import React from 'react' import { useInView } from 'react-intersection-observer' import cx from 'classnames' import styles from './index.module.less' // A sticky footer for Antd Drawer component. function DrawerFooter({ children, className, ...rest }: React.HTMLAttributes) { const { ref, inView } = useInView({ initialInView: true, // prevent shadow being displayed at the beginning rootMargin: '-24px' // equals to @drawer-body-padding }) const displayShadow = !inView return ( <>
{children}
) } export default React.memo(DrawerFooter) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/ErrorBar/index.tsx ================================================ import { Alert } from 'antd' import _ from 'lodash' import React, { useMemo } from 'react' export interface IErrorBarProps { errors: any[] } export default function ErrorBar({ errors }: IErrorBarProps) { // show at most 3 kinds of errors const errorMsgs = useMemo( () => _.uniq(_.map(errors, (err) => err?.message || '')) .filter((msg) => msg !== '') .slice(0, 3), [errors] ) if (errorMsgs.length === 0) { return null } else if (errorMsgs.length === 1) { return ( ) } else { return ( {errorMsgs.map((msg, idx) => (
  • {msg}
  • ))} } data-e2e="alert_error_bar" /> ) } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Expand/index.tsx ================================================ import React from 'react' import { useTranslation } from 'react-i18next' import { addTranslationResource } from '@lib/utils/i18n' export interface IExpandProps { expanded?: boolean collapsedContent?: React.ReactNode children: React.ReactNode } function Expand({ collapsedContent, children, expanded }: IExpandProps) { // FIXME: Animations return (
    {expanded ? children : collapsedContent ?? children}
    ) } const translations = { en: { expandText: 'Expand', collapseText: 'Collapse' }, zh: { expandText: '展开', collapseText: '收起' } } for (const key in translations) { addTranslationResource(key, { component: { expandLink: translations[key] } }) } export interface IExpandLinkProps extends React.AnchorHTMLAttributes { expanded?: boolean } function Link({ expanded, ...restProps }: IExpandLinkProps) { const { t } = useTranslation() return ( {expanded ? t('component.expandLink.collapseText') : t('component.expandLink.expandText')} ) } Expand.Link = Link export default Expand ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Head/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .headContainer { // &:before, // &:after { // // Handle margin collapse // content: ' '; // display: table; // } } .headInner { margin-top: @padding-page; } .headTitleSection { margin: @padding-lg @padding-page; display: flex; flex-direction: row; align-items: center; } .headBack { margin-right: @padding-lg; flex-shrink: 0; } .headTitle { color: @heading-color; font-size: @heading-4-size; line-height: 32px; flex-grow: 1; margin-right: @padding-lg; } .headContent { margin: @padding-lg @padding-page; } .headFooter { margin-top: @padding-lg; border-bottom: 1px solid @border-color-base; padding: 0 @padding-page; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Head/index.tsx ================================================ import React, { ReactNode } from 'react' import cx from 'classnames' import styles from './index.module.less' export interface IHeadProps { title: string titleExtra?: ReactNode back?: ReactNode footer?: ReactNode className?: string children?: ReactNode } function Head({ title, titleExtra, back, footer, className, children, ...rest }: IHeadProps) { return (
    {(title || titleExtra || back) && (
    {back &&
    {back}
    } {title &&
    {title}
    } {titleExtra &&
    {titleExtra}
    }
    )} {children &&
    {children}
    } {footer &&
    {footer}
    }
    ) } export default Head ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/HighlightSQL/index.tsx ================================================ import React, { useMemo } from 'react' // This usage will generate tons of files about languages highlight when esbuild splitting enable // See https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/src/async-languages/hljs.js to understand why // import { Light as SyntaxHighlighter } from 'react-syntax-highlighter' import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/light' import sql from 'react-syntax-highlighter/dist/esm/languages/hljs/sql' import lightTheme from 'react-syntax-highlighter/dist/esm/styles/hljs/atom-one-light' import darkTheme from 'react-syntax-highlighter/dist/esm/styles/hljs/atom-one-dark' import Pre from '../Pre' import formatSql from '@lib/utils/sqlFormatter' import moize from 'moize' SyntaxHighlighter.registerLanguage('sql', sql) interface Props { sql: string compact?: boolean theme?: 'dark' | 'light' format?: boolean maxLen?: number } function simpleSqlMinify(str) { return str .replace(/\s{1,}/g, ' ') .replace(/\{\s{1,}/g, '{') .replace(/\}\s{1,}/g, '}') .replace(/;\s{1,}/g, ';') .replace(/\/\*\s{1,}/g, '/*') .replace(/\*\/\s{1,}/g, '*/') } function HighlightSQL({ sql, compact, theme = 'light', format = true, maxLen = 5000 }: Props) { const formattedSql = useMemo(() => { const truncatedSql = sql.length > maxLen ? `${sql.slice(0, maxLen)}...(remain: ${sql.length - maxLen})` : sql let f = format ? formatSql(truncatedSql) : truncatedSql if (compact) { f = simpleSqlMinify(f) } return f }, [sql, compact, format, maxLen]) return ( {formattedSql} ) } export default moize(HighlightSQL, { isShallowEqual: true, maxArgs: 5, maxSize: 1000 }) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/InstanceSelect/DropOverlay.tsx ================================================ import React, { useState, useMemo } from 'react' import { AntCheckboxGroupHeader } from '../' import { IColumn, ISelection } from 'office-ui-fabric-react/lib/DetailsList' import { IInstanceTableItem, filterInstanceTable } from '@lib/utils/instanceTable' import { useTranslation } from 'react-i18next' import TableWithFilter, { ITableWithFilterRefProps } from './TableWithFilter' const groupProps = { onRenderHeader: (props) => } export interface IDropOverlayProps { selection: ISelection columns: IColumn[] items: IInstanceTableItem[] filterTableRef?: React.Ref containerProps?: React.HTMLAttributes } function DropOverlay({ selection, columns, items, filterTableRef, containerProps }: IDropOverlayProps) { const { t } = useTranslation() const [keyword, setKeyword] = useState('') const [finalItems, finalGroups] = useMemo(() => { return filterInstanceTable(items, keyword) }, [items, keyword]) const { style: containerStyle, ...restContainerProps } = containerProps ?? {} const finalContainerProps = useMemo(() => { const style: React.CSSProperties = { fontSize: '0.9rem', ...containerStyle } return { style, ...restContainerProps } as React.HTMLAttributes & Record }, [containerStyle, restContainerProps]) return ( ) } export default React.memo(DropOverlay) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/InstanceSelect/TableWithFilter.module.less ================================================ .tableWithFilterContainer { :global { .ms-DetailsHeader { padding-top: 0; } .ant-input-affix-wrapper { border: 0; box-shadow: none; outline: 0; } } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/InstanceSelect/TableWithFilter.tsx ================================================ import React, { useMemo, useCallback, useRef } from 'react' import cx from 'classnames' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection' import { SelectionMode } from 'office-ui-fabric-react/lib/Selection' import { useSize } from 'ahooks' import { DetailsListLayoutMode, ISelection, IDetailsListProps } from 'office-ui-fabric-react/lib/DetailsList' import { Input, InputRef } from 'antd' import { MemoDetailsList } from '../' import styles from './TableWithFilter.module.less' export interface ITableWithFilterProps extends IDetailsListProps { selection: ISelection filterPlaceholder?: string filter?: string onFilterChange?: (value: string) => void tableMaxHeight?: number tableWidth?: number containerProps?: React.HTMLAttributes } export interface ITableWithFilterRefProps { focusFilterInput: () => void } function TableWithFilter( { selection, filterPlaceholder, filter, onFilterChange, tableMaxHeight, tableWidth, containerProps, ...restProps }: ITableWithFilterProps, ref: React.Ref ) { const handleInputChange = useCallback( (e: React.ChangeEvent) => { onFilterChange?.(e.target.value) }, [onFilterChange] ) const inputRef = useRef(null) React.useImperativeHandle(ref, () => ({ focusFilterInput() { inputRef.current?.focus() } })) // FIXME: We should put Input inside ScrollablePane after https://github.com/microsoft/fluentui/issues/13557 is resolved const containerRef = useRef(null) const containerSize = useSize(containerRef) const paneStyle = useMemo( () => ({ position: 'relative', height: containerSize?.height, maxHeight: tableMaxHeight ?? 400, width: tableWidth ?? 400 } as React.CSSProperties), [containerSize?.height, tableMaxHeight, tableWidth] ) const { className: containerClassName, style: containerStyle, ...containerRestProps } = containerProps ?? {} return (
    ) } export default React.memo(React.forwardRef(TableWithFilter)) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/InstanceSelect/ValueDisplay.tsx ================================================ import React, { useMemo } from 'react' import { IInstanceTableItem, InstanceKind, instanceKindName } from '@lib/utils/instanceTable' import { useTranslation } from 'react-i18next' interface InstanceStat { all: number selected: number } function newInstanceStat(): InstanceStat { return { all: 0, selected: 0 } } export interface IValueDisplayProps { items: IInstanceTableItem[] selectedKeys: string[] } export default function ValueDisplay({ items, selectedKeys }: IValueDisplayProps) { const { t } = useTranslation() const text = useMemo(() => { const selectedKeysMap = {} selectedKeys.forEach((key) => (selectedKeysMap[key] = true)) const instanceStats: { [key in InstanceKind]: InstanceStat } = { pd: newInstanceStat(), tidb: newInstanceStat(), tikv: newInstanceStat(), tiflash: newInstanceStat(), ticdc: newInstanceStat(), tiproxy: newInstanceStat(), tso: newInstanceStat(), scheduling: newInstanceStat() } items.forEach((item) => { instanceStats[item.instanceKind].all++ if (selectedKeysMap[item.key]) { instanceStats[item.instanceKind].selected++ } }) let hasUnselected = false const p: string[] = [] for (const ik in instanceStats) { const stats = instanceStats[ik] as InstanceStat if (stats.selected !== stats.all) { hasUnselected = true } if (stats.selected > 0) { if (stats.all === stats.selected) { p.push( t('component.instanceSelect.selected.partial.all', { component: instanceKindName(ik as InstanceKind) }) ) } else { p.push( t('component.instanceSelect.selected.partial.n', { n: stats.selected, component: instanceKindName(ik as InstanceKind) }) ) } } } if (!hasUnselected) { return t('component.instanceSelect.selected.all') } return p.join(', ') }, [t, items, selectedKeys]) return <>{text} } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/InstanceSelect/index.tsx ================================================ import React, { useCallback, useRef, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useShallowCompareEffect } from 'react-use' import { Tooltip } from 'antd' import { IBaseSelectProps, BaseSelect, InstanceStatusBadge, TextWrap } from '../' import { useClientRequest } from '@lib/utils/useClientRequest' import { addTranslationResource } from '@lib/utils/i18n' import { useMemoizedFn, useControllableValue } from 'ahooks' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import { buildInstanceTable, IInstanceTableItem } from '@lib/utils/instanceTable' import SelectionWithFilter from '@lib/utils/selectionWithFilter' import DropOverlay from './DropOverlay' import ValueDisplay from './ValueDisplay' import { ITableWithFilterRefProps } from './TableWithFilter' import { useChange } from '@lib/utils/useChange' import { TopologyTiDBInfo, ClusterinfoStoreTopologyResponse, TopologyPDInfo, TopologyTiCDCInfo, TopologyTiProxyInfo, TopologyTSOInfo, TopologySchedulingInfo } from '@lib/client' import { ReqConfig } from '@lib/types' import { AxiosPromise } from 'axios' export interface IInstanceSelectProps extends Omit, 'dropdownRender' | 'valueRender'> { onChange?: (value: string[]) => void enableTiFlash?: boolean defaultSelectAll?: boolean dropContainerProps?: React.HTMLAttributes getTiDBTopology(options?: ReqConfig): AxiosPromise> getStoreTopology( options?: ReqConfig ): AxiosPromise getPDTopology(options?: ReqConfig): AxiosPromise> getTiCDCTopology?: ( options?: ReqConfig ) => AxiosPromise> getTiProxyTopology?: ( options?: ReqConfig ) => AxiosPromise> getTSOTopology?: (options?: ReqConfig) => AxiosPromise> getSchedulingTopology?: ( options?: ReqConfig ) => AxiosPromise> } export interface IInstanceSelectRefProps { getInstanceByKeys: (keys: string[]) => IInstanceTableItem[] getInstanceByKey: (key: string) => IInstanceTableItem } const translations = { en: { placeholder: 'Select Instances', filterPlaceholder: 'Filter instance', selected: { all: 'All Instances', partial: { n: '{{n}} {{component}}', all: 'All {{component}}' } }, columns: { key: 'Instance', status: 'Status' } }, zh: { placeholder: '选择实例', filterPlaceholder: '过滤实例', selected: { all: '所有实例', partial: { n: '{{n}} {{component}}', all: '所有 {{component}}' } }, columns: { key: '实例', status: '状态' } } } for (const key in translations) { addTranslationResource(key, { component: { instanceSelect: translations[key] } }) } function InstanceSelect( props: IInstanceSelectProps, ref: React.Ref ) { const [internalVal, setInternalVal] = useControllableValue(props) const setInternalValPersist = useMemoizedFn(setInternalVal) const { enableTiFlash, defaultSelectAll, dropContainerProps, value, // only to exclude from restProps onChange, // only to exclude from restProps getTiDBTopology, getPDTopology, getStoreTopology, getTiCDCTopology, getTiProxyTopology, getTSOTopology, getSchedulingTopology, ...restProps } = props const { t } = useTranslation() const { data: dataTiDB, isLoading: loadingTiDB } = useClientRequest(getTiDBTopology) const { data: dataStores, isLoading: loadingStores } = useClientRequest(getStoreTopology) const { data: dataPD, isLoading: loadingPD } = useClientRequest(getPDTopology) const { data: dataTiCDC, isLoading: loadingTiCDC } = useClientRequest(getTiCDCTopology) const { data: dataTiProxy, isLoading: loadingTiProxy } = useClientRequest(getTiProxyTopology) const { data: dataTSO, isLoading: loadingTSO } = useClientRequest(getTSOTopology) const { data: dataScheduling, isLoading: loadingScheduling } = useClientRequest(getSchedulingTopology) const columns: IColumn[] = useMemo( () => [ { name: t('component.instanceSelect.columns.key'), key: 'key', minWidth: 150, maxWidth: 150, onRender: (node: IInstanceTableItem) => { return ( {node.key} ) } }, { name: t('component.instanceSelect.columns.status'), key: 'status', minWidth: 100, maxWidth: 100, onRender: (node: IInstanceTableItem) => { return ( ) } } ], [t] ) const [tableItems] = useMemo(() => { if ( loadingTiDB || loadingStores || loadingPD || loadingTiCDC || loadingTiProxy || loadingTSO || loadingScheduling ) { return [[], []] } return buildInstanceTable({ dataPD, dataTiDB, dataTiKV: dataStores?.tikv, dataTiFlash: dataStores?.tiflash, dataTiCDC, dataTiProxy, dataTSO, dataScheduling, includeTiFlash: enableTiFlash }) }, [ enableTiFlash, dataTiDB, dataStores, dataPD, dataTiCDC, dataTiProxy, dataTSO, dataScheduling, loadingTiDB, loadingStores, loadingPD, loadingTiCDC, loadingTiProxy, loadingTSO, loadingScheduling ]) const selection = useRef( new SelectionWithFilter({ onSelectionChanged: () => { const s = selection.current.getAllSelection() as IInstanceTableItem[] const keys = s.map((v) => v.key) setInternalValPersist([...keys]) } }) ) useShallowCompareEffect(() => { selection.current?.resetAllSelection(internalVal ?? []) }, [internalVal]) const dataHasLoaded = useRef(false) useChange(() => { // When data is loaded for the first time, we need to: // - Select all if `defaultSelectAll` is set and value is not given. // - Update selection according to value if (dataHasLoaded.current) { return } if (tableItems.length === 0) { return } const sel = selection.current sel.setChangeEvents(false) sel.setAllItems(tableItems) if (internalVal && internalVal.length > 0) { sel.resetAllSelection(internalVal) } else if (defaultSelectAll) { sel.setAllSelectionSelected(true) } sel.setChangeEvents(true) dataHasLoaded.current = true }, [tableItems]) const getInstanceByKeys = useMemoizedFn((keys: string[]) => { const keyToItemMap = {} for (const item of tableItems) { keyToItemMap[item.key] = item } return keys.map((key) => keyToItemMap[key]) }) const getInstanceByKey = useMemoizedFn((key: string) => { return getInstanceByKeys([key])[0] }) React.useImperativeHandle(ref, () => ({ getInstanceByKey, getInstanceByKeys })) const renderValue = useCallback( (selectedKeys) => { if ( tableItems.length === 0 || !selectedKeys || selectedKeys.length === 0 ) { return null } return }, [tableItems] ) const filterTableRef = useRef(null) const renderDropdown = useCallback( () => ( ), [columns, tableItems, dropContainerProps] ) const handleOpened = useCallback(() => { filterTableRef.current?.focusFilterInput() }, []) return ( ) } export default React.forwardRef(InstanceSelect) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/InstanceStatusBadge/index.tsx ================================================ import React from 'react' import { useTranslation } from 'react-i18next' import { InstanceStatus } from '@lib/utils/instanceTable' import { Badge } from 'antd' import { addTranslationResource } from '@lib/utils/i18n' const translations = { en: { status: { up: 'Up', down: 'Down', tombstone: 'Tombstone', offline: 'Leaving', unknown: 'Unknown', unreachable: 'Unreachable' } }, zh: { status: { up: '在线', down: '离线', tombstone: '已缩容下线', offline: '下线中', unknown: '未知', unreachable: '无法访问' } } } for (const key in translations) { addTranslationResource(key, { component: { instanceStatusBadge: translations[key] } }) } export interface IInstanceStatusBadgeProps { status?: number } function InstanceStatusBadge({ status }: IInstanceStatusBadgeProps) { const { t } = useTranslation() switch (status) { case InstanceStatus.Down: return ( ) case InstanceStatus.Unreachable: return ( ) case InstanceStatus.Up: return ( ) case InstanceStatus.Tombstone: return ( ) case InstanceStatus.Offline: return ( ) default: return ( ) } } export default React.memo(InstanceStatusBadge) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/LanguageDropdown/index.tsx ================================================ import { Dropdown, Menu } from 'antd' import _ from 'lodash' import React, { ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { ALL_LANGUAGES } from '@lib/utils/i18n' function LanguageDropdown({ children }: { children: ReactNode }) { const { i18n } = useTranslation() function handleClick(e) { i18n.changeLanguage(e.key) } const menu = ( {_.map(ALL_LANGUAGES, (name, key) => { return {name} })} ) return ( {children} ) } export default LanguageDropdown ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/LimitTimeRange/index.tsx ================================================ import { TimeRange, TimeRangeSelector } from '@lib/components' import dayjs from 'dayjs' import React, { useMemo } from 'react' interface LimitTimeRangeProps { value: TimeRange recent_seconds?: number[] customAbsoluteRangePicker?: boolean onChange: (val: TimeRange) => void onZoomOutClick: (start: number, end: number) => void disabled?: boolean // time range limit in seconds timeRangeLimit?: number } // array of 24 numbers, start from 0 const hoursRange = [...Array(24).keys()] const minutesRange = [...Array(60).keys()] // These presets are aligned with Grafana const DEFAULT_RECENT_SECONDS = [ 5 * 60, 15 * 60, 30 * 60, 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60, 2 * 24 * 60 * 60, 7 * 24 * 60 * 60, 30 * 24 * 60 * 60, 90 * 24 * 60 * 60 ] export const LimitTimeRange: React.FC = ({ value, recent_seconds = DEFAULT_RECENT_SECONDS, customAbsoluteRangePicker, onChange, onZoomOutClick, disabled, timeRangeLimit }) => { // get the selectable time range value from rencent_seconds const selectableHours = useMemo(() => { return recent_seconds![recent_seconds!.length - 1] / 3600 }, [recent_seconds]) // Use timeRangeLimit if provided, otherwise use selectableHours const timeRangeHours = useMemo(() => { if (timeRangeLimit !== undefined) { return timeRangeLimit / 3600 // Convert seconds to hours } return selectableHours }, [timeRangeLimit, selectableHours]) const disabledDate = (current) => { const today = dayjs() const todayStartWithHour = today.startOf('hour') const todayStartWithDay = today.startOf('day') const todayEndWithDay = today.endOf('day') const curDate = dayjs(current) // Can not select days before the earliest allowed date const tooEarly = todayStartWithHour.subtract(timeRangeHours, 'hour') > curDate.startOf('hour') && todayStartWithDay.subtract(timeRangeHours / 24, 'day') > curDate.startOf('day') // Can not select days after today const tooLate = todayStartWithHour < curDate.startOf('hour') && todayEndWithDay < curDate.endOf('day') return current && (tooEarly || tooLate) } // control avaliable time on Minute level const disabledTime = (current, type) => { // current hour const today = dayjs() const hour = today.hour() const minute = today.minute() // Only apply time restrictions for today // For historical dates, allow all times if (current && current.isSame(today, 'day')) { const curHour = dayjs(current).hour() return { disabledHours: () => hoursRange.slice(hour + 1), disabledMinutes: () => // if current hour, disable minutes after current minute curHour === hour ? minutesRange.slice(minute + 1) : [] } } // For the earliest selectable date, restrict times before current time const earliestDate = today.subtract(timeRangeHours / 24, 'day') if (current && current.isSame(earliestDate, 'day')) { const curHour = dayjs(current).hour() return { disabledHours: () => hoursRange.slice(0, hour), disabledMinutes: () => // if current hour, disable minutes before current minute curHour === hour ? minutesRange.slice(0, minute) : [] } } // For all other historical dates, allow all times return { disabledHours: () => [] } } return ( <> {customAbsoluteRangePicker ? ( ) : ( )} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/MultiSelect/DropOverlay.tsx ================================================ import React, { useState, useMemo } from 'react' import { IColumn, ISelection } from 'office-ui-fabric-react/lib/DetailsList' import { useTranslation } from 'react-i18next' import TableWithFilter, { ITableWithFilterRefProps } from '../InstanceSelect/TableWithFilter' import { IItem } from '.' const containerProps: React.HTMLAttributes = { style: { fontSize: '0.9rem' } } export interface IDropOverlayProps { selection: ISelection columns: IColumn[] items: T[] filterFn?: (keyword: string, item: T) => boolean filterTableRef?: React.Ref } function DropOverlay({ selection, columns, items, filterFn, filterTableRef }: IDropOverlayProps) { const { t } = useTranslation() const [keyword, setKeyword] = useState('') const filteredItems = useMemo(() => { if (keyword.length === 0) { return items } const kw = keyword.toLowerCase() const filter = filterFn == null ? (it: T) => it.key.toLowerCase().indexOf(kw) > -1 || (it.label ?? '').toLowerCase().indexOf(kw) > -1 : (it: T) => filterFn(keyword, it) return items.filter(filter) }, [items, keyword, filterFn]) return ( ) } const typedMemo: (c: T) => T = React.memo export default typedMemo(DropOverlay) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/MultiSelect/Plain.tsx ================================================ import MultiSelect, { IMultiSelectProps, IItem } from '.' import { useMemo } from 'react' import React from 'react' export interface IPlainMultiSelectProps extends Omit, 'items' | 'filterFn'> { items?: string[] } export default function PlainMultiSelect({ items, ...restProps }: IPlainMultiSelectProps) { const objectItems = useMemo( () => items?.map((v) => ({ key: v })) ?? [], [items] ) return } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/MultiSelect/index.stories.tsx ================================================ import React, { useState } from 'react' import { Pre } from '@lib/components' import MultiSelect from '.' export default { title: 'Select/Multi Select' } function genItems() { const items: any[] = [] const items2: any[] = [] for (let i = 0; i < 100; i++) { items.push({ key: String(i), label: `Item ${i}` }) items2.push({ key: String(i), label: `Long Long Long Long Long Long Item ${i}` }) } return [items, items2] } const [items, items2] = genItems() const MultiSelectRegion = () => { const [value, setValue] = useState([]) return ( <>
    Value = {JSON.stringify(value)}
    ) } export const uncontrolled = () => ( ) export const controlled = () => ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/MultiSelect/index.tsx ================================================ import { IBaseSelectProps, BaseSelect, TextWrap } from '..' import { ITableWithFilterRefProps } from '../InstanceSelect/TableWithFilter' import React, { useMemo, useRef, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useMemoizedFn, useControllableValue } from 'ahooks' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import SelectionWithFilter from '@lib/utils/selectionWithFilter' import { useShallowCompareEffect } from 'react-use' import { addTranslationResource } from '@lib/utils/i18n' import { Tooltip } from 'antd' import DropOverlay from './DropOverlay' import PlainMultiSelect from './Plain' import { useChange } from '@lib/utils/useChange' const translations = { en: { filterPlaceholder: 'Filter', selected: '{{n}} selected', columnTitle: 'Items' }, zh: { filterPlaceholder: '过滤', selected: '已选择 {{n}} 项', columnTitle: '选择项' } } for (const key in translations) { addTranslationResource(key, { component: { multiSelect: translations[key] } }) } export interface IItem { key: string label?: string } export interface IMultiSelectProps extends Omit, 'dropdownRender' | 'valueRender'> { items?: T[] filterFn?: (keyword: string, item: T) => boolean onChange?: (value: string[]) => void selectedValueTransKey?: string columnTitle?: string } function MultiSelect(props: IMultiSelectProps) { const [internalVal, setInternalVal] = useControllableValue(props) const setInternalValPersist = useMemoizedFn(setInternalVal) const { items, filterFn, selectedValueTransKey, columnTitle, placeholder, value, // only to exclude from restProps onChange, // only to exclude from restProps ...restProps } = props const { t } = useTranslation() const columns: IColumn[] = useMemo( () => [ { name: columnTitle ?? t('component.multiSelect.columnTitle'), key: 'name', minWidth: 180, onRender: (node: T) => { let label if ('label' in node) { label = node.label } else { label = node.key } return ( {label} ) } } ], [t, columnTitle] ) const selection = useRef( new SelectionWithFilter({ onSelectionChanged: () => { if (process.env.NODE_ENV === 'development') { console.groupCollapsed( 'MultiSelect onSelectionChanged', Math.random() ) console.trace() console.groupEnd() } const s = selection.current.getAllSelection() as T[] const keys = s.map((v) => v.key) setInternalValPersist(keys) } }) ) useShallowCompareEffect(() => { selection.current?.resetAllSelection(internalVal ?? []) }, [internalVal]) useChange(() => { selection.current?.setAllItems(items ?? []) // We may receive value first and then receive items. In this case, we need to re-assign // the selection according to value after receiving new items, so that values in newly appeared // items can be selected. selection.current?.resetAllSelection(internalVal ?? []) }, [items]) const filterTableRef = useRef(null) const renderDropdown = useCallback( () => ( columns={columns} items={items ?? []} selection={selection.current} filterFn={filterFn} filterTableRef={filterTableRef} /> ), [columns, items, filterFn] ) const handleOpened = useCallback(() => { filterTableRef.current?.focusFilterInput() }, []) const renderValue = useCallback(() => { if (placeholder && (!internalVal || internalVal.length === 0)) { return null } return t(selectedValueTransKey ?? 'component.multiSelect.selected', { n: internalVal?.length ?? 0, count: internalVal?.length ?? 0 }) }, [t, internalVal, selectedValueTransKey, placeholder]) return ( ) } MultiSelect.Plain = PlainMultiSelect export default MultiSelect ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Ngm/NgmNotStarted.tsx ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. import React, { ReactNode } from 'react' import { Button, Result, Space } from 'antd' import { useTranslation } from 'react-i18next' import { Card } from '@lib/components' import { addTranslationResource } from '@lib/utils/i18n' import { isDistro } from '@lib/utils/distro' import { useNgmState, NgmState } from '@lib/utils/store' const translations = { en: { title: 'Feature Not Enabled', subTitle: 'A required component `NgMonitoring` is not started in this cluster. This feature is not available.', help_text: 'Help', help_url: 'https://docs.pingcap.com/tidb/dev/dashboard-faq#a-required-component-ngmonitoring-is-not-started-error-is-shown' }, zh: { title: '该功能未启用', subTitle: '集群中未启动必要组件 `NgMonitoring`,本功能不可用。', help_text: '帮助', help_url: 'https://docs.pingcap.com/zh/tidb/dev/dashboard-faq#界面提示-集群中未启动必要组件-ngmonitoring' } } for (const key in translations) { addTranslationResource(key, { component: { ngmNotStarted: translations[key] } }) } function NgmNotStarted() { const { t } = useTranslation() return ( {!isDistro() && ( )} } /> ) } export function NgmNotStartedGuard({ children }: { children: ReactNode }) { const ngmState = useNgmState() if (React.isValidElement(children)) { return ngmState === NgmState.Started ? children : } return null } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Ngm/index.ts ================================================ export * from './NgmNotStarted' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/ParamsPageWrapper/index.tsx ================================================ import React, { ReactNode } from 'react' import { useLocation } from 'react-router-dom' export default function ParamsPageWrapper({ children }: { children: ReactNode }) { const { search } = useLocation() if (React.isValidElement(children)) { return React.cloneElement(children, { key: search }) } return null } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/PlanText/index.tsx ================================================ import React, { useMemo } from 'react' import { CopyLink, TxtDownloadLink, Pre } from '@lib/components' type BinaryPlanTextProps = { data: string downloadFileName: string } // mysql> select tidb_decode_binary_plan("AgQgAQ=="); // +-------------------------------------+ // | tidb_decode_binary_plan("AgQgAQ==") | // +-------------------------------------+ // | (plan discarded because too long) | // +-------------------------------------+ // 1 row in set (0.00 sec) const DISCARDED_TOO_LONG = 'plan discarded because too long' const MAX_SHOW_LEN = 500 * 1024 // 500KB export const PlanText: React.FC = ({ data, downloadFileName }) => { const discardedDueToTooLong = useMemo(() => { return data .slice(0, DISCARDED_TOO_LONG.length + 10) .includes(DISCARDED_TOO_LONG) }, [data]) const truncatedStr = useMemo(() => { let str = data if (str.length > MAX_SHOW_LEN) { str = str.slice(0, MAX_SHOW_LEN) + '\n...(too long to show, copy or download to analyze)' } // binary_plan_text field starts with '\n' which will show an extra empty line // plan field starts with `\t` if (str.startsWith('\n')) { // remove the first empty line str = str.slice(1) } return str }, [data]) if (discardedDueToTooLong) { return
    {data}
    } return ( <>
            {truncatedStr}
          
    ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Pre/index.module.less ================================================ @font-face { font-family: 'JetBrains Mono NL'; src: local('JetBrains Mono NL'), data-uri('./JetBrainsMonoNL-Regular.woff') format('woff'), local('JetBrains Mono'); font-weight: normal; font-style: normal; } .font-mixin() { -webkit-font-smoothing: antialiased; font-family: 'JetBrains Mono NL', Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace; font-feature-settings: 'liga' 0; font-variant-ligatures: none; } .pre { white-space: pre-wrap; word-wrap: break-word; margin: 0; .font-mixin(); pre, code { .font-mixin(); } } .preNoWrap { white-space: pre; word-wrap: normal; overflow-x: auto; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Pre/index.tsx ================================================ import React from 'react' import cx from 'classnames' import styles from './index.module.less' export interface IPreProps extends React.HTMLAttributes { noWrap?: boolean } export default function Pre({ noWrap, className, children, ...rest }: IPreProps) { return (
          {children}
        
    ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Root/index.tsx ================================================ import React from 'react' import { ArrowUpOutlined, ArrowDownOutlined, DownOutlined, RightOutlined } from '@ant-design/icons' import { createTheme, registerIcons } from 'office-ui-fabric-react/lib/Styling' import { Customizations } from 'office-ui-fabric-react/lib/Utilities' import { ConfigProvider } from 'antd' import i18next from 'i18next' import enUS from 'antd/es/locale/en_US' import zhCN from 'antd/es/locale/zh_CN' registerIcons({ icons: { SortUp: , SortDown: , chevronrightmed: , tag: } }) const theme = createTheme({ defaultFontStyle: { fontFamily: 'inherit', fontSize: '1em' } }) Customizations.applySettings({ theme }) export default function Root({ children }) { return ( {children} ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/TextWithInfo/index.tsx ================================================ import React from 'react' import { Tooltip, Typography } from 'antd' import type { TooltipPlacement } from 'antd/es/tooltip' import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' export interface ITextWithInfoProps { tooltip?: React.ReactNode placement?: TooltipPlacement children: React.ReactNode type?: 'warning' | 'danger' } function TextWithInfo({ tooltip, placement, children, type }: ITextWithInfoProps) { let textWithIcon if (tooltip) { const Icon = type ? WarningOutlined : InfoCircleOutlined textWithIcon = ( {children} ) } else { textWithIcon = children } let textWithColor if (type) { textWithColor = ( {textWithIcon} ) } else { textWithColor = textWithIcon } if (!tooltip) { return textWithColor } return ( {textWithColor} ) } export interface ITransKeyTextWithInfo { transKey: string placement?: TooltipPlacement type?: 'warning' | 'danger' } function TransKey({ transKey, placement, type }: ITransKeyTextWithInfo) { const { t } = useTranslation() const text = t(transKey) const tooltip = t(`${transKey}_tooltip`, { defaultValue: '', fallbackLng: '_' }) return ( {text} ) } TextWithInfo.TransKey = TransKey export default TextWithInfo ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/TextWrap/index.module.less ================================================ .singleLine { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; > span { display: inline; } pre { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } } .multiLine { overflow-wrap: break-word; white-space: normal; text-overflow: inherit; overflow: auto; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/TextWrap/index.tsx ================================================ import React from 'react' import cx from 'classnames' import styles from './index.module.less' export interface ITextWrapProps extends React.HTMLAttributes { // When multiline enabled, text will be wrapped. When multiline disabled, // overflow texts will be truncated with ellipsis. multiline?: boolean } export default function TextWrap({ multiline, className, children, ...rest }: ITextWrapProps) { const c = cx(className, { [styles.multiLine]: multiline, [styles.singleLine]: !multiline }) return (
    {children}
    ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/TimePicker/index.tsx ================================================ import { Dayjs } from 'dayjs' import * as React from 'react' import DatePicker from '../DatePicker' import { PickerTimeProps } from 'antd/es/date-picker/generatePicker' export interface TimePickerProps extends Omit, 'picker'> {} const TimePicker = React.forwardRef((props, ref) => { return }) TimePicker.displayName = 'TimePicker' export default TimePicker ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/TimeRangeSelector/WithZoomOut.tsx ================================================ import { ZoomOutOutlined } from '@ant-design/icons' import { useMemoizedFn } from 'ahooks' import { Button } from 'antd' import React from 'react' import TimeRangeSelector, { fromTimeRangeValue, toTimeRangeValue, ITimeRangeSelectorProps } from '.' export interface ITimeRangeSelectorWithZoomOutProps extends ITimeRangeSelectorProps { zoomOutRate?: number minRange?: number onZoomOutClick?: (start: number, end: number) => void } export function WithZoomOut({ zoomOutRate = 0.5, minRange = 5 * 60, onZoomOutClick, ...rest }: ITimeRangeSelectorWithZoomOutProps) { const handleZoomOut = useMemoizedFn(() => { if (!rest.onChange) { return } const [start, end] = toTimeRangeValue(rest.value) let expand = (end - start) * zoomOutRate if (expand < minRange) { expand = minRange } let computedStart = start - expand let computedEnd = end + expand const newRange = fromTimeRangeValue([computedStart, computedEnd]) onZoomOutClick!(computedStart, computedEnd) rest.onChange?.(newRange) }) return ( {rangeError && (
    {rangeError}
    )}
    ) return ( ) } const c = Object.assign(React.memo(TimeRangeSelector), { WithZoomOut: React.memo(WithZoomOut) }) export default c ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Toolbar/index.module.less ================================================ .toolbar_container { display: flex; :global(.ant-space-item) { margin-bottom: 8px; } .left_space { flex: 1; display: flex; flex-wrap: wrap; } .right_space { align-self: flex-start; margin-top: 6px; } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/Toolbar/index.tsx ================================================ import React from 'react' import cx from 'classnames' import { Space } from 'antd' import styles from './index.module.less' export default function Toolbar(props: React.HTMLAttributes) { const { className, children, ...rest } = props const c = cx(className, styles.toolbar_container) // https://stackoverflow.com/questions/27366077 React.Children.forEach(children, (child) => { if (!React.isValidElement(child) || child.type !== Space) { console.error('Toolbar children only can be Space component') } }) return (
    {React.Children.map(children, (child, idx) => { // https://stackoverflow.com/questions/42261783 if (React.isValidElement(child) && child.type === Space) { const extraClassName = idx === 0 ? styles.left_space : styles.right_space return React.cloneElement(child, { className: cx(child.props.className, extraClassName), size: child.props.size || 'middle' }) } })}
    ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/TxtDownloadLink/index.module.less ================================================ @import 'antd/es/style/themes/default.less'; .successTxt { color: @success-color; } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/TxtDownloadLink/index.tsx ================================================ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { useTimeoutFn } from 'react-use' import { CheckOutlined, DownloadOutlined } from '@ant-design/icons' import { addTranslationResource } from '@lib/utils/i18n' import { downloadTxt } from '@lib/utils/local-download' import styles from './index.module.less' export interface ITxtDownloadLinkProps extends React.DetailedHTMLProps< React.HTMLAttributes, HTMLSpanElement > { data?: string fileName?: string } const translations = { en: { download: 'Download', success: 'Downloaded' }, zh: { download: '下载', success: '已下载' } } for (const key in translations) { addTranslationResource(key, { component: { txtDownloadLink: translations[key] } }) } function TxtDownloadLink({ data, fileName, ...otherProps }: ITxtDownloadLinkProps) { const { t } = useTranslation() const [showDownload, setShowDownloaded] = useState(false) const reset = useTimeoutFn(() => { setShowDownloaded(false) }, 1500)[2] const handleDownload = () => { downloadTxt(data ?? '', fileName ?? 'data.txt') setShowDownloaded(true) reset() } return ( {!showDownload && ( {t(`component.txtDownloadLink.download`)} )} {showDownload && ( {t('component.txtDownloadLink.success')} )} ) } export default React.memo(TxtDownloadLink) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/ValueWithTooltip/index.tsx ================================================ import React from 'react' import { Tooltip } from 'antd' import { getValueFormat, scaledUnits } from '@baurine/grafana-value-formats' interface IValueWithTooltip extends IInternalValueWithTooltip { Short: typeof ShortValueWithTooltip ScaledBytes: typeof ScaledBytesWithTooltip } interface IInternalValueWithTooltip { title: string value: any } function InternalValueWithTooltip({ title, value }: IValueWithTooltip) { return ( {value} ) } export interface IValueWithTooltipProps { value?: number scaledDecimal?: number } function ShortValueWithTooltip({ value = 0, scaledDecimal = 1 }: IValueWithTooltipProps) { return ( {getValueFormat('short')(value || 0, 0, scaledDecimal)} ) } const bytesScaler = scaledUnits(1024, ['', 'K', 'M', 'G', 'T']) function ScaledBytesWithTooltip({ value = 0, scaledDecimal = 2 }: IValueWithTooltipProps) { return ( {bytesScaler(value || 0, 0, scaledDecimal)} ) } const ValueWithTooltip = InternalValueWithTooltip as unknown as IValueWithTooltip ValueWithTooltip.Short = ShortValueWithTooltip ValueWithTooltip.ScaledBytes = ScaledBytesWithTooltip export { ValueWithTooltip } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/VisualPlan/DetailDrawer.tsx ================================================ import React, { useMemo } from 'react' import ReactJson from 'react-json-view' import { Tabs, Tooltip, Drawer, DrawerProps } from 'antd' import { InfoCircleTwoTone } from '@ant-design/icons' import { RawNodeDatum, Theme } from 'visual-plan' import { addTranslations } from '@lib/utils/i18n' import { useTranslation } from 'react-i18next' import translations from './translations' import { toFixed, getValueFormat } from '@baurine/grafana-value-formats' addTranslations(translations) interface DetailDrawerProps { data: RawNodeDatum theme?: Theme } function getTableName(node: RawNodeDatum): string { let tableName = '' if (!node?.accessObjects?.length) return '' const scanObject = node.accessObjects.find((obj) => Object.keys(obj).includes('scanObject') ) if (scanObject) { tableName = scanObject['scanObject']['table'] } return tableName } const DetailDrawer: React.FC = ({ data, theme = 'light', ...props }) => { const tableName = useMemo(() => getTableName(data), [data]) const { t } = useTranslation() return ( data && (

    Duration{' '} : {data.duration}

    Actual Rows: {data.actRows}

    Estimate Rows: {toFixed(data.estRows, 0)}

    Run at: {data.storeType}

    {tableName && (

    Table: {tableName}

    )} {data.cost && (

    Cost: {data.cost}

    )}

    Disk:{' '} {Number(data.diskBytes) ? getValueFormat('deckbytes')(Number(data.diskBytes), 2, null) : data.diskBytes}{' '}

    Memory:{' '} {Number(data.memoryBytes) ? getValueFormat('deckbytes')( Number(data.memoryBytes), 2, null ) : data.memoryBytes}{' '}

    Task Type: {data.taskType}

    {data.labels.length > 0 && (

    Labels:{' '} {data.labels.map((label, idx) => ( <> {idx > 0 ? ',' : ''} {label} ))}

    )} {data.operatorInfo && (

    Operator Info: {data.operatorInfo}

    )} {Object.keys(data.rootBasicExecInfo).length > 0 && (
    Root Basic Exec Info:{' '}
    )} {data.rootGroupExecInfo.length > 0 && (
    Root Group Exec Info:{' '}
    )} {Object.keys(data.copExecInfo).length > 0 && (
    Coprocessor Exec Info:{' '}
    )} {data.accessObjects.length > 0 && (
    Access Object: <> {data.accessObjects.map((obj, idx) => ( ))}
    )}
    {data.diagnosis.length > 0 && (
      {data.diagnosis.map((d: string, idx) => (
    1. {t(`binary_plan.diagnosis.${d}`)}
    2. ))}
    )}
    ) ) } export default DetailDrawer ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/VisualPlan/VisualPlan.tsx ================================================ import React, { useState } from 'react' import { VisualPlanThumbnail, VisualPlan, RawNodeDatum } from 'visual-plan' import DetailDrawer from './DetailDrawer' export const VisualPlanThumbnailView = (props) => { const binaryPlan = props.data const minimap = false const cte = { gap: 10 } return ( ) } export const VisualPlanView = (props) => { const binaryPlan = props.data const minimap = { scale: 0.15 } const [showDetailDrawer, setShowDetailDrawer] = useState(false) const [detailData, setDetailData] = useState(null) return ( <> { setDetailData(n) setShowDetailDrawer(true) }} minimap={minimap} cte={{ gap: 10 }} /> setShowDetailDrawer(false)} /> ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/VisualPlan/index.ts ================================================ export * from './VisualPlan' export * from './DetailDrawer' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/VisualPlan/translations/en.yaml ================================================ binary_plan: tooltip: duration: 'The time taken by the parent operator includes the time taken by all children.' tabs: general: General hardware_usage: Hardware Usage advanced_info: Advanced Information diagnosis: Diagnosis diagnosis: high_est_error: 'The estimation error is high. Consider checking the health state of the statistics.' disk_spill: "Disk spill is triggered for this operator because the memory quota is exceeded. The execution might be slow. Consider increasing the memory quota if there's enough memory." pseudo_est: 'This operator used pseudo statistics and the estimation might be inaccurate. It might be caused by unavailable or outdated statistics. Consider collecting statistics or setting variable tidb_enable_pseudo_for_outdated_stats to OFF.' good_filter_on_table_fullscan: 'This Selection filters a high proportion of data. Using an index on this column might achieve better performance. Consider adding an index on this column if there is not one.' bad_index_for_index_lookup: 'This IndexLookup read a lot of data from the index side. It might be slow and cause heavy pressure on TiKV. Consider using the optimizer hints to guide the optimizer to choose a better index or not to use index.' index_join_build_side_too_large: 'This index join has a large build side. It might be slow and cause heavy pressure on TiKV. Consider using the optimizer hints to guide the optimizer to choose hash join.' tikv_huge_table_scan: "The TiKV read a lot of data. Consider using TiFlash to get better performance if it's necessary to read so much data." ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/VisualPlan/translations/index.ts ================================================ import zh from './zh.yaml' import en from './en.yaml' export default { zh, en } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/VisualPlan/translations/zh.yaml ================================================ binary_plan: tooltip: duration: '父算子的耗时包含所有子算子的耗时。' tabs: general: 概览 hardware_usage: 硬件使用 advanced_info: 高级信息 diagnosis: 诊断 diagnosis: high_est_error: '估算误差较大。可以考虑检查统计信息的健康度。' disk_spill: 执行过程中由于到达内存限制,产生了落盘,执行可能会变慢。内存足够的情况下,可以考虑提高内存使用阈值。" pseudo_est: '该算子使用了 pseudo 统计信息,行数估算可能不准确。原因可能是统计信息不可用或者统计信息过期。可以考虑收集统计信息或者将系统变量 tidb_enable_pseudo_for_outdated_stats 设为 OFF。' good_filter_on_table_fullscan: '该过滤条件的过滤性较好。使用该列上的索引可能能够达到更好的性能。如果没有该列上的索引,可以考虑创建该列上的索引。' bad_index_for_index_lookup: '该 IndexLookup 算子从索引中读取了大量数据。这可能导致执行较慢以及对 TiKV 造成较大压力。可以考虑使用 Optimizer Hints 指导 TiDB 选择更好的索引或者不使用索引。' index_join_build_side_too_large: '该 IndexJoin 算子从 build 端读取了大量数据。这可能导致执行较慢以及对 TiKV 造成较大压力。可以考虑使用 Optimizer Hints 指导 TiDB 选择 Hash Join。' tikv_huge_table_scan: '该算子在 TiKV 读取了大量数据。如果确实需要读取大量数据,可以考虑使用 TiFlash 达到更好的性能。' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/components/index.ts ================================================ export * from './Root' export { default as Root } from './Root' export * from './Head' export { default as Head } from './Head' export * from './Card' export { default as Card } from './Card' export * from './CardTabs' export { default as CardTabs } from './CardTabs' export * from './CardTable' export { default as CardTable } from './CardTable' export * from './Bar' export { default as Bar } from './Bar' export * from './HighlightSQL' export { default as HighlightSQL } from './HighlightSQL' export * from './TextWrap' export { default as TextWrap } from './TextWrap' export * from './Pre' export { default as Pre } from './Pre' export * from './Descriptions' export { default as Descriptions } from './Descriptions' export * from './TextWithInfo' export { default as TextWithInfo } from './TextWithInfo' export * from './DateTime' export { default as DateTime } from './DateTime' export * from './Expand' export { default as Expand } from './Expand' export * from './CopyLink' export { default as CopyLink } from './CopyLink' export { default as TxtDownloadLink } from './TxtDownloadLink' export * from './ColumnsSelector' export { default as ColumnsSelector } from './ColumnsSelector' export * from './Toolbar' export { default as Toolbar } from './Toolbar' export * from './TimeRangeSelector' export { default as TimeRangeSelector } from './TimeRangeSelector' export * from './AnimatedSkeleton' export { default as AnimatedSkeleton } from './AnimatedSkeleton' export * from './InstanceStatusBadge' export { default as InstanceStatusBadge } from './InstanceStatusBadge' export * from './BaseSelect' export { default as BaseSelect } from './BaseSelect' export * from './InstanceSelect' export { default as InstanceSelect } from './InstanceSelect' export * from './MultiSelect' export { default as MultiSelect } from './MultiSelect' export * from './ValueWithTooltip' export * from './DatePicker' export { default as DatePicker } from './DatePicker' export * from './ErrorBar' export { default as ErrorBar } from './ErrorBar' export * from './AppearAnimate' export { default as AppearAnimate } from './AppearAnimate' export * from './Blink' export { default as Blink } from './Blink' export * from './DrawerFooter' export { default as DrawerFooter } from './DrawerFooter' export * from './VisualPlan' export * from './BinaryPlanTable' export * from './PlanText' export { default as LanguageDropdown } from './LanguageDropdown' export { default as ParamsPageWrapper } from './ParamsPageWrapper' export * from './AutoRefreshButton' export * from './Ngm' export * from './LimitTimeRange' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/hooks/useLocationChange.ts ================================================ import { useEffect } from 'react' import { useLocation } from 'react-router-dom' export function useLocationChange() { // https://thewebdev.info/2022/03/07/how-to-detect-route-change-with-react-router/ const location = useLocation() useEffect(() => { const event = new CustomEvent('dashboard:route-change', { detail: { location } }) window.dispatchEvent(event) }, [location]) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/hooks/useQueryParams.ts ================================================ import { useEffect, useState } from 'react' import { NavigateOptions, useLocation, useNavigate } from 'react-router-dom' export interface IQueryParams { [key: string]: any } export function useQueryParams( defParams: T, override?: T, options?: NavigateOptions ) { const location = useLocation() const navigate = useNavigate() const [queryParams, _setQueryParams] = useState(() => { let newParams = { ...defParams, ...override } const searchParams = new URLSearchParams(location.search) for (const [key, value] of searchParams.entries()) { const defVal = defParams[key] if (defVal !== undefined) { if (typeof defVal === 'number') { newParams[key] = Number(value) } else if (Array.isArray(defVal)) { if (value === '') { newParams[key] = [] } else { newParams[key] = value.split(',') } } else { newParams[key] = value } } else { newParams[key] = value } } return newParams }) useEffect(() => { // redirect if params are not default when mount if (override) { setQueryParams(queryParams) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) function setQueryParams(p: T) { const params = { ...queryParams, ...p } _setQueryParams(params) const prevSearchStr = location.search const searchParams = new URLSearchParams() Object.keys(params).forEach((k) => { searchParams.set(k, params[k] + '') }) const currentSearchStr = `?${searchParams.toString()}` if (prevSearchStr === currentSearchStr) { return } navigate(`${location.pathname}?${searchParams.toString()}`, options) } return { queryParams, setQueryParams } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/hooks/useURLTimeRange.ts ================================================ import { TimeRange } from '@lib/components' import { useMemo } from 'react' import { useQueryParams } from './useQueryParams' export const useURLTimeRange = () => { const { queryParams, setQueryParams } = useQueryParams<{ from: number | string to: number | string }>({ from: 30 * 60, to: 'now' }) const { from, to } = queryParams const isRecent = to === 'now' const timeRange: TimeRange = useMemo( () => ({ type: isRecent ? 'recent' : 'absolute', value: isRecent ? parseInt(`${from}`) : [parseInt(`${from}`), parseInt(`${to}`)] } as TimeRange), [from, to, isRecent] ) const setTimeRange = (tr: TimeRange) => { const isRecent = tr.type === 'recent' setQueryParams({ from: isRecent ? tr.value : tr.value[0], to: isRecent ? 'now' : tr.value[1] }) } return { timeRange, setTimeRange } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/index.ts ================================================ export * from './types' export * from './utils' export * from './components' export * from './apps' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/react-app-env.d.ts ================================================ declare module '*.module.css' { const classes: { readonly [key: string]: string } export default classes } declare module '*.module.less' { const classes: { readonly [key: string]: string } export default classes } declare module '*.yaml' { const classes: { readonly [key: string]: string } export default classes } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/types/index.ts ================================================ import { AxiosRequestConfig } from 'axios' export interface ReqConfig extends AxiosRequestConfig { handleError?: 'default' | 'custom' } export interface IContextConfig { apiPathBase: string } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/types/reqConfig.ts ================================================ import { AxiosRequestConfig } from 'axios' export interface ReqConfig extends AxiosRequestConfig { handleError?: 'default' | 'custom' } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/charts.ts ================================================ import { PartialTheme, SettingsProps, TickFormatter, timeFormatter, TooltipSettings, TooltipStickTo, TooltipType, TooltipValue } from '@elastic/charts' import { TimeRangeValue } from '@lib/components' import dayjs from 'dayjs' import React, { useRef } from 'react' import { DEFAULT_MIN_INTERVAL_SEC } from './prometheus' import '@elastic/charts/dist/theme_only_light.css' import tz from './timezone' /** * A human readable tick label formatter for time series data. It scales according to the data domain. */ export function timeTickFormatter(range: TimeRangeValue): TickFormatter { // const minDate = moment(range[0] * 1000) // const maxDate = moment(range[1] * 1000) // const diff = maxDate.diff(minDate, 'minutes') const minDate = dayjs(range[0] * 1000) const maxDate = dayjs(range[1] * 1000) const diff = maxDate.diff(minDate, 'minutes') const format = niceTimeFormatByDay(diff) function formatter(v): string { return timeFormatter(format)(v, { timeZone: tz.getTimeZoneStr() }) } return formatter } function niceTimeFormatByDay(days: number) { if (days > 5 * 60 * 24) return 'MM-DD' if (days > 1 * 60 * 24) return 'MM-DD HH:mm' if (days > 5) return 'HH:mm' return 'HH:mm:ss' } export function timeTooltipFormatter({ value }: TooltipValue): string { return timeFormatter('YYYY-MM-DD HH:mm:ss (UTCZ)')(value, { timeZone: tz.getTimeZoneStr() }) } export const DEFAULT_TOOLTIP_SETTINGS: TooltipSettings = { type: TooltipType.Crosshairs, headerFormatter: timeTooltipFormatter, stickTo: TooltipStickTo.MousePosition } export const DEFAULT_THEME: PartialTheme = { axes: { tickLine: { visible: false }, tickLabel: { padding: { inner: 10 } }, gridLine: { horizontal: { visible: true, dash: [3, 3] }, vertical: { visible: true, dash: [3, 3] } } }, crosshair: { crossLine: { dash: [] }, line: { dash: [] } } } export const DEFAULT_CHART_SETTINGS: SettingsProps = { showLegend: true, showLegendExtra: true, tooltip: DEFAULT_TOOLTIP_SETTINGS, theme: DEFAULT_THEME } export type ChartHandle = { calcIntervalSec: (range: TimeRangeValue, minIntervalSec?: number) => number } /** * Align the time range according to the minimal interval and minimal range size. */ export function alignRange( range: TimeRangeValue, minIntervalSec = DEFAULT_MIN_INTERVAL_SEC, minRangeSec = 60 ): TimeRangeValue { let [min, max] = range if (max - min < minRangeSec) { min = max - minRangeSec } min = Math.floor(min / minIntervalSec) * minIntervalSec max = Math.ceil(max / minIntervalSec) * minIntervalSec return [min, max] } export function useChartHandle( containerRef: React.RefObject, legendWidth: number = 0, minBinWidth: number = 5 ): [ChartHandle] { const chartRef = useRef({ calcIntervalSec: (range, minIntervalSec = DEFAULT_MIN_INTERVAL_SEC) => { const maxDataPoints = ((containerRef.current?.offsetWidth ?? 0) - legendWidth) / minBinWidth if (maxDataPoints <= 0) { return minIntervalSec } const interval = (range[1] - range[0]) / maxDataPoints const roundedInterval = Math.floor(interval / minIntervalSec) * minIntervalSec return Math.max(minIntervalSec, roundedInterval) } }) return [chartRef.current] } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/distro.ts ================================================ import i18next from 'i18next' interface IDistro { pd: string tidb: string tikv: string tiflash: string ticdc: string is_distro: boolean } const DEF_DISTRO: IDistro = { pd: 'PD', tidb: 'TiDB', tikv: 'TiKV', tiflash: 'TiFlash', ticdc: 'TiCDC', is_distro: false } let _distro = DEF_DISTRO export function distro() { return _distro } export function isDistro() { return Boolean(_distro.is_distro) } // newDistro example: { tidb:'TieDB', tikv: 'TieKV' } export function updateDistro(newDistro: Partial) { _distro = { ..._distro, ...newDistro } // update i18n resource i18next.addResourceBundle( 'en', 'translation', { distro: _distro }, true, true ) // update i18n interpolation defaultVariables by hack way // https://stackoverflow.com/a/71031838/2998877 const interpolator = i18next.services.interpolator as any interpolator.options.interpolation.defaultVariables = { distro: _distro } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/format.ts ================================================ import { getValueFormat } from '@baurine/grafana-value-formats' export function formatNumByUnit( value: number, unit: string, precision: number = 1 ) { if (isNaN(value)) { return '' } const formatFn = getValueFormat(unit) if (!formatFn) { return value + '' } if (unit === 'short') { return formatFn(value, 0, precision) } return formatFn(value, precision) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/i18n.ts ================================================ import 'dayjs/locale/zh' import dayjs from 'dayjs' import i18next from 'i18next' import LanguageDetector from 'i18next-browser-languagedetector' import { initReactI18next } from 'react-i18next' import { distro } from './distro' i18next.on('languageChanged', function (lng) { dayjs.locale(lng.toLowerCase()) }) export function addTranslations(translations) { Object.keys(translations).forEach((key) => { addTranslationResource(key, translations[key]) }) } export function addTranslationResource(lang, translations) { i18next.addResourceBundle(lang, 'translation', translations, true, false) } export const ALL_LANGUAGES = { zh: '简体中文', en: 'English' } i18next .use(LanguageDetector) .use(initReactI18next) .init({ resources: { en: { translation: { distro: distro() } } }, fallbackLng: 'en', // fallbackLng won't change the detected language supportedLngs: ['zh', 'en'], // supportedLngs will change the detected lanuage interpolation: { escapeValue: false, defaultVariables: { distro: distro() } } }) export default { addTranslations, addTranslationResource, ALL_LANGUAGES } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/index.ts ================================================ export { default as routing } from './routing' export { default as i18n } from './i18n' export { default as telemetry } from './telemetry' export { default as tz } from './timezone' export * from './distro' export * from './store' export * from './useVersionedLocalStorageState' export * from './prometheus' ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/instanceTable.ts ================================================ import _ from 'lodash' import i18next from 'i18next' import { IGroup } from 'office-ui-fabric-react/lib/DetailsList' import { TopologyPDInfo, TopologyTiDBInfo, TopologyStoreInfo, TopologyTiCDCInfo, TopologyTiProxyInfo, TopologyTSOInfo, TopologySchedulingInfo } from '@lib/client' export const InstanceKinds = [ 'pd', 'tidb', 'tikv', 'tiflash', 'ticdc', 'tiproxy', 'tso', 'scheduling' ] as const export type InstanceKind = typeof InstanceKinds[number] export const InstanceStatus = { Unreachable: 0, Up: 1, Tombstone: 2, Offline: 3, Down: 4 } export function instanceKindName(kind: InstanceKind) { return i18next.t(`distro.${kind}`) } export interface IInstanceTableItem extends TopologyPDInfo, TopologyTiDBInfo, TopologyStoreInfo, TopologyTiCDCInfo, TopologyTiProxyInfo, TopologyTSOInfo, TopologySchedulingInfo { key: string instanceKind: InstanceKind } export interface IBuildInstanceTableProps { dataPD?: TopologyPDInfo[] dataTiDB?: TopologyTiDBInfo[] dataTiKV?: TopologyStoreInfo[] dataTiFlash?: TopologyStoreInfo[] dataTiCDC?: TopologyTiCDCInfo[] dataTiProxy?: TopologyTiProxyInfo[] dataTSO?: TopologyTSOInfo[] dataScheduling?: TopologySchedulingInfo[] includeTiFlash?: boolean } export function buildInstanceTable({ dataPD, dataTiDB, dataTiKV, dataTiFlash, dataTiCDC, dataTiProxy, dataTSO, dataScheduling, includeTiFlash }: IBuildInstanceTableProps): [IInstanceTableItem[], IGroup[]] { const tableData: IInstanceTableItem[] = [] const groupData: IGroup[] = [] let startIndex = 0 const kinds: { [key in InstanceKind]?: | TopologyPDInfo[] | TopologyTiDBInfo[] | TopologyStoreInfo[] | TopologyTiCDCInfo[] | TopologyTiProxyInfo[] | TopologyTSOInfo[] | TopologySchedulingInfo[] | undefined } = {} kinds.pd = dataPD kinds.tidb = dataTiDB kinds.tikv = dataTiKV kinds.ticdc = dataTiCDC kinds.tiproxy = dataTiProxy kinds.tso = dataTSO kinds.scheduling = dataScheduling if (includeTiFlash) { kinds.tiflash = dataTiFlash } for (const ik of InstanceKinds) { const instances = kinds[ik] if (!instances || instances.length === 0) { continue } groupData.push({ key: ik, name: instanceKindName(ik), startIndex: startIndex, count: instances.length, level: 0 }) startIndex += instances.length instances.forEach((instance) => { const key = `${instance.ip}:${instance.port}` tableData.push({ key: key, instanceKind: ik, ...instance }) }) } return [tableData, groupData] } export function filterInstanceTable( items: IInstanceTableItem[], filterKeyword: string ): [IInstanceTableItem[], IGroup[]] { const tableData: IInstanceTableItem[] = [] const groupData: IGroup[] = [] let startIndex = 0 const kw = filterKeyword.toLowerCase() const filteredItems = items.filter((i) => { if (filterKeyword.length === 0) { return true } return ( i.key.toLowerCase().indexOf(kw) > -1 || i.instanceKind.indexOf(kw) > -1 ) }) const itemsByIk = _.groupBy(filteredItems, 'instanceKind') as { [key in InstanceKind]: IInstanceTableItem[] } for (const ik of InstanceKinds) { const instances = itemsByIk[ik] if (!instances || instances.length === 0) { continue } groupData.push({ key: ik, name: instanceKindName(ik), startIndex: startIndex, count: instances.length, level: 0 }) startIndex += instances.length instances.forEach((instance) => { tableData.push(instance) }) } return [tableData, groupData] } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/local-download.ts ================================================ export function downloadTxt(data: string, fileName: string) { const fileUrl = URL.createObjectURL( new Blob([data], { type: 'text/plain;charset=utf-8;' }) ) const a = document.createElement('a') document.body.appendChild(a) a.href = fileUrl a.download = fileName a.click() setTimeout(() => { document.body.removeChild(a) }, 0) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/openLink.ts ================================================ import { NavigateFunction } from 'react-router-dom' import React from 'react' // the url param starts with '/', for example: '/statement/detail' export default function openLink( url: string, ev: React.MouseEvent, navigate: NavigateFunction ) { const { origin, pathname, search } = window.location const fullUrl = `${origin}${pathname}${search}#${url}` if (ev.metaKey || ev.altKey || ev.ctrlKey) { // open in a new tab window.open(fullUrl, '_blank') } else if (ev.shiftKey) { // open in a new window window.open(fullUrl) } else { navigate(url, { state: { historyBack: true } }) } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/prometheus/data.ts ================================================ // Copyright Grafana. Licensed under Apache-2.0. // Extracted from: // https://github.com/grafana/grafana/blob/c986aaa0a8e7fb167b9d10304129f6aea85ad45c/public/app/plugins/datasource/prometheus/result_transformer.ts import { DEFAULT_MIN_INTERVAL_SEC } from '.' import { DataPoint, isMatrixData, MatrixOrVectorResult, QueryOptions } from './types' const POSITIVE_INFINITY_SAMPLE_VALUE = '+Inf' const NEGATIVE_INFINITY_SAMPLE_VALUE = '-Inf' function parseSampleValue(value: string): number { switch (value) { case POSITIVE_INFINITY_SAMPLE_VALUE: return Number.POSITIVE_INFINITY case NEGATIVE_INFINITY_SAMPLE_VALUE: return Number.NEGATIVE_INFINITY default: return parseFloat(value) } } export function processRawData( data: MatrixOrVectorResult, options: QueryOptions ): DataPoint[] | null { if (isMatrixData(data)) { const stepMs = options.step ? options.step * 1000 : NaN let baseTimestamp = options.start * 1000 const dps: DataPoint[] = [] for (const value of data.values) { let dpValue: number | null = parseSampleValue(value[1]) if (isNaN(dpValue)) { dpValue = null } const timestamp = value[0] * 1000 for (let t = baseTimestamp; t < timestamp; t += stepMs) { dps.push([t, null]) } baseTimestamp = timestamp + stepMs dps.push([timestamp, dpValue]) } const endTimestamp = options.end * 1000 for (let t = baseTimestamp; t <= endTimestamp; t += stepMs) { dps.push([t, null]) } return dps } return null } export function resolveQueryTemplate( template: string, options: QueryOptions ): string { return template.replaceAll( '$__rate_interval', `${Math.max(options.step, 4 * DEFAULT_MIN_INTERVAL_SEC)}s` ) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/prometheus/index.ts ================================================ export * from './data' export * from './types' export const DEFAULT_MIN_INTERVAL_SEC = 30 ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/prometheus/types.ts ================================================ // Copyright Grafana. Licensed under Apache-2.0. // Extracted from: // https://github.com/grafana/grafana/blob/c986aaa0a8e7fb167b9d10304129f6aea85ad45c/public/app/plugins/datasource/prometheus/types.ts export interface PromMetricsMetadataItem { type: string help: string unit?: string } export interface PromMetricsMetadata { [metric: string]: PromMetricsMetadataItem[] } export interface PromDataSuccessResponse { status: 'success' data: T } export interface PromDataErrorResponse { status: 'error' errorType: string error: string data: T } export type PromData = | PromMatrixData | PromVectorData | PromScalarData | PromExemplarData[] export interface Labels { [index: string]: any } export interface Exemplar { labels: Labels value: number timestamp: number } export interface PromExemplarData { seriesLabels: PromMetric exemplars: Exemplar[] } export interface PromVectorData { resultType: 'vector' result: Array<{ metric: PromMetric value: PromValue }> } export interface PromMatrixData { resultType: 'matrix' result: Array<{ metric: PromMetric values: PromValue[] }> } export interface PromScalarData { resultType: 'scalar' result: PromValue } export type PromValue = [number, any] export interface PromMetric { __name__?: string [index: string]: any } export function isMatrixData( result: MatrixOrVectorResult ): result is PromMatrixData['result'][0] { return 'values' in result } export type MatrixOrVectorResult = | PromMatrixData['result'][0] | PromVectorData['result'][0] export enum TransformNullValue { NULL = 'null', AS_ZERO = 'as_zero' } export enum ColorType { BLUE_1 = '#C0D8FF', BLUE_2 = '#8AB8FF', BLUE_3 = '#3274D9', BLUE_4 = '#1F60C4', GREEN_1 = '#C8F2C2', GREEN_2 = '#96D98D', GREEN_3 = '#56A64B', GREEN_4 = '#37872D', RED_1 = '#FFA6B0', RED_2 = '#FF7383', RED_3 = '#E02F44', RED_4 = '#C4162A', RED_5 = '#701313', PURPLE = '#8778ee', ORANGE = '#FF9830', YELLOW = '#FADE2A', PINK = '#F2495C' } // Our customized types export interface QueryOptions { step: number start: number end: number } export type DataPoint = [msTimestamp: number, value: number | null] ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/query.ts ================================================ export interface IQueryParams { [key: string]: any } export function parseQueryFn() { return (qs: string): T => { const p = new URLSearchParams(qs) const json = p.get('query') if (json == null) { return {} as T } const r = JSON.parse(json) if (!!r && r.constructor === Object) { return r as T } return {} as T } } export function buildQueryFn() { return (q: T): string => { const json = JSON.stringify(q) const p = new URLSearchParams() p.set('query', json) return p.toString() } } export function stripQueryString(url: string) { return url.split('?')[0] } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/routing.ts ================================================ export const signInRoute = '/signin' export const portalRoute = '/portal' export function isLocationMatch(s, matchPrefix = false): boolean { let hash = window.location.hash if (!hash || hash === '#') { hash = '#/' } if (matchPrefix) { return hash.indexOf(`#${s}`) === 0 } else { return hash.trim() === `#${s}` } } export function isLocationMatchPrefix(s): boolean { return isLocationMatch(s, true) } export function isSignInPage(): boolean { return isLocationMatchPrefix(signInRoute) } export function isPortalPage(): boolean { return isLocationMatchPrefix(portalRoute) } export function getPathInLocationHash(): string { const hash = window.location.hash const pos = hash.indexOf('?') if (pos === -1) { return hash } return hash.substring(0, pos) } export default { signInRoute, portalRoute, isLocationMatch, isLocationMatchPrefix, isSignInPage, isPortalPage, getPathInLocationHash } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/selectionWithFilter.ts ================================================ import { ISelection, IObjectWithKey, Selection, SelectionMode, ISelectionOptions, ISelectionOptionsWithRequiredGetKey, EventGroup, SELECTION_CHANGE } from 'office-ui-fabric-react/lib/Utilities' export default class SelectionWithFilter implements ISelection { private _inner: Selection private _allItems: T[] = [] private _allItemsMap: Map = new Map() private _allSelectedKeysSet: Set = new Set() private _itemKeysSet: Set = new Set() private _allSelectionCache: T[] | null = null private _onSelectionChangedOriginal?: () => void get count(): number { return this._inner.count } set count(v: number) { this._inner.count = v } get mode(): SelectionMode { return this._inner.mode } canSelectItem(item: T, index?: number): boolean { return this._inner.canSelectItem(item, index) } setChangeEvents(isEnabled: boolean, suppressChange?: boolean) { return this._inner.setChangeEvents(isEnabled, suppressChange) } getItems(): T[] { return this._inner.getItems() } getSelection(): T[] { return this._inner.getSelection() } getSelectedIndices(): number[] { return this._inner.getSelectedIndices() } getSelectedCount(): number { return this._inner.getSelectedCount() } isRangeSelected(fromIndex: number, count: number): boolean { return this._inner.isRangeSelected(fromIndex, count) } isAllSelected(): boolean { return this._inner.isAllSelected() } isKeySelected(key: string): boolean { return this._inner.isKeySelected(key) } isIndexSelected(index: number): boolean { return this._inner.isIndexSelected(index) } setKeySelected( key: string, isSelected: boolean, shouldAnchor: boolean ): void { this._inner.setKeySelected(key, isSelected, shouldAnchor) } setIndexSelected( index: number, isSelected: boolean, shouldAnchor: boolean ): void { this._inner.setIndexSelected(index, isSelected, shouldAnchor) } selectToKey(key: string, clearSelection?: boolean | undefined): void { this._inner.selectToKey(key, clearSelection) } selectToIndex(index: number, clearSelection?: boolean | undefined): void { this._inner.selectToIndex(index, clearSelection) } toggleAllSelected(): void { this.setAllSelected(!this._inner.isAllSelected()) } toggleKeySelected(key: string): void { this._inner.toggleKeySelected(key) } toggleIndexSelected(index: number): void { this._inner.toggleIndexSelected(index) } toggleRangeSelected(fromIndex: number, count: number): void { this._inner.toggleRangeSelected(fromIndex, count) } // Override setItems(items: T[], shouldClear?: boolean) { this._allSelectionCache = null if (shouldClear) { this._allSelectedKeysSet.clear() } // Only items in AllItems can be added const itemSubset: T[] = [] this._itemKeysSet.clear() for (const item of items) { const key = this._inner.getKey(item) if (this._allItemsMap.has(key)) { this._itemKeysSet.add(key) itemSubset.push(item) } else { if (process.env.NODE_ENV === 'development') { console.warn( 'Warning: SelectionWithFilter::setItems is called with an item not in allItems', item, key ) } } } this._inner.setChangeEvents(false) this._inner.setItems(itemSubset, shouldClear) // Re-select if newly added items are selected in allSelected for (const key of this._allSelectedKeysSet) { if (this._itemKeysSet.has(key)) { this._inner.setKeySelected(key, true, false) } } this._inner.setChangeEvents(true) } // Override setAllSelected(isAllSelected: boolean) { if (isAllSelected && this._itemKeysSet.size !== this._allItemsMap.size) { // If items is a true subset of allItems, we emulate a selectAll by selecting one by one. this._inner.setChangeEvents(false) for (const key of this._itemKeysSet) { this._inner.setKeySelected(key, true, false) } this._inner.setChangeEvents(true) } else { this._inner.setAllSelected(isAllSelected) } } constructor( ...options: T extends IObjectWithKey ? [] | [ISelectionOptions] : [ISelectionOptionsWithRequiredGetKey] ) { const { onSelectionChanged, ...rest } = options[0] || ({} as ISelectionOptions) this._onSelectionChangedOriginal = onSelectionChanged this._inner = new (Selection as any)({ onSelectionChanged: () => this._handleSelectionChanged(), ...rest }) } private _handleSelectionChanged() { this._triggerSelectionChanged() } private _triggerSelectionChanged() { this._allSelectionCache = null EventGroup.raise(this, SELECTION_CHANGE) if (this._onSelectionChangedOriginal) { this._onSelectionChangedOriginal() } } setAllItems(items: T[]) { this._allSelectionCache = null this._allItems = items this._allItemsMap.clear() for (const item of items) { const key = this._inner.getKey(item) this._allItemsMap.set(key, item) } // Ensure `items` is a subset of `alllItems`. If not, update `items`. const filteredItems = this._inner.getItems() const newItems: T[] = [] for (const item of filteredItems) { const key = this._inner.getKey(item) if (this._allItemsMap.has(key)) { newItems.push(item) } else { if (process.env.NODE_ENV === 'development') { console.log( 'Note: SelectionWithFilter::setAllItems is filtering away an item previously in items but not in allItems', item, key ) } } } if (filteredItems.length !== newItems.length) { this.setItems(newItems) } } getAllItems(): T[] { return this._allItems } getAllSelection(): T[] { if (!this._allSelectionCache) { this._allSelectionCache = [] for (const [key, item] of this._allItemsMap) { // Selected state of the internal Selection takes precedence if (this._itemKeysSet.has(key)) { if (this._inner.isKeySelected(key)) { this._allSelectionCache.push(item) } } else { if (this._allSelectedKeysSet.has(key)) { this._allSelectionCache.push(item) } } } // Sync current selection to _allSelectedKeysSet. This is optional but // can avoid unnecessary selectionChanged event when calling `resetAllSelection` // again with the same selection. this._allSelectedKeysSet.clear() for (const key of this._allSelectionCache) { this._allSelectedKeysSet.add(this._inner.getKey(key)) } } return this._allSelectionCache } resetAllSelection(selectedKeys: string[]) { if (process.env.NODE_ENV === 'development') { console.groupCollapsed('SelectionWithFilter.resetAllSelection') console.log('selectedKeys', selectedKeys) console.log('_allSelectedKeysSet', this._allSelectedKeysSet) console.groupEnd() } // Check whether update can be avoided let unChanged = true let validSelectedKeysCount = 0 for (const key of selectedKeys) { if (this._allItemsMap.has(key)) { validSelectedKeysCount++ if (!this._allSelectedKeysSet.has(key)) { unChanged = false break } } } if (validSelectedKeysCount !== this._allSelectedKeysSet.size) { unChanged = false } if (unChanged) { return } this._allSelectedKeysSet.clear() for (const key of selectedKeys) { if (this._allItemsMap.has(key)) { this._allSelectedKeysSet.add(key) } } // Update selection subset this._inner.setChangeEvents(false) this._inner.setAllSelected(false) for (const key of selectedKeys) { if (this._itemKeysSet.has(key)) { this._inner.setKeySelected(key, true, false) } } this._inner.setChangeEvents(true, true) this._triggerSelectionChanged() // Force trigger a selection change anyway } setAllSelectionSelected(isAllSelected: boolean) { this._inner.setChangeEvents(false) if (!isAllSelected) { this._allSelectedKeysSet.clear() this._inner.setAllSelected(false) } else { for (const key of this._allItemsMap.keys()) { this._allSelectedKeysSet.add(key) } this._inner.setAllSelected(true) } this._inner.setChangeEvents(true, true) this._triggerSelectionChanged() // Force trigger a selection change anyway } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/sqlFormatter/index.ts ================================================ import { format } from '@baurine/sql-formatter-plus' export default function formatSql(sql?: string): string { let formatedSQL = sql || '' try { formatedSQL = format(sql || '', { uppercase: true, language: 'tidb' }) } catch (err) { console.log(err) console.log(sql) } return formatedSQL } // ------------------ // a hack way to do unit test for formatSQL method if (process.env.NODE_ENV === 'development' || process.env.E2E_TEST === 'true') { function test() { const oriSQLs = [ 'select distinct `floor` ( `unix_timestamp` ( `summary_begin_time` ) ) as `begin_time` , `floor` ( `unix_timestamp` ( `summary_end_time` ) ) as `end_time` from `information_schema` . `cluster_statements_summary_history` order by `begin_time` desc , `end_time` desc', 'select `topics` . `id` from `topics` left outer join `categories` on `categories` . `id` = `topics` . `category_id` where ( `topics` . `archetype` <> ? ) and ( coalesce ( `categories` . `topic_id` , ? ) <> `topics` . `id` ) and `topics` . `visible` = true and ( `topics` . `deleted_at` is ? ) and ( `topics` . `category_id` is ? or `topics` . `category_id` in ( ... ) ) and ( `topics` . `category_id` != ? ) and `topics` . `closed` = false and `topics` . `archived` = false and ( `topics` . `created_at` > ? ) order by `rand` ( ) limit ?', 'update `app_tidb_en`.`useranyone` set `Today` = \'2022-12-28 15:29:35.604\', `DigTreasureNoPrizeCount` = 1, `DigTreasureDrawCount` = 4, `IntegralExchangeSendName` = NULL, `IntegralExchangeSendPhone` = NULL, `IntegralExchangeSendAddress` = NULL, `DayResetTaskData` = \'{"38":{"Value":5,"HasGet":true,"GetCount":5},"23":{"Value":1,"HasGet":true,"GetCount":0},"15":{"Value":10,"HasGet":true,"GetCount":1},"16":{"Value":30,"HasGet":true,"GetCount":1},"17":{"Value":60,"HasGet":true,"GetCount":1},"22":{"Value":1,"HasGet":false,"GetCount":0}}\',`NotDayResetTaskData` = \'{"27":{"Value":1,"HasGet":true,"GetCount":0},"34":{"Value":1,"HasGet":true,"GetCount":0},"28":{"Value":1,"HasGet":true,"GetCount":0},"18":{"Value":1,"HasGet":false,"GetCount":0},"45":{"Value":1,"HasGet":true,"GetCount":1}}\', `Id` = 11111 WHERE `Id` = 11111 LIMIT 1;', 'insert into event_log(`all_json`,`track_id`,`distinct_id`,`lib`,`event`,`type`,`created_at`,`date`,`hour`,`user_agent`,`host`,`connection`,`pragma`,`cache_control`,`accept`,`accept_encoding`,`accept_language`,`ip`,`ip_city`,`ip_asn`,`url`,`referrer`,`remark`,`ua_platform`,`ua_browser`,`ua_version`,`ua_language`) values (\'{ "login_id": "11111", "time": 1640768088867, "anonymous_id": "12345", "event": "$AppViewScreen", "_track_id": 12345, "_flush_time": 1640768092287, "properties": { "$os": "iOS", "$app_id": "com.app", "$screen_width": 375, "$app_version": "1.0.0", "deviceLang": "zh_TW", "$is_first_day": false, "appChannel": "ios_apple", "$device_id": "12345", "$model": "iPhone9,4", "$carrier": "Chunghwa Telecom LDM", "appCoreVer": "3", "$network_type": "3G", "$app_name": "app_name", "$wifi": false, "appProductX": "172", "$timezone_offset": -480, "$url": "app_name.CDPageViewController", "mac": "", "appVersionCode": "1", "$screen_height": 667, "appId": "11111", "$referrer": "app.CDPageBeforeViewController", "$lib_method": "autoTrack", "$screen_name": "app.CDPageViewController", "$lib_version": "1.0.0", "$os_version": "14.8.1", "$lib": "iOS", "appLangId": "2", "$manufacturer": "Apple", "idfa": "00000000-0000-0000-0000-000000000000" }, "lib": { "$lib_detail": "app.CDPageViewController######", "$lib_version": "1.0.0", "$lib": "iOS", "$app_version": "1.0.0", "$lib_method": "autoTrack" }, "distinct_id": "1111", "type": "track" }\',"1111","111","iOS","$AppViewScreen","track",111,timestamp("2022-12-29 00:00:00.000000"),16,"SensorsAnalytics iOS SDK","log.app.com",NULL,NULL,NULL,"","gzip, deflate, br","","111.111","{}","{}","url",NULL,"online","","","","");' ] const expects = [ 'SELECT\n DISTINCT `floor` (`unix_timestamp` (`summary_begin_time`)) AS `begin_time`,\n `floor` (`unix_timestamp` (`summary_end_time`)) AS `end_time`\nFROM\n `information_schema`.`cluster_statements_summary_history`\nORDER BY\n `begin_time` DESC,\n `end_time` DESC', 'SELECT\n `topics`.`id`\nFROM\n `topics`\n LEFT OUTER JOIN `categories` ON `categories`.`id` = `topics`.`category_id`\nWHERE\n (`topics`.`archetype` <> ?)\n AND (\n coalesce (`categories`.`topic_id`, ?) <> `topics`.`id`\n )\n AND `topics`.`visible` = TRUE\n AND (`topics`.`deleted_at` IS ?)\n AND (\n `topics`.`category_id` IS ?\n OR `topics`.`category_id` IN (...)\n )\n AND (`topics`.`category_id` != ?)\n AND `topics`.`closed` = false\n AND `topics`.`archived` = false\n AND (`topics`.`created_at` > ?)\nORDER BY\n `rand` ()\nLIMIT\n ?', 'UPDATE\n `app_tidb_en`.`useranyone`\nSET\n `Today` = \'2022-12-28 15:29:35.604\',\n `DigTreasureNoPrizeCount` = 1,\n `DigTreasureDrawCount` = 4,\n `IntegralExchangeSendName` = NULL,\n `IntegralExchangeSendPhone` = NULL,\n `IntegralExchangeSendAddress` = NULL,\n `DayResetTaskData` = \'{"38":{"Value":5,"HasGet":true,"GetCount":5},"23":{"Value":1,"HasGet":true,"GetCount":0},"15":{"Value":10,"HasGet":true,"GetCount":1},"16":{"Value":30,"HasGet":true,"GetCount":1},"17":{"Value":60,"HasGet":true,"GetCount":1},"22":{"Value":1,"HasGet":false,"GetCount":0}}\',\n `NotDayResetTaskData` = \'{"27":{"Value":1,"HasGet":true,"GetCount":0},"34":{"Value":1,"HasGet":true,"GetCount":0},"28":{"Value":1,"HasGet":true,"GetCount":0},"18":{"Value":1,"HasGet":false,"GetCount":0},"45":{"Value":1,"HasGet":true,"GetCount":1}}\',\n `Id` = 11111\nWHERE\n `Id` = 11111\nLIMIT\n 1;', 'INSERT INTO\n event_log(\n `all_json`,\n `track_id`,\n `distinct_id`,\n `lib`,\n `event`,\n `type`,\n `created_at`,\n `date`,\n `hour`,\n `user_agent`,\n `host`,\n `connection`,\n `pragma`,\n `cache_control`,\n `accept`,\n `accept_encoding`,\n `accept_language`,\n `ip`,\n `ip_city`,\n `ip_asn`,\n `url`,\n `referrer`,\n `remark`,\n `ua_platform`,\n `ua_browser`,\n `ua_version`,\n `ua_language`\n )\nVALUES\n (\n \'{ "login_id": "11111", "time": 1640768088867, "anonymous_id": "12345", "event": "$AppViewScreen", "_track_id": 12345, "_flush_time": 1640768092287, "properties": { "$os": "iOS", "$app_id": "com.app", "$screen_width": 375, "$app_version": "1.0.0", "deviceLang": "zh_TW", "$is_first_day": false, "appChannel": "ios_apple", "$device_id": "12345", "$model": "iPhone9,4", "$carrier": "Chunghwa Telecom LDM", "appCoreVer": "3", "$network_type": "3G", "$app_name": "app_name", "$wifi": false, "appProductX": "172", "$timezone_offset": -480, "$url": "app_name.CDPageViewController", "mac": "", "appVersionCode": "1", "$screen_height": 667, "appId": "11111", "$referrer": "app.CDPageBeforeViewController", "$lib_method": "autoTrack", "$screen_name": "app.CDPageViewController", "$lib_version": "1.0.0", "$os_version": "14.8.1", "$lib": "iOS", "appLangId": "2", "$manufacturer": "Apple", "idfa": "00000000-0000-0000-0000-000000000000" }, "lib": { "$lib_detail": "app.CDPageViewController######", "$lib_version": "1.0.0", "$lib": "iOS", "$app_version": "1.0.0", "$lib_method": "autoTrack" }, "distinct_id": "1111", "type": "track" }\',\n "1111",\n "111",\n "iOS",\n "$AppViewScreen",\n "track",\n 111,\n timestamp("2022-12-29 00:00:00.000000"),\n 16,\n "SensorsAnalytics iOS SDK",\n "log.app.com",\n NULL,\n NULL,\n NULL,\n "",\n "gzip, deflate, br",\n "",\n "111.111",\n "{}",\n "{}",\n "url",\n NULL,\n "online",\n "",\n "",\n "",\n ""\n );' ] oriSQLs.forEach((s, idx) => { const f = formatSql(s) if (f !== expects[idx]) { console.log('expected:', expects[idx]) console.log('received:', f) throw new Error(`Format sql failed!, idx: ${idx}`) } }) } test() } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/store.ts ================================================ import { InfoInfoResponse, InfoWhoAmIResponse } from '@lib/client' import { Store } from 'pullstate' interface StoreState { whoAmI?: InfoWhoAmIResponse appInfo?: InfoInfoResponse } // sync with /tidb-dashboard/pkg/apiserver/utils/ngm.go NgmState export enum NgmState { NotSupported = 'not_supported', NotStarted = 'not_started', Started = 'started' } export const store = new Store({}) export const useIsWriteable = () => store.useState((s) => Boolean(s.whoAmI && s.whoAmI.is_writeable)) export const useIsFeatureSupport = (feature: string) => store.useState( (s) => (s.appInfo?.supported_features?.indexOf(feature) ?? -1) !== -1 ) export const useNgmState = () => store.useState((s) => s.appInfo?.ngm_state) ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/tableColumnFactory.tsx ================================================ import { Tooltip } from 'antd' import { max as _max, capitalize } from 'lodash' import { IColumn, ColumnActionsMode } from 'office-ui-fabric-react/lib/DetailsList' import React from 'react' import { getValueFormat } from '@baurine/grafana-value-formats' import { Bar, Pre, TextWithInfo, TextWrap, DateTime, HighlightSQL, IColumnKeys } from '@lib/components' import { formatNumByUnit } from './format' export type DerivedField = { displayTransKey?: string // it is same as avg field name default sources: T[] } export type DerivedBar = DerivedField<{ tooltipPrefix: string fieldName: string }> export type DerivedCol = DerivedField export function formatVal(val: number, unit: string, decimals: number = 1) { return formatNumByUnit(val, unit, decimals) } export function TranslatedColumnName( transPrefix: string, fieldName: string ): any { const fullTransKey = `${transPrefix}.${fieldName}` return } export class Column { constructor( private config: IColumn, private options: { transPrefix?: string } = {} ) {} getConfig(): IColumn { const { transPrefix } = this.options return { ...this.config, name: transPrefix ? TranslatedColumnName(transPrefix, this.config.name) : this.config.name } } setConfig(config: IColumn) { this.config = config return this } patchConfig(config: Partial) { this.config = { ...this.config, ...config } return this } } export class ColumnArray { controls: Column[] // could be IColumn or Column mixed array type, not (IColumn | Column)[] type constructor(controlsConfig: any[]) { this.controls = controlsConfig.map((c) => c instanceof Column ? c : new Column(c) ) } getConfig(): IColumn[] { return this.controls.map((c) => c.getConfig()) } } export class TableColumnFactory { bar: BarColumn = new BarColumn(this) private allowColumnsMap: { [f: string]: boolean } constructor( private transPrefix: string, private allowColumns: string[] = [] ) { this.allowColumnsMap = allowColumns.reduce((prev, f) => { prev[f] = true return prev }, {} as { [f: string]: boolean }) } control(config: IColumn): Column { return new Column(config, { transPrefix: this.transPrefix }) } array(controlsConfig: any[]): ColumnArray { return new ColumnArray(controlsConfig) } columns(controlsConfig: any[]): IColumn[] { const columns = this.array(controlsConfig).getConfig() const needFilterColumn = this.allowColumns.length if (!needFilterColumn) { return columns } return columns.filter((c) => this.allowColumnsMap[c.fieldName!]) } textWithTooltip( fieldName: T, _rows?: U[] ): Column { return this.control({ ...this.getDefaultColumnConfig(fieldName), maxWidth: 150, onRender: (rec: U) => ( {rec[fieldName]} ) }) } singleBar( fieldName: T, unit: string, rows?: U[] ): Column { const capacity = rows ? _max(rows.map((v) => v[fieldName])) ?? 0 : 0 return this.control({ ...this.getDefaultColumnConfig(fieldName), minWidth: 140, maxWidth: 200, columnActionsMode: ColumnActionsMode.clickable, onRender: (rec: U) => { const fmtVal = formatVal(rec[fieldName]!, unit) return ( {fmtVal} ) } }) } multipleBar(barsConfig: DerivedBar, unit: string, rows?: T[]): Column { const { displayTransKey, sources: [avg, max, min] } = barsConfig const tooltipPrefixLens: number[] = [] tooltipPrefixLens.push(avg.tooltipPrefix.length) tooltipPrefixLens.push(max.tooltipPrefix.length) if (min) { tooltipPrefixLens.push(min.tooltipPrefix.length) } const maxTooltipPrefixLen = _max(tooltipPrefixLens) || 0 const capacity = rows ? _max(rows.map((v) => v[max.fieldName])) ?? 0 : 0 return this.control({ ...this.getDefaultColumnConfig(avg.fieldName), name: displayTransKey || avg.fieldName, minWidth: 140, maxWidth: 200, columnActionsMode: ColumnActionsMode.clickable, onRender: (rec) => { const avgVal = rec[avg.fieldName] const maxVal = rec[max.fieldName] const minVal = min ? rec[min.fieldName] : undefined const tooltips = [avg, min, max] .filter((el) => el !== undefined) .map((bar) => { const prefix = capitalize(bar!.tooltipPrefix + ':').padEnd( maxTooltipPrefixLen + 2 ) const fmtVal = formatVal(rec[bar!.fieldName], unit) return `${prefix}${fmtVal}` }) .join('\n') return ( {tooltips.trim()}}> {formatVal(avgVal, unit)} ) } }) } timestamp( fieldName: T, _rows?: U[] ): Column { return this.control({ ...this.getDefaultColumnConfig(fieldName), maxWidth: 150, columnActionsMode: ColumnActionsMode.clickable, onRender: (rec: U) => ( ) }) } sqlText( fieldName: T, showFullSQL?: boolean, _rows?: U[] ): Column { return this.control({ ...this.getDefaultColumnConfig(fieldName), maxWidth: 500, isMultiline: showFullSQL, onRender: (rec: U) => showFullSQL ? ( ) : ( } placement="right" overlayInnerStyle={{ maxHeight: '700px', overflowY: 'auto' }} > ) }) } private getDefaultColumnConfig(fieldName: string) { return { name: fieldName, key: fieldName, fieldName: fieldName, minWidth: 100 } } } export class BarColumn { constructor(public factory: TableColumnFactory) {} single( fieldName: T, unit: string, rows?: U[] ) { return this.factory.singleBar(fieldName, unit, rows) } multiple(bars: DerivedBar, unit: string, rows?: T[]) { return this.factory.multipleBar(bars, unit, rows) } } //////////////////////////////////////////// export type DerivedFields = Record< string, DerivedBar['sources'] | DerivedCol['sources'] > export function genDerivedBarSources( avg: string, max: string, min?: string ): DerivedBar['sources'] { const res = [ { tooltipPrefix: 'mean', fieldName: avg }, { tooltipPrefix: 'max', fieldName: max } ] if (min) { res.push({ tooltipPrefix: 'min', fieldName: min }) } return res } function isDerivedBarSources(v: any): v is DerivedBar['sources'] { return !!v[0].fieldName } export function getSelectedFields( visibleColumnKeys: IColumnKeys, derivedFields: DerivedFields ) { let fields: string[] = [] let sources: DerivedFields[keyof DerivedFields] for (const columnKey in visibleColumnKeys) { if (visibleColumnKeys[columnKey]) { if ((sources = derivedFields[columnKey])) { if (isDerivedBarSources(sources)) { fields.push(...sources.map((b) => b.fieldName)) } else { fields.push(...sources) } } else { fields.push(columnKey) } } } return fields } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/tableColumns.tsx ================================================ import { Tooltip } from 'antd' import { max } from 'lodash' import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' import React from 'react' import { useTranslation } from 'react-i18next' import { getValueFormat } from '@baurine/grafana-value-formats' import { Bar, Pre } from '@lib/components' import { addTranslationResource } from './i18n' import { TranslatedColumnName } from './tableColumnFactory' const translations = { en: { name: 'Name', value: 'Value', time: 'Time', desc: 'Description' }, zh: { name: '名称', value: '值', time: '时间', desc: '描述' } } for (const key in translations) { addTranslationResource(key, { component: { commonColumn: translations[key] } }) } function TransText({ transKey, noFallback }: { transKey: string noFallback?: boolean }) { const { t } = useTranslation() let opt if (noFallback) { opt = { defaultValue: '', fallbackLng: '_' } } return {t(transKey, opt)} } //////////////////////////////////// const TRANS_KEY_PREFIX = 'component.commonColumn' function fieldsKeyColumn(transKeyPrefix: string): IColumn { return { name: TranslatedColumnName(TRANS_KEY_PREFIX, 'name'), key: 'key', minWidth: 200, maxWidth: 400, onRender: (rec) => { return (
    {rec.keyDisplay ?? ( )}
    ) } } } function fieldsValueColumn(): IColumn { return { name: TranslatedColumnName(TRANS_KEY_PREFIX, 'value'), key: 'value', fieldName: 'value', minWidth: 150, maxWidth: 250 } } function fieldsTimeValueColumn( rows?: { avg?: number; min?: number; max?: number; value?: number }[] ): IColumn { const capacity = rows ? max(rows.map((v) => max([v.max, v.min, v.avg, v.value]))) ?? 0 : 0 return { name: TranslatedColumnName(TRANS_KEY_PREFIX, 'time'), key: 'time', minWidth: 150, maxWidth: 200, onRender: (rec) => { const tooltipContent: string[] = [] if (rec.avg) { tooltipContent.push(`Mean: ${getValueFormat('ns')(rec.avg, 1)}`) } if (rec.min) { tooltipContent.push(`Min: ${getValueFormat('ns')(rec.min, 1)}`) } if (rec.max) { tooltipContent.push(`Max: ${getValueFormat('ns')(rec.max, 1)}`) } const bar = ( {rec.avg != null ? getValueFormat('ns')(rec.avg, 1) : getValueFormat('ns')(rec.value, 1)} ) if (tooltipContent.length > 0) { return ( {tooltipContent.join('\n').trim()}}> {bar} ) } else { return bar } } } } function fieldsDescriptionColumn(transKeyPrefix: string): IColumn { return { name: TranslatedColumnName(TRANS_KEY_PREFIX, 'desc'), key: 'description', minWidth: 150, maxWidth: 300, onRender: (rec) => { const content = ( ) return ( {content} ) } } } //////////////////////////////////////////// export function valueColumns(transKeyPrefix: string) { return [ fieldsKeyColumn(transKeyPrefix), fieldsValueColumn(), fieldsDescriptionColumn(transKeyPrefix) ] } export function timeValueColumns( transKeyPrefix: string, items?: { avg?: number; min?: number; max?: number; value?: number }[] ) { return [ fieldsKeyColumn(transKeyPrefix), fieldsTimeValueColumn(items), fieldsDescriptionColumn(transKeyPrefix) ] } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/telemetry.ts ================================================ import mixpanel, { Config } from 'mixpanel-browser' import { getPathInLocationHash } from './routing' export { mixpanel } function init(apiHost?: string, token?: string) { let options: Partial = { autotrack: false, opt_out_tracking_by_default: true, batch_requests: true, persistence: 'localStorage', property_blacklist: [ '$initial_referrer', '$initial_referring_domain', '$referrer', '$referring_domain' ], debug: process.env.NODE_ENV === 'development' } if (apiHost) { options['api_host'] = apiHost } mixpanel.init(token || '00000000000000000000000000000000', options) // disable mixpanel to report data immediately mixpanel.opt_out_tracking() } function enable( dashboardVersion: string, extraData: { [k: string]: any } = {} ) { mixpanel.register({ $current_url: getPathInLocationHash(), dashboard_version: dashboardVersion, ...extraData }) mixpanel.opt_in_tracking() } function identifyUser(userId: string) { mixpanel.identify(userId) } // TODO: refine naming function trackRouteChange(curRoute: string) { mixpanel.register({ $current_url: curRoute }) mixpanel.track('Page Change') } export default { init, enable, identifyUser, trackRouteChange } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/timezone.ts ================================================ import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' dayjs.extend(utc) // value range: -12~14 // 8 means UTC+08:00 let _tz: number | null = null function getLocalTimeZone(): number { // in UTC+08:00 time zone, `dayjs().utcOffset()` will get 480 // while `new Date().getTimezoneOffset()` will get -480 return dayjs().utcOffset() / 60 } function getTimeZone() { if (_tz === null) { _tz = getLocalTimeZone() } return _tz } function getTimeZoneStr() { const z = getTimeZone() if (getTimeZone() >= 0) { return `utc+${z}` } return `utc${z}` } function setTimeZone(timezone: number) { // https://en.wikipedia.org/wiki/List_of_UTC_offsets if (timezone >= -12 && timezone <= 14) { _tz = timezone return } throw new Error('timezone value must be range in -12~14.') } export default { getTimeZone, getTimeZoneStr, setTimeZone } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/useCache.ts ================================================ import { createContext, useRef } from 'react' type CacheItem = { expireAt: number data: any } type CacheStorage = Record const ONE_HOUR_TIME = 1 * 60 * 60 * 1000 export const CacheContext = createContext(undefined) export class CacheMgr { private cache: CacheStorage = {} private cacheItemKeys: string[] = [] private capacity: number private globalExpire: number constructor(capacity: number = 1, globalExpire: number = ONE_HOUR_TIME) { this.capacity = capacity this.globalExpire = globalExpire } get(key: string): any { const item = this.cache[key] if (item === undefined) { return undefined } if (item.expireAt < new Date().valueOf()) { this.remove(key) return undefined } return item.data } set(key: string, val: any, expire?: number) { const curTime = new Date().valueOf() let expireAt: number if (expire) { expireAt = curTime + expire } else { expireAt = curTime + this.globalExpire } this.cache[key] = { expireAt, data: val } // put the latest key in the end of cacheItemKeys this.cacheItemKeys = this.cacheItemKeys.filter((k) => k !== key).concat(key) // if size beyonds the capacity // remove the old ones while (this.capacity > 0 && this.cacheItemKeys.length > this.capacity) { this.remove(this.cacheItemKeys[0]) } } remove(key: string) { delete this.cache[key] this.cacheItemKeys = this.cacheItemKeys.filter((k) => k !== key) } clear() { this.cache = {} this.cacheItemKeys = [] } } export default function useCache( capacity: number = 1, globalExpire: number = ONE_HOUR_TIME ): CacheMgr { const cache = useRef(new CacheMgr(capacity, globalExpire)) return cache.current } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/useCacheItemIndex.ts ================================================ import { useMemoizedFn } from 'ahooks' import { CacheMgr } from './useCache' export default function useCacheItemIndex(cacheMgr?: CacheMgr) { const CLICKED_ITEM_INDEX = 'clicked_item_index' const saveClickedItemIndex = useMemoizedFn((idx: number) => { cacheMgr?.set(CLICKED_ITEM_INDEX, idx) }) const getClickedItemIndex = useMemoizedFn(() => { return Number(cacheMgr?.get(CLICKED_ITEM_INDEX) || -1) }) return { saveClickedItemIndex, getClickedItemIndex } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/useChange.ts ================================================ import { useMemoizedFn } from 'ahooks' import { DependencyList, useEffect } from 'react' import { useDeepCompareEffect } from 'react-use' // useChange calls fn when changeList changes. // It's very similar to useEffect, but does not require fn and its dependencies to be matched. export function useChange(fn: () => any, changeList: DependencyList) { const mfn = useMemoizedFn(fn) useEffect(() => { mfn() // eslint-disable-next-line react-hooks/exhaustive-deps }, changeList) } export function useDeepCompareChange( fn: () => any, changeList: DependencyList ) { const mfn = useMemoizedFn(fn) useDeepCompareEffect(() => { mfn() // eslint-disable-next-line react-hooks/exhaustive-deps }, changeList) } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/useClientRequest.ts ================================================ import { useMount, useUnmount, useMemoizedFn } from 'ahooks' import { useState, useRef, useEffect } from 'react' import axios, { CancelToken, AxiosPromise, CancelTokenSource } from 'axios' import { ReqConfig } from '@lib/types' interface ClientReqConfig extends ReqConfig { cancelToken: CancelToken } export interface RequestFactory { (reqConfig: ClientReqConfig): AxiosPromise } interface Options { immediate?: boolean afterRequest?: () => void beforeRequest?: () => void } interface State { isLoading: boolean data?: T error?: any } export function useClientRequest( reqFactory?: RequestFactory, options?: Options ) { const { immediate = true, afterRequest = null, beforeRequest = null } = options || {} const [state, setState] = useState>({ isLoading: immediate }) // If `cancelTokenSource` is null, it means there is no running requests. const cancelTokenSource = useRef(null) const mounted = useRef(false) const sendRequest = useMemoizedFn(async () => { if (!mounted.current) { return } if (cancelTokenSource.current) { return } beforeRequest && beforeRequest() cancelTokenSource.current = axios.CancelToken.source() setState((s) => ({ ...s, isLoading: true, error: undefined })) try { if (!reqFactory) { setState({ isLoading: false }) } else { const reqConfig: ClientReqConfig = { cancelToken: cancelTokenSource.current.token, handleError: 'custom' // handle the error by component self } const resp = await reqFactory(reqConfig) if (mounted.current) { setState({ data: resp.data, isLoading: false }) } } } catch (e) { if (mounted.current) { setState({ error: e, isLoading: false }) } } cancelTokenSource.current = null afterRequest && afterRequest() }) useMount(() => { mounted.current = true if (immediate) { sendRequest() } }) useUnmount(() => { mounted.current = false if (cancelTokenSource.current != null) { cancelTokenSource.current.cancel() cancelTokenSource.current = null } }) return { ...state, sendRequest } } interface OptionsWithPolling extends Options { pollingInterval?: number shouldPoll?: ((data: T) => boolean) | null } export function useClientRequestWithPolling( reqFactory: RequestFactory, options?: OptionsWithPolling ) { const { pollingInterval = 1000, shouldPoll = null, afterRequest = null, beforeRequest = null, immediate = true } = options || {} const mounted = useRef(false) const pollingTimer = useRef | null>(null) const scheduleNextPoll = () => { if (pollingTimer.current == null && mounted.current) { pollingTimer.current = setTimeout(() => { retRef.current.sendRequest() pollingTimer.current = null }, pollingInterval) } } const cancelNextPoll = () => { if (pollingTimer.current != null) { clearTimeout(pollingTimer.current) pollingTimer.current = null } } const myBeforeRequest = () => { beforeRequest?.() cancelNextPoll() } const myAfterRequest = () => { let triggerPoll = true if (retRef.current.error) { triggerPoll = false } else if (retRef.current.data && shouldPoll) { triggerPoll = shouldPoll(retRef.current.data) } if (triggerPoll) { scheduleNextPoll() } afterRequest?.() } const ret = useClientRequest(reqFactory, { immediate, beforeRequest: myBeforeRequest, afterRequest: myAfterRequest }) const retRef = useRef(ret) useEffect(() => { retRef.current = ret }, [ret]) useMount(() => { mounted.current = true }) useUnmount(() => { mounted.current = false cancelNextPoll() }) return ret } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/useOrderState.ts ================================================ import { useState, useMemo } from 'react' import { useVersionedLocalStorageState } from './useVersionedLocalStorageState' export interface IOrderOptions { orderBy: string desc: boolean } export default function useOrderState( storeKeyPrefix: string, needSave: boolean, options: IOrderOptions ) { const storeKey = `${storeKeyPrefix}.order_options` const [memoryOrderOptions, setMemoryOrderOptions] = useState(options) const [localOrderOptions, setLocalOrderOptions] = useVersionedLocalStorageState(storeKey, { defaultValue: options }) const orderOptions = useMemo( () => (needSave ? localOrderOptions : memoryOrderOptions), [needSave, memoryOrderOptions, localOrderOptions] ) function changeOrder(orderBy: string, desc: boolean) { if (needSave) { setLocalOrderOptions({ orderBy, desc }) } else { setMemoryOrderOptions({ orderBy, desc }) } } return { orderOptions, changeOrder } } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/useQueryParams.ts ================================================ import { useMemo } from 'react' import { useLocation } from 'react-router-dom' export default function useQueryParams() { // Note: seems that history.location can be outdated sometimes. const { search } = useLocation() const params = useMemo(() => { const searchParams = new URLSearchParams(search) let _params: { [k: string]: any } = {} for (const [k, v] of searchParams) { _params[k] = v } return _params }, [search]) return params } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/useVersionedLocalStorageState.ts ================================================ import { useLocalStorageState } from 'ahooks' import { Options } from 'ahooks/lib/createUseStorageState' import { useState } from 'react' // These type definitions is a workaround for https://github.com/alibaba/hooks/issues/1582 export interface IFuncUpdaterWithDefaultValue { (previousState: T): T } // export interface OptionsWithDefaultValue extends Options { // defaultValue: T | IFuncUpdaterWithDefaultValue // } export type StorageStateResultHasDefaultValue = [ T, (value?: T | IFuncUpdaterWithDefaultValue) => void ] // Use the version field in package.json as the postfix for the localstorage key // we can **update version field in package.json** to upgrade local storage version key export function useVersionedLocalStorageState( key: string, // options: OptionsWithDefaultValue options: Options, enabled: boolean = true ): StorageStateResultHasDefaultValue { const localStorageValue = useLocalStorageState( `v${process.env.REACT_APP_VERSION}.${key}`, options ) as any const stateVlaue = useState(options.defaultValue) as any return enabled ? localStorageValue : stateVlaue } ================================================ FILE: ui/packages/tidb-dashboard-lib/src/utils/wdyr.ts ================================================ import React from 'react' if (process.env.NODE_ENV === 'development') { console.log('Development mode, enable render trackers') const whyDidYouRender = require('@welldone-software/why-did-you-render') whyDidYouRender(React) } ================================================ FILE: ui/packages/tidb-dashboard-lib/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "declaration": true, "emitDeclarationOnly": true, "outDir": "./dist", "noImplicitAny": false, "noImplicitThis": false, "baseUrl": ".", "paths": { "@lib/*": ["src/*"] } }, "include": ["src"] } ================================================ FILE: ui/pnpm-workspace.yaml ================================================ packages: # all packages in subdirs of packages/ - 'packages/**' ================================================ FILE: ui/tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": false, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx" } } ================================================ FILE: ui-v2/.changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: ui-v2/.changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [], "access": "public", "baseBranch": "ui-refactor", "updateInternalDependencies": "patch", "ignore": [] } ================================================ FILE: ui-v2/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? # tanstack router generated file routeTree.gen.ts # Cloudflare Workers .wrangler ================================================ FILE: ui-v2/.husky/pre-commit ================================================ cd ui-v2 pnpm lint-staged ================================================ FILE: ui-v2/.prettierignore ================================================ # Ignore artifacts: dist build coverage pnpm-lock.yaml # orval generated files packages/api/server/src azores.json ================================================ FILE: ui-v2/.prettierrc ================================================ { "semi": false } ================================================ FILE: ui-v2/README.md ================================================ # TiDB Dashboard UI Lib ## Requirements - Node >= 22.0.0 - [use corepack](https://www.totaltypescript.com/how-to-use-corepack): `corepack enable && corepack enable npm` ## Development ```bash pnpm i # pnpm gen:api # pnpm gen:locales pnpm dev:portals:test ``` ## Build ```bash pnpm i pnpm build ``` ## Release ```bash pnpm changeset pnpm changeset version pnpm changeset publish ``` ================================================ FILE: ui-v2/api-specs/azores.json ================================================ { "swagger": "2.0", "info": { "title": "Azores Open API", "version": "2.0.0" }, "tags": [ { "name": "MetricsService" }, { "name": "UserService" }, { "name": "RoleService" }, { "name": "ApiKeyService" }, { "name": "AlertService" }, { "name": "AuditService" }, { "name": "ClusterBRService" }, { "name": "ClusterParamService" }, { "name": "ClusterParamTemplateService" }, { "name": "CredentialService" }, { "name": "HostService" }, { "name": "TagService" }, { "name": "TiupsService" }, { "name": "ClusterService" }, { "name": "CMServerService" }, { "name": "DiagnosisService" }, { "name": "DomainService" }, { "name": "GlobalBRService" }, { "name": "LicenseService" }, { "name": "LocationService" }, { "name": "TaskFlowService" } ], "consumes": ["application/json"], "produces": ["application/json"], "paths": { "/api/v2/alert/channels": { "get": { "summary": "Get Alert Channels", "operationId": "AlertService_ListChannels", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListChannelsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "type", "description": "The channel type", "in": "query", "required": false, "type": "string", "enum": ["email", "webhook"] }, { "name": "nameLike", "description": "Filter channels by name", "in": "query", "required": false, "type": "string" }, { "name": "application", "description": "Monitor object application", "in": "query", "required": false, "type": "string" }, { "name": "alertObject", "description": "Alert object", "in": "query", "required": false, "type": "string" }, { "name": "pageSize", "description": "The number of users to retrieve per page.", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Pagination token for retrieving the next page of users.", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "The number of users to skip for pagination purposes.", "in": "query", "required": false, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] }, "post": { "summary": "Create Alert Channel", "operationId": "AlertService_CreateChannel", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Channel" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "channel", "description": "Channel", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2Channel" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/channels/{channelId}": { "delete": { "summary": "Delete Alert Channel", "operationId": "AlertService_DeleteChannel", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "channelId", "description": "ID of the channel to delete", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] }, "patch": { "summary": "Update Alert Channel", "operationId": "AlertService_UpdateChannel", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Channel" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "channelId", "description": "ID of the channel to update", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/AlertServiceUpdateChannelBody" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/events": { "get": { "summary": "Get Alert Events", "operationId": "AlertService_ListEvents", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListEventsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "ID of the cluster to filter events", "in": "query", "required": false, "type": "string" }, { "name": "level", "description": "The event level", "in": "query", "required": false, "type": "string", "enum": ["warning", "critical", "emergency"] }, { "name": "instance", "description": "Instance name to filter events", "in": "query", "required": false, "type": "string" }, { "name": "status", "description": "The event status", "in": "query", "required": false, "type": "string", "enum": ["alerting", "resolved", "silenced"] }, { "name": "startTime", "description": "Start time for event filtering", "in": "query", "required": true, "type": "string", "format": "date-time" }, { "name": "endTime", "description": "End time for event filtering", "in": "query", "required": true, "type": "string", "format": "date-time" }, { "name": "keyword", "description": "Keyword to search in event summary", "in": "query", "required": false, "type": "string" }, { "name": "pageSize", "description": "The number of users to retrieve per page.", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Pagination token for retrieving the next page of users.", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "The number of users to skip for pagination purposes.", "in": "query", "required": false, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] } }, "/api/v2/alert/events/overview": { "get": { "summary": "cluster events overview", "operationId": "AlertService_GetEventsOverview", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2EventsOverview" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "query", "required": false, "type": "string" }, { "name": "startTime", "description": "Start time for filtering", "in": "query", "required": false, "type": "string", "format": "date-time" }, { "name": "endTime", "description": "End time for filtering", "in": "query", "required": false, "type": "string", "format": "date-time" } ], "tags": ["AlertService"] } }, "/api/v2/alert/events/{eventId}:silenceEvent": { "post": { "summary": "silence alert", "operationId": "AlertService_SilenceEvent", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Event" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "eventId", "description": "ID of the alert to silence", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/AlertServiceSilenceEventBody" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/monitorObjects": { "get": { "summary": "Get Monitor Objects", "operationId": "AlertService_ListMonitorObjects", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListMonitorObjectsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "application", "description": "Filter by application", "in": "query", "required": false, "type": "string" }, { "name": "objectType", "description": "Filter by object type", "in": "query", "required": false, "type": "string" } ], "tags": ["AlertService"] }, "post": { "summary": "Create Monitor Object", "operationId": "AlertService_CreateMonitorObject", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2MonitorObject" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "monitorObject", "description": "The monitor object to create", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2MonitorObject" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/monitorObjects/{objectId}": { "get": { "summary": "Get Monitor Object", "operationId": "AlertService_GetMonitorObject", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2MonitorObject" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "objectId", "description": "The ID of the object to retrieve", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] }, "delete": { "summary": "Delete Monitor Object", "operationId": "AlertService_DeleteMonitorObject", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "objectId", "description": "The ID of the object to delete", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] }, "patch": { "summary": "Update Monitor Object", "operationId": "AlertService_UpdateMonitorObject", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2MonitorObject" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "objectId", "description": "The ID of the object to update", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/AlertServiceUpdateMonitorObjectBody" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/objectRules": { "get": { "summary": "Get Alert Object Rules", "operationId": "AlertService_ListObjectRules", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListObjectRulesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "skip", "description": "The number of rules to skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageSize", "description": "The number of rules to retrieve per page", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "The pagination token", "in": "query", "required": false, "type": "string" }, { "name": "orderBy", "description": "The sorting criteria", "in": "query", "required": false, "type": "string" }, { "name": "keyword", "description": "Filter by name ,expression", "in": "query", "required": false, "type": "string" }, { "name": "status", "description": "The rule status", "in": "query", "required": false, "type": "string", "enum": ["enabled", "disabled"] }, { "name": "level", "description": "The alert level", "in": "query", "required": false, "type": "string", "enum": ["warning", "critical", "emergency"] }, { "name": "alertObject", "description": "Filter by alert object", "in": "query", "required": false, "type": "string" }, { "name": "monitorObject.id", "description": "The unique identifier of the alert rule", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "monitorObject.application", "description": "The monitor object application", "in": "query", "required": false, "type": "string" }, { "name": "monitorObject.objectType", "description": "The type of the monitor object", "in": "query", "required": false, "type": "string" } ], "tags": ["AlertService"] }, "post": { "summary": "Create Alert Object Rule", "operationId": "AlertService_CreateObjectRule", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ObjectRule" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "objectRule", "description": "The object rule to create", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2ObjectRule" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/objectRules/{objectRuleId}": { "get": { "summary": "Get Alert Object Rule", "operationId": "AlertService_GetObjectRule", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ObjectRule" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "objectRuleId", "description": "The ID of the rule to retrieve", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] }, "delete": { "summary": "Delete Alert Object Rule", "operationId": "AlertService_DeleteObjectRule", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "objectRuleId", "description": "The ID of the rule to delete", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] }, "patch": { "summary": "Update Alert Object Rule", "operationId": "AlertService_UpdateObjectRule", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ObjectRule" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "objectRuleId", "description": "The ID of the rule to update", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/AlertServiceUpdateObjectRuleBody" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/rules": { "get": { "summary": "Get Alert Rules", "operationId": "AlertService_ListRules", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListRulesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "skip", "description": "The number of rules to skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageSize", "description": "The number of rules to retrieve per page", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "The pagination token", "in": "query", "required": false, "type": "string" }, { "name": "orderBy", "description": "The sorting criteria", "in": "query", "required": false, "type": "string" }, { "name": "keyword", "description": "Filter by name ,expression", "in": "query", "required": false, "type": "string" }, { "name": "level", "description": "The alert level", "in": "query", "required": false, "type": "string", "enum": ["warning", "critical", "emergency"] }, { "name": "monitorObject.id", "description": "The unique identifier of the alert rule", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "monitorObject.application", "description": "The monitor object application", "in": "query", "required": false, "type": "string" }, { "name": "monitorObject.objectType", "description": "The type of the monitor object", "in": "query", "required": false, "type": "string" } ], "tags": ["AlertService"] }, "post": { "summary": "Create Alert Rule", "operationId": "AlertService_CreateRule", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Rule" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "rule", "description": "The rule to create", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2Rule" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/rules/{ruleId}": { "get": { "summary": "Get Alert Rule", "operationId": "AlertService_GetRule", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Rule" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "ruleId", "description": "The ID of the rule to retrieve", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] }, "delete": { "summary": "Delete Alert Rule", "operationId": "AlertService_DeleteRule", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "ruleId", "description": "The ID of the rule to delete", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] }, "patch": { "summary": "Update Alert Rule", "operationId": "AlertService_UpdateRule", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Rule" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "ruleId", "description": "The ID of the rule to update", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/AlertServiceUpdateRuleBody" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/templates": { "get": { "summary": "Get Alert Templates", "operationId": "AlertService_ListTemplates", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListTemplatesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "The number of templates to retrieve per page", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Pagination token for retrieving the next page of templates", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "The number of templates to skip for pagination purposes", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "name", "description": "Filter templates by name", "in": "query", "required": false, "type": "string" }, { "name": "tidbVersion", "description": "TiDB cluster version, used to filter templates", "in": "query", "required": false, "type": "string" }, { "name": "nameLike", "description": "Filter templates by name use like", "in": "query", "required": false, "type": "string" } ], "tags": ["AlertService"] }, "post": { "summary": "Create Alert Template", "operationId": "AlertService_CreateTemplate", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Template" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "template", "description": "Template", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2Template" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/templates/{templateId}": { "delete": { "summary": "Delete Alert Template", "operationId": "AlertService_DeleteTemplate", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "templateId", "description": "ID of the template to delete", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] }, "patch": { "summary": "Update Alert Template", "operationId": "AlertService_UpdateTemplate", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TemplateWithOutRules" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "templateId", "description": "ID of the template to update", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/AlertServiceUpdateTemplateBody" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/templates/{templateId}/rules": { "get": { "summary": "Get Alert Template rules", "operationId": "AlertService_ListTemplateRules", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListTemplateRulesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "templateId", "description": "ID of the template", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "pageSize", "description": "The number of rules to retrieve per page", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Pagination token for retrieving the next page", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "The number of rules to skip for pagination purposes", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "keyword", "description": "Filter rules by keyword", "in": "query", "required": false, "type": "string" }, { "name": "level", "description": "The alert level", "in": "query", "required": false, "type": "string", "enum": ["warning", "critical", "emergency"] } ], "tags": ["AlertService"] }, "post": { "summary": "Create rule in template", "operationId": "AlertService_CreateTemplateRule", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TemplateRule" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "templateId", "description": "ID of the template to update", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "templateRule", "description": "Rule to create", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2TemplateRule" } } ], "tags": ["AlertService"] } }, "/api/v2/alert/templates/{templateId}/rules/{ruleId}": { "delete": { "summary": "Delete rule in template", "operationId": "AlertService_DeleteTemplateRule", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "templateId", "description": "ID of the template to update", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "ruleId", "description": "ID of the rule to delete", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["AlertService"] }, "patch": { "summary": "Update rule in template", "operationId": "AlertService_UpdateTemplateRule", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TemplateRule" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "templateId", "description": "ID of the template to update", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "ruleId", "description": "ID of the rule to update", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/AlertServiceUpdateTemplateRuleBody" } } ], "tags": ["AlertService"] } }, "/api/v2/apiKeys": { "get": { "summary": "ListApiKeys retrieves a list of API keys.", "operationId": "ApiKeyService_ListApiKeys", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListApiKeysResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "accessKey", "description": "The access_key of the apikey", "in": "query", "required": false, "type": "string" }, { "name": "creator", "description": "The access_key of the apikey", "in": "query", "required": false, "type": "string" }, { "name": "status", "description": "The status of the apikey", "in": "query", "required": false, "type": "string" } ], "tags": ["ApiKeyService"] }, "post": { "summary": "CreateApiKey creates a new API key.", "operationId": "ApiKeyService_CreateApiKey", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ApiKey" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2CreateApiKeyRequest" } } ], "tags": ["ApiKeyService"] } }, "/api/v2/apiKeys/{accessKey}": { "get": { "summary": "GetApiKeyRequest get an API key by its access key.", "operationId": "ApiKeyService_GetApiKey", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ApiKey" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "accessKey", "description": "The access key of the apiKey", "in": "path", "required": true, "type": "string" } ], "tags": ["ApiKeyService"] }, "delete": { "summary": "DeleteApiKey deletes an API key by its access key.", "operationId": "ApiKeyService_DeleteApiKey", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "accessKey", "description": "The access key of the apiKey", "in": "path", "required": true, "type": "string" } ], "tags": ["ApiKeyService"] }, "patch": { "summary": "UpdateApiKey updates an API key by its access key.", "operationId": "ApiKeyService_UpdateApiKey", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ApiKey" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "accessKey", "description": "The access_key of apikey", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ApiKeyServiceUpdateApiKeyBody" } } ], "tags": ["ApiKeyService"] } }, "/api/v2/apiKeys/{accessKey}:resetSecretKey": { "patch": { "summary": "ResetSecretKey resets the secret key for an existing API key.", "operationId": "ApiKeyService_ResetSecretKey", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ResetSecretKeyResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "accessKey", "description": "The access key of the apiKey", "in": "path", "required": true, "type": "string" } ], "tags": ["ApiKeyService"] } }, "/api/v2/audit/config": { "get": { "summary": "get audit configuration", "operationId": "AuditService_GetAuditConfigs", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2AuditConfigs" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["AuditService"] }, "put": { "summary": "Replace audit configuration", "operationId": "AuditService_UpdateAuditConfigs", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2AuditConfigs" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2UpdateAuditConfigsRequest" } } ], "tags": ["AuditService"] } }, "/api/v2/audit/logs": { "get": { "summary": "Get audit logs", "operationId": "AuditService_GetAuditLogs", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2AuditLogs" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "startTime", "description": "Start time", "in": "query", "required": true, "type": "string", "format": "date-time" }, { "name": "endTime", "description": "End time", "in": "query", "required": true, "type": "string", "format": "date-time" }, { "name": "operatorId", "description": "Operator id", "in": "query", "required": false, "type": "string" }, { "name": "operatorType", "description": "The audit operator type", "in": "query", "required": false, "type": "string", "enum": ["USER", "API_KEY"] }, { "name": "event", "description": "The audit event", "in": "query", "required": false, "type": "string", "enum": [ "User", "Role", "ApiKey", "Metrics", "Host", "Tag", "Cluster", "Credential", "GlobalBR", "ClusterBR", "License", "Diagnosis", "Location", "Tiups", "Audit" ] }, { "name": "operation", "description": "operation", "in": "query", "required": false, "type": "string" }, { "name": "result", "description": "The audit result", "in": "query", "required": false, "type": "string", "enum": ["success", "failure"] }, { "name": "skip", "description": "skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" } ], "tags": ["AuditService"] } }, "/api/v2/audit/logs:download": { "get": { "summary": "Get download URL for audit logs", "operationId": "AuditService_DownloadAuditLogs", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2DownloadAuditLogsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "startTime", "description": "Start time for filtering audit logs", "in": "query", "required": true, "type": "string", "format": "date-time" }, { "name": "endTime", "description": "End time for filtering audit logs", "in": "query", "required": true, "type": "string", "format": "date-time" }, { "name": "operatorType", "description": "Operator type", "in": "query", "required": false, "type": "string" }, { "name": "operatorId", "description": "Value of the Operator", "in": "query", "required": false, "type": "string" }, { "name": "event", "description": "Event (e.g., \"cluster\", \"user\")", "in": "query", "required": false, "type": "string" }, { "name": "operation", "description": "Operation type (e.g., \"create\", \"update\", \"delete\")", "in": "query", "required": false, "type": "string" }, { "name": "result", "description": "Result of the operation (e.g., \"success\", \"failure\")", "in": "query", "required": false, "type": "string" } ], "tags": ["AuditService"] } }, "/api/v2/backup/policies": { "get": { "summary": "ListBackupPolicies lists Backup policies", "operationId": "GlobalBRService_ListBackupPolicies", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListBackupPoliciesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "isDesc", "description": "Is descending order", "in": "query", "required": false, "type": "boolean" } ], "tags": ["GlobalBRService"] }, "post": { "summary": "CreateBackupPolicy creates a Backup policy", "operationId": "GlobalBRService_CreateBackupPolicy", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BackupPolicy" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2BackupPolicy" } } ], "tags": ["GlobalBRService"] } }, "/api/v2/backup/policies/precheck": { "post": { "summary": "PreCheckBackupPolicy pre-checks a Backup policy", "operationId": "GlobalBRService_PreCheckBackupPolicy", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2PreCheckBackupPolicyResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2BackupPolicy" } } ], "tags": ["GlobalBRService"] } }, "/api/v2/backup/policies/{policyId}": { "get": { "summary": "GetBackupPolicy gets a Backup policy", "operationId": "GlobalBRService_GetBackupPolicy", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BackupPolicy" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "policyId", "description": "The policy ID", "in": "path", "required": true, "type": "string" } ], "tags": ["GlobalBRService"] }, "delete": { "summary": "DeleteBackupPolicy deletes a Backup policy", "operationId": "GlobalBRService_DeleteBackupPolicy", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "policyId", "description": "The policy ID", "in": "path", "required": true, "type": "string" } ], "tags": ["GlobalBRService"] }, "put": { "summary": "UpdateBackupPolicy updates a Backup policy", "operationId": "GlobalBRService_UpdateBackupPolicy", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BackupPolicy" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "policyId", "description": "The policy ID", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/GlobalBRServiceUpdateBackupPolicyBody" } } ], "tags": ["GlobalBRService"] } }, "/api/v2/backup/summary": { "get": { "summary": "GetBRSummary retrieves the summary of BR", "operationId": "GlobalBRService_GetBRSummary", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BRSummary" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "top", "description": "Number of top clusters", "in": "query", "required": false, "type": "integer", "format": "int32" } ], "tags": ["GlobalBRService"] } }, "/api/v2/backup/tasks": { "get": { "summary": "ListBRTasks retrieves the tasks of BR", "operationId": "GlobalBRService_ListBRTasks", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListBRTasksResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "isDesc", "description": "Is descending order", "in": "query", "required": false, "type": "boolean" }, { "name": "brTaskId", "description": "The br task ID", "in": "query", "required": false, "type": "string" }, { "name": "clusterId", "description": "The cluster ID", "in": "query", "required": false, "type": "string" }, { "name": "clusterName", "description": "The cluster name", "in": "query", "required": false, "type": "string" }, { "name": "type", "description": "Type of the br task\n\n - all: All\n - full_backup: Full backup\n - log_backup: Log backup\n - restore_by_file: Restore by file\n - restore_by_time: Restore by time\n - all_backup: All backup\n - all_restore: All restore", "in": "query", "required": false, "type": "string", "enum": [ "all", "full_backup", "log_backup", "restore_by_file", "restore_by_time", "all_backup", "all_restore" ], "default": "all" }, { "name": "status", "description": "Status of the br task\n\n - all: All\n - running: Running\n - finished: Finished\n - abnormal: Abnormal\n - stopped: Stopped", "in": "query", "required": false, "type": "string", "enum": ["all", "running", "finished", "abnormal", "stopped"], "default": "all" } ], "tags": ["GlobalBRService"] } }, "/api/v2/backup/tasks/{taskId}": { "delete": { "summary": "DeleteBRTask deletes a BR task", "operationId": "GlobalBRService_DeleteBRTask", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "taskId", "description": "The br task ID", "in": "path", "required": true, "type": "string" }, { "name": "deleteBackupFile", "description": "delete_backup_file for whether delete the backup files or not", "in": "query", "required": false, "type": "boolean" } ], "tags": ["GlobalBRService"] } }, "/api/v2/backup/tasks/{taskId}/start": { "post": { "summary": "StartBRTask starts a BR task", "operationId": "GlobalBRService_StartBRTask", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "taskId", "description": "The br task ID", "in": "path", "required": true, "type": "string" } ], "tags": ["GlobalBRService"] } }, "/api/v2/backup/tasks/{taskId}/stop": { "post": { "summary": "StopBRTask stops a BR task", "operationId": "GlobalBRService_StopBRTask", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "taskId", "description": "The br task ID", "in": "path", "required": true, "type": "string" } ], "tags": ["GlobalBRService"] } }, "/api/v2/clusters": { "get": { "summary": "ListClusters retrieves the list of running processes in a cluster", "operationId": "ClusterService_ListClusters", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListClustersResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "searchValue", "description": "The name of the user", "in": "query", "required": false, "type": "string" }, { "name": "tagIds", "description": "tag_ids", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/clusterTaskFlow": { "get": { "summary": "ClusterTaskFlow", "operationId": "ClusterService_ClusterTaskFlow", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterTaskFlowResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}": { "get": { "summary": "GetClusters in a cluster", "operationId": "ClusterService_GetClusters", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Clusters" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the host", "in": "path", "required": true, "type": "string" } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}/backup": { "post": { "summary": "CreateBackupTask backups a cluster", "operationId": "ClusterBRService_CreateBackupTask", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterBRTask" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterBRServiceCreateBackupTaskBody" } } ], "tags": ["ClusterBRService"] } }, "/api/v2/clusters/{clusterId}/backup/policy": { "get": { "summary": "GetClusterBackupPolicy gets the backup info of a specific cluster", "operationId": "ClusterBRService_GetClusterBackupPolicy", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterBackupPolicy" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" } ], "tags": ["ClusterBRService"] } }, "/api/v2/clusters/{clusterId}/backup/records": { "get": { "summary": "ListBackupRecords lists the valid full backup records of a specific cluster", "operationId": "ClusterBRService_ListClusterBackupRecords", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListClusterBackupRecordsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "isDesc", "description": "Is descending order", "in": "query", "required": false, "type": "boolean" } ], "tags": ["ClusterBRService"] } }, "/api/v2/clusters/{clusterId}/backup/restore": { "post": { "summary": "CreateRestoreTask restores a cluster", "operationId": "ClusterBRService_CreateRestoreTask", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterBRTask" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID represents the cluster to be restored", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterBRServiceCreateRestoreTaskBody" } } ], "tags": ["ClusterBRService"] } }, "/api/v2/clusters/{clusterId}/backup/tasks": { "get": { "summary": "ListClusterBRTasks lists the backup tasks of a specific cluster", "operationId": "ClusterBRService_ListClusterBRTasks", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListClusterBRTasksResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "isDesc", "description": "Is descending order", "in": "query", "required": false, "type": "boolean" }, { "name": "brTaskId", "description": "The br task ID", "in": "query", "required": false, "type": "string" }, { "name": "clusterName", "description": "The cluster name", "in": "query", "required": false, "type": "string" }, { "name": "type", "description": "Type of the br task\n\n - full_backup: Full backup\n - log_backup: Log backup\n - restore_by_file: Restore by file\n - restore_by_time: Restore by time\n - all_backup: All backup\n - all_restore: All restore", "in": "query", "required": false, "type": "string", "enum": [ "full_backup", "log_backup", "restore_by_file", "restore_by_time", "all_backup", "all_restore" ], "default": "full_backup" }, { "name": "status", "description": "Status of the br task\n\n - running: Running\n - finished: Finished\n - abnormal: Abnormal\n - stopped: Stopped", "in": "query", "required": false, "type": "string", "enum": ["running", "finished", "abnormal", "stopped"], "default": "running" } ], "tags": ["ClusterBRService"] } }, "/api/v2/clusters/{clusterId}/backup:detect": { "post": { "summary": "DetectCluster detects the if the cluster exist", "operationId": "ClusterBRService_DetectCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2DetectClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" } ], "tags": ["ClusterBRService"] } }, "/api/v2/clusters/{clusterId}/clusterYaml": { "get": { "summary": "ClusterYaml cluster", "operationId": "ClusterService_ClusterYaml", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterYamlResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id", "in": "path", "required": true, "type": "string" } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}/config/components": { "get": { "summary": "GetClusterAvailableConfigComponents retrieves a list of available config components", "operationId": "ClusterParamService_GetClusterAvailableConfigComponents", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterAvailableConfigComponents" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "ClusterID is the ID of the cluster", "in": "path", "required": true, "type": "string" } ], "tags": ["ClusterParamService"] } }, "/api/v2/clusters/{clusterId}/configs": { "get": { "summary": "ListClusterConfigs retrieves a list of cluster configs", "operationId": "ClusterParamService_ListClusterConfigs", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListClusterConfigsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "ClusterID is the ID of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "nonDefault", "description": "NonDefault is if the config is non-default", "in": "query", "required": false, "type": "boolean" }, { "name": "needReload", "description": "NeedReload is if the config needs to be reloaded", "in": "query", "required": false, "type": "boolean" }, { "name": "dynamic", "description": "Dynamic is if the config is dynamic", "in": "query", "required": false, "type": "boolean" }, { "name": "instanceType", "description": "InstanceType is the instance type of the config", "in": "query", "required": false, "type": "string" }, { "name": "name", "description": "Name is the name of the config", "in": "query", "required": false, "type": "string" } ], "tags": ["ClusterParamService"] }, "patch": { "summary": "UpdateClusterConfig updates a cluster config", "operationId": "ClusterParamService_UpdateClusterConfig", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "ClusterID is the ID of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterParamServiceUpdateClusterConfigBody" } } ], "tags": ["ClusterParamService"] } }, "/api/v2/clusters/{clusterId}/metrics/{name}/data": { "get": { "summary": "Get cluster metric data", "operationId": "MetricsService_GetClusterMetricData", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterMetricData" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "name", "description": "Metric Name", "in": "path", "required": true, "type": "string" }, { "name": "startTime", "description": "Start time in Unix timestamp format", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "endTime", "description": "End time in Unix timestamp format", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "step", "description": "Step time in seconds", "in": "query", "required": false, "type": "string", "format": "int64" }, { "name": "label", "description": "Line Label for the metric", "in": "query", "required": false, "type": "string" }, { "name": "range", "description": "Time Range for the query", "in": "query", "required": false, "type": "string" } ], "tags": ["MetricsService"] } }, "/api/v2/clusters/{clusterId}/metrics/{name}/instance": { "get": { "summary": "Get metric instances", "operationId": "MetricsService_GetClusterMetricInstance", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterMetricInstance" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "name", "description": "Metric name", "in": "path", "required": true, "type": "string" } ], "tags": ["MetricsService"] } }, "/api/v2/clusters/{clusterId}/nodes": { "get": { "summary": "Nodes", "operationId": "ClusterService_Nodes", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2NodesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id", "in": "path", "required": true, "type": "string" }, { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "component", "description": "The component", "in": "query", "required": true, "type": "string" }, { "name": "status", "description": "The status", "in": "query", "required": true, "type": "string" }, { "name": "ipLike", "description": "The ip_like", "in": "query", "required": true, "type": "string" } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}/nodes:batchPause": { "post": { "summary": "BatchPause cluster", "operationId": "ClusterService_BatchPause", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BatchPauseResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceBatchPauseBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}/nodes:batchReload": { "post": { "summary": "BatchReload", "operationId": "ClusterService_BatchReload", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BatchReloadResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceBatchReloadBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}/nodes:batchRestart": { "post": { "summary": "BatchStart cluster", "operationId": "ClusterService_BatchRestart", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BatchRestartResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceBatchRestartBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}/nodes:batchResume": { "post": { "summary": "BatchResume cluster", "operationId": "ClusterService_BatchResume", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BatchResumeResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceBatchResumeBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}/resourceUsage": { "get": { "summary": "Get cluster metric resource level", "operationId": "MetricsService_GetClusterResourceUsage", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterResourceUsage" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" } ], "tags": ["MetricsService"] } }, "/api/v2/clusters/{clusterId}/resourcegroups": { "get": { "summary": "Get resource group list", "operationId": "DiagnosisService_GetResourceGroupList", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ResourceGroupList" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster id", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/sessions": { "get": { "summary": "GetProcessList retrieves the list of running processes in a cluster", "operationId": "ClusterService_GetProcessList", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ProcessList" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID uniquely identifies the target cluster", "in": "path", "required": true, "type": "string" } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}/sessions/{sessionId}": { "delete": { "summary": "DeleteProcess terminates a specific process in the cluster", "operationId": "ClusterService_DeleteProcess", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID uniquely identifies the target cluster", "in": "path", "required": true, "type": "string" }, { "name": "sessionId", "description": "Session ID identifies the process to be terminated", "in": "path", "required": true, "type": "string" } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}/slowqueries": { "get": { "summary": "GetSlowQueryList retrieves the list of slow queries", "operationId": "DiagnosisService_GetSlowQueryList", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2SlowQueryList" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "beginTime", "description": "Begin time in Unix timestamp", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "endTime", "description": "End time in Unix timestamp", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "db", "description": "List of databases", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" }, { "name": "text", "description": "Search text", "in": "query", "required": false, "type": "string" }, { "name": "orderBy", "description": "Order by field", "in": "query", "required": false, "type": "string" }, { "name": "isDesc", "description": "Is descending order", "in": "query", "required": false, "type": "boolean" }, { "name": "fields", "description": "Fields to select, e.g., \"Query,Digest\"", "in": "query", "required": false, "type": "string" }, { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token for pagination", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Number of records to skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "advancedFilter", "description": "Advanced filters, such as \"digest = xxx\"", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/slowqueries/advancedFilters": { "get": { "summary": "GetSlowQueryAvailableAdvancedFilters retrieves the list of available advanced filters", "operationId": "DiagnosisService_GetSlowQueryAvailableAdvancedFilters", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2SlowQueryAvailableAdvancedFilters" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/slowqueries/advancedFilters/{filterName}": { "get": { "summary": "GetSlowQueryAvailableAdvancedFilterInfo retrieves the list of available advanced filter info", "operationId": "DiagnosisService_GetSlowQueryAvailableAdvancedFilterInfo", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2SlowQueryAvailableAdvancedFilterInfo" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "filterName", "description": "filter name", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/slowqueries/download": { "get": { "summary": "DownloadSlowQueryList downloads the list of slow queries", "operationId": "DiagnosisService_DownloadSlowQueryList", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2SlowQueryDownloadResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "beginTime", "description": "Begin time in Unix timestamp", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "endTime", "description": "End time in Unix timestamp", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "db", "description": "List of databases", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" }, { "name": "text", "description": "Search text", "in": "query", "required": false, "type": "string" }, { "name": "orderBy", "description": "Order by field", "in": "query", "required": false, "type": "string" }, { "name": "isDesc", "description": "Is descending order", "in": "query", "required": false, "type": "boolean" }, { "name": "fields", "description": "Fields to select, e.g., \"Query,Digest\"", "in": "query", "required": false, "type": "string" }, { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token for pagination", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Number of records to skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "advancedFilter", "description": "Advanced filters, such as \"digest = xxx\"", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/slowqueries/fields": { "get": { "summary": "GetSlowQueryAvailableFields retrieves the list of available fields for slow queries", "operationId": "DiagnosisService_GetSlowQueryAvailableFields", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2SlowQueryAvailableFields" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/slowqueries/{digest}": { "get": { "summary": "GetSlowQueryDetail retrieves the details of a specific slow query", "operationId": "DiagnosisService_GetSlowQueryDetail", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2SlowQueryDetail" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "digest", "description": "Query digest", "in": "path", "required": true, "type": "string" }, { "name": "timestamp", "description": "Timestamp", "in": "query", "required": true, "type": "number", "format": "double" }, { "name": "connectionId", "description": "Connection ID", "in": "query", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/sqllimits:addSqlLimit": { "post": { "summary": "Create SQL limit", "operationId": "DiagnosisService_AddSqlLimit", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster Id", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/DiagnosisServiceAddSqlLimitBody" } } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/sqllimits:checkSupport": { "get": { "summary": "Check if SQL limit is supported", "operationId": "DiagnosisService_CheckSqlLimitSupport", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2CheckSupportResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/sqllimits:removeSqlLimit": { "post": { "summary": "Remove SQL limit", "operationId": "DiagnosisService_RemoveSqlLimit", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster Id", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/DiagnosisServiceRemoveSqlLimitBody" } } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/sqllimits:showSqlLimit": { "get": { "summary": "Query SQL limit", "operationId": "DiagnosisService_GetSqlLimitList", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2SqlLimitList" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster Id", "in": "path", "required": true, "type": "string" }, { "name": "watchText", "description": "Watch text", "in": "query", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/sqlplans": { "get": { "summary": "GetSqlPlanList retrieves the list of plans", "operationId": "DiagnosisService_GetSqlPlanList", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2SqlPlanList" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "beginTime", "description": "Begin time", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "endTime", "description": "End time", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "digest", "description": "SQL digest", "in": "query", "required": false, "type": "string" }, { "name": "schemaName", "description": "Table name", "in": "query", "required": false, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/sqlplans/{planDigest}:bindSqlPlan": { "post": { "summary": "BindSqlPlan binds a plan to a specific sql", "operationId": "DiagnosisService_BindSqlPlan", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "planDigest", "description": "SQL plan digest", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/sqlplans:checkSupport": { "get": { "summary": "CheckSupport returns whether sql plan binding is supported", "operationId": "DiagnosisService_CheckSqlPlanSupport", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2CheckSupportResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/sqlplans:showSqlPlanBinding": { "get": { "summary": "GetSQLBindInfo", "operationId": "DiagnosisService_GetSqlPlanBindingList", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2SqlPlanBindingList" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "beginTime", "description": "Begin time", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "endTime", "description": "End time", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "digest", "description": "SQL digest", "in": "query", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/sqlplans:unbindSqlPlan": { "post": { "summary": "UnbindSqlPlan unbinds a plan from a specific sql", "operationId": "DiagnosisService_UnbindSqlPlan", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "digest", "description": "SQL digest", "in": "query", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/topologySummary": { "get": { "summary": "TopologySummary", "operationId": "ClusterService_TopologySummary", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TopologySummaryResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id", "in": "path", "required": true, "type": "string" } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}/topsqls": { "get": { "summary": "GetTopSqlList retrieves the list of top sql", "operationId": "DiagnosisService_GetTopSqlList", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TopSqlList" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "beginTime", "description": "Begin time", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "endTime", "description": "End time", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "db", "description": "Database list", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" }, { "name": "text", "description": "SQL Text, used for fuzzy query", "in": "query", "required": false, "type": "string" }, { "name": "orderBy", "description": "Order by field", "in": "query", "required": false, "type": "string" }, { "name": "isDesc", "description": "Is descending order", "in": "query", "required": false, "type": "boolean" }, { "name": "fields", "description": "Fields to select, e.g., \"Query,Digest\"", "in": "query", "required": false, "type": "string" }, { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "advancedFilter", "description": "Advanced filters, such as \"digest = xxx\"", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" }, { "name": "isGroupByTime", "description": "Is group by time", "in": "query", "required": false, "type": "boolean" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/topsqls/advancedFilters": { "get": { "summary": "GetTopSqlAvailableAdvancedFilters retrieves the list of available advanced filters", "operationId": "DiagnosisService_GetTopSqlAvailableAdvancedFilters", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TopSqlAvailableAdvancedFilters" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/topsqls/advancedFilters/{filterName}": { "get": { "summary": "GetTopSqlAvailableAdvancedFilterInfo retrieves the list of available advanced filter info", "operationId": "DiagnosisService_GetTopSqlAvailableAdvancedFilterInfo", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TopSqlAvailableAdvancedFilterInfo" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "filterName", "description": "filter name", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/topsqls/configs": { "get": { "summary": "GetTopSqlConfigs retrieves the list of top sql configs", "operationId": "DiagnosisService_GetTopSqlConfigs", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TopSqlConfigs" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] }, "patch": { "summary": "UpdateTopSqlConfigs updates the list of top sql configs", "operationId": "DiagnosisService_UpdateTopSqlConfigs", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TopSqlConfigs" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/DiagnosisServiceUpdateTopSqlConfigsBody" } } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/topsqls/fields": { "get": { "summary": "GetTopSqlAvailableFields retrieves the list of available fields for top sqls", "operationId": "DiagnosisService_GetTopSqlAvailableFields", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TopSqlAvailableFields" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/topsqls/{digest}": { "get": { "summary": "GetTopSqlDetail retrieves the details of a specific top sql", "operationId": "DiagnosisService_GetTopSqlDetail", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TopSqlDetail" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "digest", "description": "Query digest", "in": "path", "required": true, "type": "string" }, { "name": "beginTime", "description": "Begin time", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "endTime", "description": "End time", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "planDigest", "description": "Plan digest list", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" } ], "tags": ["DiagnosisService"] } }, "/api/v2/clusters/{clusterId}/variables": { "get": { "summary": "ListClusterVariables retrieves a list of cluster variables", "operationId": "ClusterParamService_ListClusterVariables", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListClusterVariablesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "ClusterID is the ID of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "nonDefault", "description": "NonDefault is if the variable is non-default", "in": "query", "required": false, "type": "boolean" }, { "name": "name", "description": "Name is the name of the variable", "in": "query", "required": false, "type": "string" } ], "tags": ["ClusterParamService"] }, "patch": { "summary": "UpdateClusterVariable updates a cluster variable", "operationId": "ClusterParamService_UpdateClusterVariable", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "ClusterID is the ID of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterParamServiceUpdateClusterVariableBody" } } ], "tags": ["ClusterParamService"] } }, "/api/v2/clusters/{clusterId}:cancelTaskFlow": { "post": { "summary": "Retry cluster", "operationId": "ClusterService_CancelTaskFlow", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2CancelTaskFlowResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceCancelTaskFlowBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}:deployCluster": { "post": { "summary": "Deploy cluster", "operationId": "ClusterService_DeployCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2DeployClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceDeployClusterBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}:destroyCluster": { "post": { "summary": "DestroyCluster cluster", "operationId": "ClusterService_DestroyCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2DestroyClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceDestroyClusterBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}:offlineCluster": { "post": { "summary": "OfflineCluster cluster", "operationId": "ClusterService_OfflineCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2OfflineClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceOfflineClusterBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}:pauseCluster": { "post": { "summary": "PauseCluster cluster", "operationId": "ClusterService_PauseCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2PauseClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServicePauseClusterBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}:reloadCluster": { "post": { "summary": "ReloadCluster cluster", "operationId": "ClusterService_ReloadCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ReloadClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceReloadClusterBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}:restartCluster": { "post": { "summary": "StartCluster cluster", "operationId": "ClusterService_RestartCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2RestartClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceRestartClusterBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}:resumeCluster": { "post": { "summary": "ResumeCluster cluster", "operationId": "ClusterService_ResumeCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ResumeClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceResumeClusterBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}:retryTaskFlow": { "post": { "summary": "Retry cluster", "operationId": "ClusterService_RetryTaskFlow", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2RetryTaskFlowResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceRetryTaskFlowBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}:scaleInCluster": { "post": { "summary": "ScaleInCluster cluster", "operationId": "ClusterService_ScaleInCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ScaleInClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceScaleInClusterBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters/{clusterId}:scaleOutCluster": { "post": { "summary": "ScaleOut cluster", "operationId": "ClusterService_ScaleOutCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ScaleOutClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "clusterId", "description": "The cluster_id of the cluster", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterServiceScaleOutClusterBody" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters:clusterConfig": { "post": { "summary": "ClusterConfig", "operationId": "ClusterService_ClusterConfig", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterConfigResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2ClusterConfigRequest" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters:clusterVersions": { "get": { "summary": "ClusterVersions cluster", "operationId": "ClusterService_ClusterVersions", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ClusterVersionsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tidbVersion", "description": "The tidb_version", "in": "query", "required": true, "type": "string" }, { "name": "tiupId", "description": "The tiup_id", "in": "query", "required": true, "type": "string" } ], "tags": ["ClusterService"] } }, "/api/v2/clusters:configTemplate": { "post": { "summary": "ConfigTemplate cluster", "operationId": "ClusterService_ConfigTemplate", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ConfigTemplateResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2ConfigTemplateRequest" } } ], "tags": ["ClusterService"] } }, "/api/v2/clusters:download": { "get": { "summary": "HostConfirm one host", "operationId": "ClusterService_DownloadListClusters", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2DownloadListClustersResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "searchValue", "description": "The name of the user", "in": "query", "required": false, "type": "string" }, { "name": "tagIds", "description": "tag_ids", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" } ], "tags": ["ClusterService"] } }, "/api/v2/clusters:takeoverCluster": { "post": { "summary": "Takeover cluster", "operationId": "ClusterService_TakeoverCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TakeoverClusterResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2TakeoverClusterRequest" } } ], "tags": ["ClusterService"] } }, "/api/v2/cmservers": { "get": { "summary": "ListCMServers", "operationId": "CMServerService_ListCMServers", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListCMServersResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "searchValue", "description": "name or ip like", "in": "query", "required": false, "type": "string" } ], "tags": ["CMServerService"] }, "post": { "summary": "CreateCMServer", "operationId": "CMServerService_CreateCMServer", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2CMServer" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "cmserver", "description": "create cm server resource", "in": "body", "required": true, "schema": { "$ref": "#/definitions/cmserverv2CreateCMServer" } } ], "tags": ["CMServerService"] } }, "/api/v2/cmservers/{id}": { "delete": { "summary": "delete one host by host_id", "operationId": "CMServerService_DeleteCMServer", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "id", "description": "the CM Server id of the CMServer", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["CMServerService"] }, "patch": { "summary": "UpdateCMServer", "operationId": "CMServerService_UpdateCMServer", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2CMServer" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "id", "description": "The id of the CMServer", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/CMServerServiceUpdateCMServerBody" } } ], "tags": ["CMServerService"] } }, "/api/v2/cmservers/{id}/clusters": { "get": { "summary": "Get CMServerCluster", "operationId": "CMServerService_GetCMServerCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2CMServerClustersResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "id", "description": "the CMServer id of the CMServer", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["CMServerService"] } }, "/api/v2/cmservers/{id}:getWithTiup": { "get": { "summary": "get CMServer with tiup", "operationId": "CMServerService_GetCMServerWithTiup", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2GetCMServerWithTiupResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "id", "description": "the CM Server id of the CMServer", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["CMServerService"] } }, "/api/v2/cmservers/{tiupId}/clusters/{clusterName}": { "get": { "summary": "GetClusterTopology", "operationId": "CMServerService_GetClusterTopology", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2GetClusterTopologyResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tiupId", "description": "tiup_id", "in": "path", "required": true, "type": "string" }, { "name": "clusterName", "description": "the cluster_name", "in": "path", "required": true, "type": "string" } ], "tags": ["CMServerService"] } }, "/api/v2/credentials": { "get": { "summary": "List credentials", "operationId": "CredentialService_ListCredentials", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListCredentialsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "credentialType", "description": "the credential type of the credential\n\n - CREDENTIAL_TYPE_UNSPECIFIED: resource type unspecified\n - HOST: credential type host\n - TIDB: credential type tidb", "in": "query", "required": false, "type": "string", "enum": ["CREDENTIAL_TYPE_UNSPECIFIED", "HOST", "TIDB"], "default": "CREDENTIAL_TYPE_UNSPECIFIED" }, { "name": "credentialId", "description": "the credential id of the credential", "in": "query", "required": false, "type": "string" } ], "tags": ["CredentialService"] }, "post": { "summary": "Create credential", "operationId": "CredentialService_CreateCredential", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Credential" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "credential", "description": "\nthe credential resource", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2Credential" } } ], "tags": ["CredentialService"] } }, "/api/v2/credentials/{credentialId}": { "get": { "summary": "Get credential", "operationId": "CredentialService_GetCredential", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Credential" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "credentialId", "description": "the credential id of the credential", "in": "path", "required": true, "type": "string" } ], "tags": ["CredentialService"] }, "delete": { "summary": "Delete credential by credential id", "operationId": "CredentialService_DeleteCredential", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "credentialId", "description": "the credential id of the credential", "in": "path", "required": true, "type": "string" } ], "tags": ["CredentialService"] }, "patch": { "summary": "Update credential by credential id", "operationId": "CredentialService_UpdateCredential", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Credential" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "credentialId", "description": "the credential id of the credential", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/CredentialServiceUpdateCredentialBody" } } ], "tags": ["CredentialService"] } }, "/api/v2/credentials/{credentialId}:downloadRsaKey": { "get": { "summary": "Download credential public key and private key", "operationId": "CredentialService_DownloadRSAKey", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2DownloadRSAKeyResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "credentialId", "description": "the credential id of the credential", "in": "path", "required": true, "type": "string" } ], "tags": ["CredentialService"] } }, "/api/v2/credentials:generateRsaKey": { "post": { "summary": "Generate credential public key and private key", "operationId": "CredentialService_GenerateRSAKey", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2GenerateRSAKeyResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2GenerateRSAKeyRequest" } } ], "tags": ["CredentialService"] } }, "/api/v2/credentials:validateConnection": { "post": { "summary": "Validate credential is accessible", "operationId": "CredentialService_ValidateConnection", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ValidateConnectionResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2ValidateConnectionRequest" } } ], "tags": ["CredentialService"] } }, "/api/v2/domains": { "get": { "summary": "List domains", "operationId": "DomainService_ListDomains", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListDomainsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" } ], "tags": ["DomainService"] }, "post": { "summary": "Create domain", "operationId": "DomainService_CreateDomain", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Domain" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "domain", "description": "the domain with basic resource", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2CreateDomainBody" } } ], "tags": ["DomainService"] } }, "/api/v2/domains/{id}": { "get": { "summary": "Get domain", "operationId": "DomainService_GetDomain", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Domain" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "id", "description": "the domain id of the domain", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["DomainService"] }, "delete": { "summary": "Delete domain by domain id", "operationId": "DomainService_DeleteDomain", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "id", "description": "the domain id of the domain", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["DomainService"] }, "patch": { "summary": "Update domain basic info by domain id", "operationId": "DomainService_UpdateDomain", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Domain" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "id", "description": "the domain id of the domain", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/DomainServiceUpdateDomainBody" } } ], "tags": ["DomainService"] } }, "/api/v2/domains/{id}:getWithCMServer": { "get": { "summary": "Get domain", "operationId": "DomainService_GetDomainWithCMServer", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2DomainWithCMServer" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "id", "description": "the domain id of the domain", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["DomainService"] } }, "/api/v2/domains:getWithCMServer": { "get": { "summary": "List domains with cm server", "operationId": "DomainService_ListDomainsWithCMServer", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListDomainsWithCMServerResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" } ], "tags": ["DomainService"] } }, "/api/v2/hosts": { "get": { "summary": "ListHosts", "operationId": "HostService_ListHosts", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListHostsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "searchValue", "description": "The name of the user", "in": "query", "required": false, "type": "string" }, { "name": "locationIds", "description": "location_ids", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" }, { "name": "tagIds", "description": "tag_ids", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" } ], "tags": ["HostService"] }, "post": { "summary": "CreateHosts", "operationId": "HostService_CreateHosts", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2HostCreateResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "host", "description": "\nhost resource", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2CreateHost" } } ], "tags": ["HostService"] } }, "/api/v2/hosts/import/tasks": { "post": { "consumes": ["multipart/form-data"], "description": "Upload a csv form data to host.", "operationId": "HostService_Import", "parameters": [ { "description": "Upload a csv form data to host.", "format": "binary", "in": "formData", "name": "hostData", "required": true, "type": "file" }, { "description": "The Credential_Id of the Import", "in": "formData", "name": "credentialId", "required": true, "type": "string" } ], "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ImportTaskResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "summary": "import host", "tags": ["HostService"] } }, "/api/v2/hosts/import/tasks/{taskId}": { "get": { "summary": "Import one host", "operationId": "HostService_ImportTask", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ImportTaskResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "taskId", "description": "task_id", "in": "path", "required": true, "type": "string" } ], "tags": ["HostService"] } }, "/api/v2/hosts/import/tasks/{taskId}:confirm": { "post": { "summary": "HostConfirm one host", "operationId": "HostService_HostConfirm", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ConfirmResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "taskId", "description": "task_id", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/HostServiceHostConfirmBody" } } ], "tags": ["HostService"] } }, "/api/v2/hosts/{hostId}": { "get": { "summary": "Get", "operationId": "HostService_GetHost", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Host" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "hostId", "description": "The host_id of the host", "in": "path", "required": true, "type": "string" } ], "tags": ["HostService"] }, "delete": { "summary": "delete one host by host_id", "operationId": "HostService_Delete", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "hostId", "description": "The host_id of the host", "in": "path", "required": true, "type": "string" } ], "tags": ["HostService"] }, "patch": { "summary": "update one host by host_id", "operationId": "HostService_UpdateHost", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Host" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "hostId", "description": "The host_id of the Host", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2HostServiceUpdateHostBody" } } ], "tags": ["HostService"] } }, "/api/v2/hosts/{hostId}/disks": { "get": { "summary": "GetDisks", "operationId": "HostService_GetDisks", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2HostDiskResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "hostId", "description": "The host_id of the host", "in": "path", "required": true, "type": "string" } ], "tags": ["HostService"] } }, "/api/v2/hosts/{hostId}/metrics/{name}/data": { "get": { "summary": "Get host metric data", "operationId": "MetricsService_GetHostMetricData", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2HostMetricData" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "hostId", "description": "Cluster ID", "in": "path", "required": true, "type": "string" }, { "name": "name", "description": "Metric Name", "in": "path", "required": true, "type": "string" }, { "name": "startTime", "description": "Start time in Unix timestamp format", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "endTime", "description": "End time in Unix timestamp format", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "step", "description": "Step time in seconds", "in": "query", "required": false, "type": "string", "format": "int64" }, { "name": "label", "description": "Line Label for the metric", "in": "query", "required": false, "type": "string" }, { "name": "range", "description": "Time Range for the query", "in": "query", "required": false, "type": "string" } ], "tags": ["MetricsService"] } }, "/api/v2/hosts/{hostId}/report/{reportId}": { "get": { "summary": "Report", "operationId": "HostService_Report", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ReportResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "hostId", "description": "The host_id of the host", "in": "path", "required": true, "type": "string" }, { "name": "reportId", "description": "The report_id of the host", "in": "path", "required": true, "type": "string" } ], "tags": ["HostService"] } }, "/api/v2/hosts/{hostId}/tidbProcesses": { "get": { "summary": "GetInstances", "operationId": "HostService_GetTiDBProcesses", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2HostTiDBProcessesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "hostId", "description": "The host_id of the host", "in": "path", "required": true, "type": "string" } ], "tags": ["HostService"] } }, "/api/v2/hosts/{hostId}:fix": { "post": { "summary": "Fix", "operationId": "HostService_Fix", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2HostFixResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "hostId", "description": "The host_id of the host", "in": "path", "required": true, "type": "string" } ], "tags": ["HostService"] } }, "/api/v2/hosts/{hostId}:systemCheck": { "post": { "summary": "Check", "operationId": "HostService_Check", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2HostCheckResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "hostId", "description": "The host_id of the host", "in": "path", "required": true, "type": "string" } ], "tags": ["HostService"] } }, "/api/v2/hosts:batchDelete": { "post": { "summary": "delete one host by host_id", "operationId": "HostService_BatchDelete", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2BatchDeleteRequest" } } ], "tags": ["HostService"] } }, "/api/v2/hosts:download": { "get": { "summary": "HostConfirm one host", "operationId": "HostService_DownloadListHosts", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2DownloadListHostResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "searchValue", "description": "The name of the user", "in": "query", "required": false, "type": "string" }, { "name": "locationIds", "description": "location_ids", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" }, { "name": "tagIds", "description": "tag_ids", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" } ], "tags": ["HostService"] } }, "/api/v2/hosts:downloadHostTemplate": { "get": { "summary": "DownloadHostTemplate one host", "operationId": "HostService_DownloadHostTemplate", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2DownloadHostTemplateResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["HostService"] } }, "/api/v2/license": { "get": { "summary": "GetLicense returns the license details", "operationId": "LicenseService_GetLicense", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/licensev2License" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["LicenseService"] } }, "/api/v2/license/devicecode": { "get": { "summary": "GetDeviceCode returns the device code to help activate the license", "operationId": "LicenseService_GetDeviceCode", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2DeviceCode" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["LicenseService"] } }, "/api/v2/license:activate": { "post": { "consumes": ["multipart/form-data"], "description": "Upload a license using form data to activate.", "operationId": "LicenseService_ActivateLicense", "parameters": [ { "description": "The content of the license file\n\nThe license file to upload to activate the license", "format": "binary", "in": "formData", "name": "license", "required": true, "type": "file" } ], "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/licensev2License" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "summary": "Activate a license", "tags": ["LicenseService"] } }, "/api/v2/license:trial": { "post": { "summary": "ActivateFreeLicense activate the embedded free license", "operationId": "LicenseService_ActivateFreeLicense", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/licensev2License" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["LicenseService"] } }, "/api/v2/locations": { "get": { "summary": "list location", "operationId": "LocationService_ListLocations", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListLocationsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "locationKey", "description": "location key (e.g., \"zone\", \"dc\")", "in": "query", "required": false, "type": "string", "enum": ["zone", "dc", "rack"] }, { "name": "locationValue", "description": "the Location value of the Location", "in": "query", "required": false, "type": "string" }, { "name": "parentId", "description": "the Location parent_Id of the Location", "in": "query", "required": false, "type": "string" } ], "tags": ["LocationService"] }, "post": { "summary": "create CreateLocationRequest", "operationId": "LocationService_CreateLocations", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Locations" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "location", "description": "the Location basic resource", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2Locations" } } ], "tags": ["LocationService"] } }, "/api/v2/locations/{locationId}": { "get": { "summary": "get Location", "operationId": "LocationService_GetLocations", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Locations" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "locationId", "description": "the Location id of the Location", "in": "path", "required": true, "type": "string" } ], "tags": ["LocationService"] }, "delete": { "summary": "delete Location by Location id", "operationId": "LocationService_DeleteLocation", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "locationId", "description": "the Location id of the Location", "in": "path", "required": true, "type": "string" } ], "tags": ["LocationService"] }, "patch": { "summary": "update Location basic info by Location id", "operationId": "LocationService_UpdateLocations", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Locations" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "locationId", "description": "the Location id of the Location", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/LocationServiceUpdateLocationsBody" } } ], "tags": ["LocationService"] } }, "/api/v2/login": { "post": { "summary": "Login allows a user to log in and start a session.", "operationId": "UserService_Login", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2LoginRequest" } } ], "tags": ["UserService"] } }, "/api/v2/logout": { "post": { "summary": "Logout allows a user to log out and end their session.", "operationId": "UserService_Logout", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["UserService"] } }, "/api/v2/metrics": { "get": { "summary": "Get metrics info", "operationId": "MetricsService_GetMetrics", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Metrics" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "class", "description": "Level 1 classification\n\n - unspecified: Unspecified\n - cluster: Cluster metrics\n - host: Host metrics\n - overview: Overview metrics", "in": "query", "required": false, "type": "string", "enum": ["unspecified", "cluster", "host", "overview"], "default": "unspecified" }, { "name": "group", "description": "Level 2 grouping\n\n - unspecified: Unspecified group\n - overview: Overview group\n - basic: Basic group\n - advanced: Advanced group\n - resource: Resource group\n - performance: Performance group\n - process: Process group", "in": "query", "required": false, "type": "string", "enum": [ "unspecified", "overview", "basic", "advanced", "resource", "performance", "process" ], "default": "unspecified" }, { "name": "type", "description": "Level 3 type", "in": "query", "required": false, "type": "string" }, { "name": "name", "description": "The metric name", "in": "query", "required": false, "type": "string" } ], "tags": ["MetricsService"] } }, "/api/v2/overview/metrics/config": { "get": { "summary": "Get top metric config", "operationId": "MetricsService_GetTopMetricConfig", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TopMetricConfig" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["MetricsService"] } }, "/api/v2/overview/metrics/{name}/data": { "get": { "summary": "Get top metric data", "operationId": "MetricsService_GetTopMetricData", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TopMetricData" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "name", "description": "Metric name to query", "in": "path", "required": true, "type": "string" }, { "name": "startTime", "description": "Start time for the query", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "endTime", "description": "End time for the query", "in": "query", "required": true, "type": "string", "format": "int64" }, { "name": "step", "description": "Step time for the query", "in": "query", "required": false, "type": "string", "format": "int64" }, { "name": "limit", "description": "Limit for the number of top results", "in": "query", "required": false, "type": "string", "format": "int64" } ], "tags": ["MetricsService"] } }, "/api/v2/overview/status": { "get": { "summary": "Get overview status", "operationId": "MetricsService_GetOverviewStatus", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2OverviewStatus" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "taskStartTime", "description": "Task start time in Unix timestamp format", "in": "query", "required": false, "type": "string", "format": "int64" }, { "name": "taskEndTime", "description": "Task end time in Unix timestamp format", "in": "query", "required": false, "type": "string", "format": "int64" } ], "tags": ["MetricsService"] } }, "/api/v2/roles": { "get": { "summary": "ListRoles retrieves a list of roles.", "operationId": "RoleService_ListRoles", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListRolesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "roleNameLike", "description": "role_name_like", "in": "query", "required": false, "type": "string" }, { "name": "roleName", "description": "The name of the role", "in": "query", "required": false, "type": "string" } ], "tags": ["RoleService"] }, "post": { "summary": "CreateRole creates a new role.", "operationId": "RoleService_CreateRole", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Role" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "role", "description": "\nUser resource", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2Role" } } ], "tags": ["RoleService"] } }, "/api/v2/roles/{roleId}": { "delete": { "summary": "DeleteRole deletes a role by role ID.", "operationId": "RoleService_DeleteRole", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "roleId", "description": "The id of the role", "in": "path", "required": true, "type": "integer", "format": "int32" } ], "tags": ["RoleService"] }, "patch": { "summary": "UpdateRole updates a role by role ID.", "operationId": "RoleService_UpdateRole", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Role" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "roleId", "description": "The id of the role", "in": "path", "required": true, "type": "integer", "format": "int32" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/RoleServiceUpdateRoleBody" } } ], "tags": ["RoleService"] } }, "/api/v2/tags": { "get": { "summary": "List tags", "operationId": "TagService_ListTags", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListTagsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" } ], "tags": ["TagService"] }, "post": { "summary": "Create tag", "operationId": "TagService_CreateTag", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/tagv2Tag" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tag", "description": "the tag with basic resource", "in": "body", "required": true, "schema": { "$ref": "#/definitions/tagv2Tag" } } ], "tags": ["TagService"] } }, "/api/v2/tags/{tagId}": { "get": { "summary": "Get tag", "operationId": "TagService_GetTag", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/tagv2Tag" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tagId", "description": "the tag id of the tag", "in": "path", "required": true, "type": "string" } ], "tags": ["TagService"] }, "delete": { "summary": "Delete tag by tag id", "operationId": "TagService_DeleteTag", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tagId", "description": "the tag id of the tag", "in": "path", "required": true, "type": "string" } ], "tags": ["TagService"] }, "patch": { "summary": "Update tag basic info by tag id", "operationId": "TagService_UpdateTag", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/tagv2Tag" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tagId", "description": "the tag id of the tag", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/TagServiceUpdateTagBody" } } ], "tags": ["TagService"] } }, "/api/v2/tags/{tagId}:getWithBindings": { "get": { "summary": "Get tag with bindings", "operationId": "TagService_GetTagWithBindings", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2GetTagWithBindingsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tagId", "description": "the tag id of the tag", "in": "path", "required": true, "type": "string" } ], "tags": ["TagService"] } }, "/api/v2/tags:batchCreate": { "post": { "summary": "Batch create tags", "operationId": "TagService_BatchCreateTags", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BatchCreateTagsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2BatchCreateTagsRequest" } } ], "tags": ["TagService"] } }, "/api/v2/tags:bindResource": { "post": { "summary": "Modify bind object by resource id", "operationId": "TagService_BindResource", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BindResourceResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2BindResourceRequest" } } ], "tags": ["TagService"] } }, "/api/v2/tags:bindTag": { "post": { "summary": "Modify bind object by tag id", "operationId": "TagService_BindTag", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2BindTagResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2BindTagRequest" } } ], "tags": ["TagService"] } }, "/api/v2/tags:listByResourceType": { "get": { "summary": "List tags by resource type", "operationId": "TagService_ListTagsByResourceType", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListTagsByResourceTypeResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "tagKey", "description": "the tag key which the tag values belong to", "in": "query", "required": false, "type": "string" }, { "name": "keyword", "description": "the keyword which tag values similar to", "in": "query", "required": false, "type": "string" }, { "name": "resourceType", "description": "the resource type of the tag has bound with\n\n - TAG_BIND_RESOURCE_TYPE_UNSPECIFIED: resource type unspecified\n - HOST: resource type host\n - TIUP: resource type tiup\n - CLUSTER: resource type cluster\n - CM_SERVER: resource type cm server", "in": "query", "required": false, "type": "string", "enum": [ "TAG_BIND_RESOURCE_TYPE_UNSPECIFIED", "HOST", "TIUP", "CLUSTER", "CM_SERVER" ], "default": "TAG_BIND_RESOURCE_TYPE_UNSPECIFIED" } ], "tags": ["TagService"] } }, "/api/v2/tags:listKeys": { "get": { "summary": "List tag keys", "operationId": "TagService_ListTagKeys", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListTagKeysResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "keyword", "description": "the keyword which tag key similar to", "in": "query", "required": false, "type": "string" }, { "name": "resourceType", "description": "the resource type of the tag has bound with\n\n - TAG_BIND_RESOURCE_TYPE_UNSPECIFIED: resource type unspecified\n - HOST: resource type host\n - TIUP: resource type tiup\n - CLUSTER: resource type cluster\n - CM_SERVER: resource type cm server", "in": "query", "required": false, "type": "string", "enum": [ "TAG_BIND_RESOURCE_TYPE_UNSPECIFIED", "HOST", "TIUP", "CLUSTER", "CM_SERVER" ], "default": "TAG_BIND_RESOURCE_TYPE_UNSPECIFIED" } ], "tags": ["TagService"] } }, "/api/v2/tags:listWithBindings": { "get": { "summary": "List tags with bindings", "operationId": "TagService_ListTagsWithBindings", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListTagsWithBindingsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "tagKeys", "description": "the tag which the tag key in", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" }, { "name": "tagValueLike", "description": "the tag which the tag value like", "in": "query", "required": false, "type": "string" } ], "tags": ["TagService"] } }, "/api/v2/taskflows": { "get": { "summary": "ListTasks retrieves a list of tasks.", "operationId": "TaskFlowService_ListTaskFlows", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListTaskFlowsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "The number of tasks to retrieve per page.", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Pagination token for retrieving the next page of tasks.", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "The number of tasks to skip for pagination purposes.", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "The sorting criteria for the task list.", "in": "query", "required": false, "type": "string" }, { "name": "gtStartTime", "description": "Filter tasks by start_time gt.", "in": "query", "required": false, "type": "string", "format": "date-time" }, { "name": "ltStartTime", "description": "Filter tasks by start_time lt.", "in": "query", "required": false, "type": "string", "format": "date-time" }, { "name": "parentId", "description": "Filter tasks by parent_id.", "in": "query", "required": false, "type": "string" }, { "name": "status", "description": "Filter tasks by status.", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" }, { "name": "taskId", "description": "The unique identifier of the task.", "in": "query", "required": false, "type": "string" }, { "name": "taskIdLike", "description": "The unique identifier of the task ,fuzzy query.", "in": "query", "required": false, "type": "string" }, { "name": "templateId", "description": "The template_idr of the task.", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" } ], "tags": ["TaskFlowService"] } }, "/api/v2/taskflows/{taskId}": { "get": { "summary": "GetTask retrieves a task by task ID.", "operationId": "TaskFlowService_GetTaskFlow", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TaskFlowDetail" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "taskId", "description": "The unique identifier of the task.", "in": "path", "required": true, "type": "string" } ], "tags": ["TaskFlowService"] } }, "/api/v2/taskflows/{taskId}/{nodeKey}": { "get": { "summary": "GetTask retrieves a task by task ID.", "operationId": "TaskFlowService_GetTaskFlowInfo", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TaskFlowInfoResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "taskId", "description": "The task_id of the task.", "in": "path", "required": true, "type": "string" }, { "name": "nodeKey", "description": "The node_key of the task.", "in": "path", "required": true, "type": "string" } ], "tags": ["TaskFlowService"] } }, "/api/v2/taskflows/{taskId}:restartTaskFlow": { "get": { "summary": "RestartTaskFlow", "operationId": "TaskFlowService_RestartTaskFlow", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2RestartTaskFlowResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "taskId", "description": "The task_id of the task.", "in": "path", "required": true, "type": "string" } ], "tags": ["TaskFlowService"] } }, "/api/v2/tidbParamTemplates": { "get": { "summary": "ListParameterTemplates retrieves a list of parameter templates", "operationId": "ClusterParamTemplateService_ListParameterTemplates", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListParameterTemplatesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" } ], "tags": ["ClusterParamTemplateService"] }, "post": { "summary": "CreateParameterTemplate creates a new parameter template", "operationId": "ClusterParamTemplateService_CreateParameterTemplate", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2CreateParameterTemplateRequest" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2CreateParameterTemplateRequest" } } ], "tags": ["ClusterParamTemplateService"] } }, "/api/v2/tidbParamTemplates/params": { "get": { "summary": "ListParameters retrieves a list of parameters", "operationId": "ClusterParamTemplateService_ListParameters", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListParametersResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "Page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "version", "description": "version", "in": "query", "required": false, "type": "string" }, { "name": "name", "description": "The name of the parameter", "in": "query", "required": false, "type": "string" }, { "name": "configSource", "description": "The parameter instance type. e.g.: CLUSTER_CONFIG, GLOBAL_VARIABLES", "in": "query", "required": false, "type": "string" }, { "name": "instanceType", "description": "The parameter instance type. e.g.: TiDB, TiKV, PD", "in": "query", "required": false, "type": "string" } ], "tags": ["ClusterParamTemplateService"] } }, "/api/v2/tidbParamTemplates/params/versions": { "get": { "summary": "GetSupportVersions retrieves a list of supported versions", "operationId": "ClusterParamTemplateService_GetSupportVersions", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2SupportVersions" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["ClusterParamTemplateService"] } }, "/api/v2/tidbParamTemplates/unsupportedParams:check": { "post": { "summary": "CheckUnsupportedParams checks if the parameters are supported in the specified version", "operationId": "ClusterParamTemplateService_CheckUnsupportedParams", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2CheckUnsupportedParamsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2CheckUnsupportedParamsRequest" } } ], "tags": ["ClusterParamTemplateService"] } }, "/api/v2/tidbParamTemplates/{templateId}": { "get": { "summary": "GetParameterTemplate retrieves a parameter template by ID", "operationId": "ClusterParamTemplateService_GetParameterTemplate", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2GetParameterTemplateResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "templateId", "description": "The ID of the parameter template", "in": "path", "required": true, "type": "string", "format": "int64" } ], "tags": ["ClusterParamTemplateService"] }, "delete": { "summary": "DeleteParameterTemplate deletes a parameter template by ID", "operationId": "ClusterParamTemplateService_DeleteParameterTemplate", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "templateId", "description": "The ID of the parameter template", "in": "path", "required": true, "type": "string", "format": "int64" } ], "tags": ["ClusterParamTemplateService"] }, "put": { "summary": "UpdateParameterTemplate updates a parameter template", "operationId": "ClusterParamTemplateService_UpdateParameterTemplate", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2UpdateParameterTemplateRequest" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "templateId", "description": "The ID of the parameter template", "in": "path", "required": true, "type": "string", "format": "int64" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ClusterParamTemplateServiceUpdateParameterTemplateBody" } } ], "tags": ["ClusterParamTemplateService"] } }, "/api/v2/tidbParamTemplates/{templateId}/{paramId}": { "delete": { "summary": "DeleteParameter deletes a parameter by ID", "operationId": "ClusterParamTemplateService_DeleteParameter", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "templateId", "description": "The ID of the parameter template", "in": "path", "required": true, "type": "string", "format": "int64" }, { "name": "paramId", "description": "The ID of the parameter", "in": "path", "required": true, "type": "string", "format": "int64" } ], "tags": ["ClusterParamTemplateService"] } }, "/api/v2/tiups": { "get": { "summary": "list Tiups", "operationId": "TiupsService_ListTiups", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListTiupsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "page size", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "page token", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "Skip", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "order_by", "in": "query", "required": false, "type": "string" }, { "name": "searchValue", "description": "the Tiups key of the Tiups", "in": "query", "required": false, "type": "string" }, { "name": "tagIds", "description": "the Tiups tag_ids of the tagIds", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" }, { "name": "hostIds", "description": "the Tiups host_ids of the tagIds", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" } ], "tags": ["TiupsService"] }, "post": { "summary": "create Tiups", "operationId": "TiupsService_CreateTiups", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Tiups" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tiups", "description": "the Tiups basic resource", "in": "body", "required": true, "schema": { "$ref": "#/definitions/tiupv2CreateTiups" } } ], "tags": ["TiupsService"] } }, "/api/v2/tiups/{tiupId}": { "get": { "summary": "get Tiups", "operationId": "TiupsService_GetTiups", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Tiups" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tiupId", "description": "the Tiups id of the Tiups", "in": "path", "required": true, "type": "string" } ], "tags": ["TiupsService"] }, "delete": { "summary": "delete Tiups by Tiups id", "operationId": "TiupsService_DeleteTiups", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tiupId", "description": "the Tiups id of the Tiups", "in": "path", "required": true, "type": "string" } ], "tags": ["TiupsService"] }, "patch": { "summary": "update Tiups basic info by Tiups id", "operationId": "TiupsService_UpdateTiups", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2Tiups" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tiupId", "description": "the tiup_id id of the Tiups", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2TiupsServiceUpdateTiupsBody" } } ], "tags": ["TiupsService"] } }, "/api/v2/tiups/{tiupId}/clusters": { "get": { "summary": "Get TiupsCluster", "operationId": "TiupsService_GetTiupsCluster", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2TiupsClustersResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tiupId", "description": "the Tiups id of the Tiups", "in": "path", "required": true, "type": "string" } ], "tags": ["TiupsService"] } }, "/api/v2/tiups/{tiupId}/clusters/{clusterName}": { "get": { "summary": "GetClusterTopology", "operationId": "TiupsService_GetClusterTopology", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2GetClusterTopologyResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tiupId", "description": "tiup_id", "in": "path", "required": true, "type": "string" }, { "name": "clusterName", "description": "the cluster_name", "in": "path", "required": true, "type": "string" } ], "tags": ["TiupsService"] } }, "/api/v2/tiups/{tiupId}/tidbVersions": { "get": { "summary": "GetTidbVersions", "operationId": "TiupsService_GetTidbVersions", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2GetTidbVersionsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "tiupId", "description": "tiup_id", "in": "path", "required": true, "type": "string" } ], "tags": ["TiupsService"] } }, "/api/v2/users": { "get": { "summary": "ListUsers retrieves a list of users.", "operationId": "UserService_ListUsers", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ListUsersResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "pageSize", "description": "The number of users to retrieve per page.", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "pageToken", "description": "Pagination token for retrieving the next page of users.", "in": "query", "required": false, "type": "string" }, { "name": "skip", "description": "The number of users to skip for pagination purposes.", "in": "query", "required": false, "type": "integer", "format": "int32" }, { "name": "orderBy", "description": "The sorting criteria for the user list.", "in": "query", "required": false, "type": "string" }, { "name": "nameLike", "description": "Filter users by username using a \"like\" operation.", "in": "query", "required": false, "type": "string" }, { "name": "emailLike", "description": "Filter users by email using a \"like\" operation.", "in": "query", "required": false, "type": "string" }, { "name": "roleName", "description": "Filter users by role name.", "in": "query", "required": false, "type": "string" } ], "tags": ["UserService"] }, "post": { "summary": "CreateUser creates a new user.", "operationId": "UserService_CreateUser", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2User" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "user", "description": "\nUser resource", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2User" } } ], "tags": ["UserService"] } }, "/api/v2/users/profile": { "get": { "summary": "GetUserProfile retrieves the profile information of the authenticated user.", "operationId": "UserService_GetUserProfile", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2UserProfile" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["UserService"] } }, "/api/v2/users/{userId}": { "get": { "summary": "GetUser retrieves a user by user ID.", "operationId": "UserService_GetUser", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2User" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "userId", "description": "The user_id of the user", "in": "path", "required": true, "type": "string" } ], "tags": ["UserService"] }, "delete": { "summary": "DeleteUser deletes a user by user ID.", "operationId": "UserService_DeleteUser", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "userId", "description": "The id of the user", "in": "path", "required": true, "type": "string" } ], "tags": ["UserService"] }, "patch": { "summary": "UpdateUser updates a user's information by user ID.", "operationId": "UserService_UpdateUser", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2User" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "userId", "description": "The unique user ID of the user.", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/UserServiceUpdateUserBody" } } ], "tags": ["UserService"] } }, "/api/v2/users/{userId}:resetPassword": { "patch": { "summary": "ResetPassword allows an admin user to reset the password of another user.", "operationId": "UserService_ResetPassword", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "userId", "description": "The id of the user", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/UserServiceResetPasswordBody" } } ], "tags": ["UserService"] } }, "/api/v2/users:changePassword": { "patch": { "summary": "ChangePassword allows the authenticated user to change their password.", "operationId": "UserService_ChangePassword", "responses": { "200": { "description": "A successful response.", "schema": { "type": "object", "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/v2ChangePasswordRequest" } } ], "tags": ["UserService"] } }, "/api/v2/users:validateSession": { "get": { "summary": "ValidateSession verifies the validity of the current session.", "operationId": "UserService_ValidateSession", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ValidateSessionResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["UserService"] } }, "/documentation/errorDetail": { "get": { "summary": "GetTemErrorDetail", "operationId": "ApiKeyService_GetTemErrorDetail", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/v2ErrorDetail" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/rpcStatus" } } }, "tags": ["ApiKeyService"] } } }, "definitions": { "AlertServiceSilenceEventBody": { "type": "object", "properties": { "silenceStartTime": { "type": "string", "format": "date-time", "title": "Silence start time" }, "silenceEndTime": { "type": "string", "format": "date-time", "title": "Silence end time" } }, "title": "SilenceAlertRequest represents a request to silence an alert", "required": ["silenceStartTime", "silenceEndTime"] }, "AlertServiceUpdateChannelBody": { "type": "object", "properties": { "type": { "type": "string", "enum": ["email", "webhook"], "title": "The channel type" }, "emailConfig": { "$ref": "#/definitions/v2EmailConfig", "title": "Email configuration, required if type is EMAIL" }, "webhookConfig": { "$ref": "#/definitions/v2WebhookConfig", "title": "Webhook configuration, required if type is WEBHOOK" }, "channelMonitorObjects": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ChannelMonitorObject" }, "title": "Channel Object List" }, "updateChannelObjects": { "type": "boolean", "title": "Whether to update monitor object associations. If true, existing associations will be replaced with the provided monitor_objects. If false, monitor object associations will remain unchanged" } }, "title": "UpdateAlertChannelRequest represents a request to update an alert channel" }, "AlertServiceUpdateMonitorObjectBody": { "type": "object", "properties": { "application": { "type": "string", "title": "The application of the monitor object" }, "objectType": { "type": "string", "title": "The type of the monitor object" } }, "title": "UpdateMonitorObjectRequest defines the request for updating an existing monitor object" }, "AlertServiceUpdateObjectRuleBody": { "type": "object", "properties": { "name": { "type": "string", "title": "The name of the rule" }, "expr": { "type": "string", "title": "The Prometheus expression" }, "status": { "type": "string", "enum": ["enabled", "disabled"], "title": "The rule status" }, "duration": { "type": "integer", "format": "int32", "title": "The duration in seconds" }, "labels": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The labels" }, "annotations": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The annotations" }, "level": { "type": "string", "enum": ["warning", "critical", "emergency"], "title": "The alert level" } }, "title": "UpdateObjectRuleRequest defines the request for updating an existing object rule" }, "AlertServiceUpdateRuleBody": { "type": "object", "properties": { "metricId": { "type": "integer", "format": "int32", "title": "The metric ID" }, "monitorObjectId": { "type": "integer", "format": "int32", "title": "The id of monitor object" }, "application": { "type": "string", "title": "The application of monitor object" }, "duration": { "type": "integer", "format": "int32", "title": "The duration in seconds" }, "labels": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The labels" }, "annotations": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The annotations" } }, "title": "UpdateRuleRequest defines the request for updating an existing rule" }, "AlertServiceUpdateTemplateBody": { "type": "object", "properties": { "name": { "type": "string", "title": "Template name" }, "tidbVersion": { "type": "string", "title": "TiDB version this template applies to" }, "description": { "type": "string", "title": "Template description" } }, "title": "UpdateAlertTemplateRequest represents a request to update an alert template" }, "AlertServiceUpdateTemplateRuleBody": { "type": "object", "properties": { "duration": { "type": "integer", "format": "int32", "title": "Rule to update" }, "labels": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The labels of the Prometheus rule" }, "annotations": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The annotations of the Prometheus rule" }, "level": { "type": "string", "enum": ["warning", "critical", "emergency"], "title": "The alert level" } }, "title": "UpdateTemplateRuleRequest represents a request to update a rule in a template" }, "ApiKeyServiceUpdateApiKeyBody": { "type": "object", "properties": { "secretKey": { "type": "string", "title": "The secret_key of apikey" }, "creator": { "type": "string", "title": "The creator of apikey" }, "status": { "type": "string", "enum": ["disable", "enable"], "title": "The apikey status" }, "description": { "type": "string", "title": "The description of apikey" } }, "title": "UpdateApikeyRequest Request" }, "CMServerServiceUpdateCMServerBody": { "type": "object", "properties": { "sshPort": { "type": "integer", "format": "int32", "title": "The SSHPort of the CMServer Host" }, "credentialId": { "type": "string", "title": "The Credential_Id of the CMServer Host" }, "serverName": { "type": "string", "title": "The CMServer name of the CMServer Host" }, "tagIds": { "type": "array", "items": { "type": "string" }, "title": "The tagIds of the CMServer Host" }, "servicePort": { "type": "integer", "format": "int32", "title": "The proxy port of the CMServer Host" } }, "title": "UpdateCMServerRequest", "required": ["serverName", "servicePort"] }, "ClusterBRServiceCreateBackupTaskBody": { "type": "object", "properties": { "name": { "type": "string", "title": "Name of the task" }, "destination": { "type": "string", "title": "Destination of the task or source for restore" }, "accessKeyId": { "type": "string", "title": "Access key ID for the task" }, "secretAccessKey": { "type": "string", "title": "Secret access key for the task" }, "rateLimit": { "type": "integer", "format": "int32", "title": "Rate limit for the task" }, "concurrency": { "type": "integer", "format": "int32", "title": "Concurrency for the task" }, "logFile": { "type": "string", "title": "Log file for the task" }, "retention": { "type": "integer", "format": "int32", "title": "Retention for the task" } }, "title": "CreateBackupTaskRequest represents the request to create a backup task", "required": ["destination"] }, "ClusterBRServiceCreateRestoreTaskBody": { "type": "object", "properties": { "targetClusterId": { "type": "string", "title": "Target cluster ID, only for restore" }, "type": { "$ref": "#/definitions/v2ClusterBRTypeEnumData", "title": "Type of the task, only restore by file and restore by time allowed" }, "backupTaskId": { "type": "string", "title": "Backup task ID" }, "restoreTime": { "type": "string", "title": "Restore time for the task" }, "destination": { "type": "string", "title": "Destination of the task or source for restore" }, "accessKeyId": { "type": "string", "title": "Access key ID for the task" }, "secretAccessKey": { "type": "string", "title": "Secret access key for the task" }, "rateLimit": { "type": "integer", "format": "int32", "title": "Rate limit for the task" }, "concurrency": { "type": "integer", "format": "int32", "title": "Concurrency for the task" }, "logFile": { "type": "string", "title": "Log file for the task" } }, "title": "CreateRestoreTaskRequest represents the request to create a br task", "required": ["targetClusterId"] }, "ClusterParamServiceUpdateClusterConfigBody": { "type": "object", "properties": { "instanceType": { "type": "string", "title": "InstanceType is the instance type of the parameter" }, "instance": { "type": "string", "title": "Instance is the instance of the parameter" }, "name": { "type": "string", "title": "Name is the name of the parameter" }, "value": { "type": "string", "title": "Value is the value of the parameter" }, "oldValue": { "type": "string", "title": "OldValue is the old value of the parameter" }, "resetDefault": { "type": "boolean", "title": "ResetDefault is if the parameter will be reset to default" } }, "title": "UpdateClusterConfigRequest is the request message for UpdateClusterConfig", "required": [ "instanceType", "instance", "name", "value", "oldValue", "resetDefault" ] }, "ClusterParamServiceUpdateClusterVariableBody": { "type": "object", "properties": { "name": { "type": "string", "title": "Name is the name of the parameter variable" }, "value": { "type": "string", "title": "Value is the value of the parameter variable" }, "oldValue": { "type": "string", "title": "OldValue is the old value of the parameter variable" } }, "title": "UpdateClusterVariableRequest is the request message for UpdateClusterVariable", "required": ["name", "value", "oldValue"] }, "ClusterParamTemplateServiceUpdateParameterTemplateBody": { "type": "object", "properties": { "parameterTemplate": { "$ref": "#/definitions/v2ParameterTemplate", "title": "The parameter template" }, "templateParameterMappings": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TemplateParameterMapping" }, "title": "The mappings of the parameter template" } }, "title": "UpdateParameterTemplateRequest represents an update parameter template request", "required": ["parameterTemplate", "templateParameterMappings"] }, "ClusterServiceBatchPauseBody": { "type": "object", "properties": { "instanceId": { "type": "array", "items": { "type": "string" }, "title": "The instance_id of the cluster" }, "concurrency": { "type": "integer", "format": "int32", "title": "concurrency" }, "waitTime": { "type": "integer", "format": "int32", "title": "wait_time" } }, "title": "BatchPauseRequest", "required": ["instanceId"] }, "ClusterServiceBatchReloadBody": { "type": "object", "properties": { "instanceId": { "type": "array", "items": { "type": "string" }, "title": "The instance_id of the cluster" }, "concurrency": { "type": "integer", "format": "int32", "title": "concurrency" }, "waitTime": { "type": "integer", "format": "int32", "title": "wait_time" }, "transferTime": { "type": "integer", "format": "int32", "title": "transfer_time" }, "skipRestart": { "type": "boolean", "title": "skip_restart" }, "force": { "type": "boolean", "title": "force" } }, "title": "BatchReloadRequest", "required": ["instanceId"] }, "ClusterServiceBatchRestartBody": { "type": "object", "properties": { "instanceId": { "type": "array", "items": { "type": "string" }, "title": "The instance_id of the cluster" }, "concurrency": { "type": "integer", "format": "int32", "title": "concurrency" }, "waitTime": { "type": "integer", "format": "int32", "title": "wait_time" } }, "title": "BatchRestartRequest", "required": ["instanceId"] }, "ClusterServiceBatchResumeBody": { "type": "object", "properties": { "instanceId": { "type": "array", "items": { "type": "string" }, "title": "The instance_id of the cluster" }, "concurrency": { "type": "integer", "format": "int32", "title": "concurrency" }, "waitTime": { "type": "integer", "format": "int32", "title": "wait_time" } }, "title": "BatchResumeRequest", "required": ["instanceId"] }, "ClusterServiceCancelTaskFlowBody": { "type": "object", "properties": { "taskId": { "type": "string", "title": "The task_id" } }, "title": "CancelTaskFlowRequest", "required": ["taskId"] }, "ClusterServiceDeployClusterBody": { "type": "object", "properties": { "concurrency": { "type": "integer", "format": "int32", "title": "concurrency" }, "waitTime": { "type": "integer", "format": "int32", "title": "wait_time" } }, "title": "DeployRequest" }, "ClusterServiceDestroyClusterBody": { "type": "object", "title": "DestroyClusterRequest" }, "ClusterServiceOfflineClusterBody": { "type": "object", "title": "OfflineClusterRequest" }, "ClusterServicePauseClusterBody": { "type": "object", "title": "DeployRequest" }, "ClusterServiceReloadClusterBody": { "type": "object", "properties": { "concurrency": { "type": "integer", "format": "int32", "title": "concurrency" }, "waitTime": { "type": "integer", "format": "int32", "title": "wait_time" }, "skipRestart": { "type": "boolean", "title": "skip_restart" }, "transferTimeout": { "type": "integer", "format": "int32", "title": "wait_time" }, "force": { "type": "boolean", "title": "force" } }, "title": "ReloadClusterRequest" }, "ClusterServiceRestartClusterBody": { "type": "object", "properties": { "concurrency": { "type": "integer", "format": "int32", "title": "concurrency" }, "waitTime": { "type": "integer", "format": "int32", "title": "wait_time" } }, "title": "StartClusterRequest" }, "ClusterServiceResumeClusterBody": { "type": "object", "properties": { "concurrency": { "type": "integer", "format": "int32", "title": "concurrency" }, "waitTime": { "type": "integer", "format": "int32", "title": "wait_time" } }, "title": "ResumeClusterRequest" }, "ClusterServiceRetryTaskFlowBody": { "type": "object", "properties": { "taskId": { "type": "string", "title": "The task_id" } }, "title": "RetryTaskFlowRequest", "required": ["taskId"] }, "ClusterServiceScaleInClusterBody": { "type": "object", "properties": { "instanceId": { "type": "array", "items": { "type": "string" }, "title": "The instance_id of the cluster" } }, "title": "ScaleInClusterRequest", "required": ["instanceId"] }, "ClusterServiceScaleOutClusterBody": { "type": "object", "title": "ScaleOutClusterRequest" }, "CredentialServiceUpdateCredentialBody": { "type": "object", "properties": { "userName": { "type": "string", "title": "the user name of the credential" }, "credentialType": { "$ref": "#/definitions/v2CredentialType", "title": "the credential type of the credential" }, "validateType": { "$ref": "#/definitions/v2CredentialValidateType", "title": "the validate type of the credential" }, "credentialName": { "type": "string", "title": "the credential name of the credential" }, "description": { "type": "string", "title": "the description of the credential" }, "hostCredential": { "$ref": "#/definitions/v2HostCredentialObject", "title": "the host credential object" }, "tidbCredential": { "$ref": "#/definitions/v2TiDBCredentialObject", "title": "the tidb cluster credential object" }, "forceUpdate": { "type": "boolean", "title": "auto ssh-copy or set password when change validate type host credential" } }, "title": "UpdateCredential Request", "required": ["userName", "credentialType", "validateType"] }, "DiagnosisServiceAddSqlLimitBody": { "type": "object", "properties": { "resourceGroup": { "type": "string", "title": "Resource group" }, "action": { "type": "string", "enum": ["DRYRUN", "COOLDOWN", "KILL"], "title": "Action" }, "watchText": { "type": "string", "title": "Watch text" } }, "title": "Request message for creating SQL limit", "required": ["resourceGroup", "action", "watchText"] }, "DiagnosisServiceRemoveSqlLimitBody": { "type": "object", "properties": { "watchText": { "type": "string", "title": "Watch text" }, "id": { "type": "string", "title": "SQl limit id" } }, "title": "Request message for removing SQL limit", "required": ["watchText", "id"] }, "DiagnosisServiceUpdateTopSqlConfigsBody": { "type": "object", "properties": { "enable": { "type": "boolean", "title": "tidb_enable_stmt_summary" }, "refreshInterval": { "type": "integer", "format": "int32", "title": "tidb_stmt_summary_refresh_interval" }, "historySize": { "type": "integer", "format": "int32", "title": "tidb_stmt_summary_history_size" }, "maxSize": { "type": "integer", "format": "int32", "title": "tidb_stmt_summary_max_stmt_count" }, "internalQuery": { "type": "boolean", "title": "tidb_stmt_summary_internal_query" } }, "title": "Request message for updating top sql configs", "required": ["enable"] }, "DomainServiceUpdateDomainBody": { "type": "object", "properties": { "name": { "type": "string", "title": "the domain name of the domain" }, "description": { "type": "string", "title": "the domain description of the domain" } }, "title": "UpdateDomain Request" }, "GlobalBRServiceUpdateBackupPolicyBody": { "type": "object", "properties": { "name": { "type": "string", "title": "The policy name" }, "logBackup": { "type": "boolean", "title": "LogBackup means whether to backup log" }, "destination": { "type": "string", "title": "Destination of the backup" }, "accessKeyId": { "type": "string", "title": "AccessKeyID" }, "secretAccessKey": { "type": "string", "title": "SecretAccessKey" }, "rateLimit": { "type": "integer", "format": "int32", "title": "RateLimit" }, "concurrency": { "type": "integer", "format": "int32", "title": "Concurrency" }, "logFile": { "type": "string", "title": "LogFile" }, "cycle": { "$ref": "#/definitions/v2CycleEnumData", "title": "Cycle of backup" }, "frequency": { "type": "string", "title": "Frequency: week:0~6 for Sunday To Saturday, month:1~31 for Date.example:1,2,3,4,5,6,7" }, "time": { "type": "string", "title": "Time" }, "retention": { "type": "integer", "format": "int32", "title": "Retention" }, "clusters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Cluster" }, "title": "Clusters" }, "clusterIds": { "type": "array", "items": { "type": "string" }, "title": "ClusterIDs" } }, "title": "BackupPolicy represents a backup policy", "required": [ "name", "logBackup", "destination", "cycle", "frequency", "time", "retention" ] }, "HostServiceHostConfirmBody": { "type": "object", "title": "ConfirmRequest" }, "LocationServiceUpdateLocationsBody": { "type": "object", "properties": { "location": { "$ref": "#/definitions/v2Locations", "title": "the Location basic resource" } }, "title": "UpdateLocation Request" }, "RoleServiceUpdateRoleBody": { "type": "object", "properties": { "roleName": { "type": "string", "title": "The name of the role" }, "roleType": { "type": "integer", "format": "int32", "title": "The id of the role" }, "detail": { "type": "string", "title": "The detail of the role" }, "note": { "type": "string", "title": "The note of the role" } }, "title": "Update Role Request", "required": ["roleName"] }, "TagServiceUpdateTagBody": { "type": "object", "properties": { "tagKey": { "type": "string", "title": "the tag key of the tag" }, "tagValue": { "type": "string", "title": "the tag value of the tag" } }, "title": "UpdateTag Request", "required": ["tagValue"] }, "UserServiceResetPasswordBody": { "type": "object", "properties": { "newPassword": { "type": "string", "title": "User new password" } }, "title": "ResetPasswordRequest Request", "required": ["newPassword"] }, "UserServiceUpdateUserBody": { "type": "object", "properties": { "email": { "type": "string", "description": "The email address of the user." }, "note": { "type": "string", "description": "Additional notes about the user." }, "userType": { "type": "integer", "format": "int32", "description": "The type of the user (e.g., admin, regular user)." }, "phone": { "type": "string", "description": "The user's phone number." }, "roles": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2UserRole" }, "description": "The roles assigned to the user." } }, "title": "UpdateUser Request" }, "clusterv2ClusterTaskFlow": { "type": "object", "properties": { "taskId": { "type": "string", "description": "The unique identifier of the task." }, "templateId": { "type": "string", "description": "The template ID associated with the task." }, "creator": { "type": "string", "description": "The creator of the task." }, "parentId": { "type": "string", "enum": [ "tidb:deploy", "tidb:takeover", "tidb:scaleOut", "tidb:scaleIn", "tidb:destroy", "tidb:stop", "tidb:start", "tidb:restart", "tidb:reload" ], "description": "The parent task identifier.", "title": "The parent enum" }, "clusterId": { "type": "string", "description": "The creator of the task." }, "clusterName": { "type": "string", "description": "The creator of the task." }, "status": { "type": "string", "enum": [ "success", "abort", "timeout", "failed", "running", "pending" ], "description": "The status of the task.", "title": "The task status" }, "startTime": { "type": "string", "format": "date-time", "description": "The timestamp when the task started.", "readOnly": true }, "endTime": { "type": "string", "format": "date-time", "description": "The timestamp when the task ended.", "readOnly": true } }, "title": "Task ClusterTaskFlow", "required": ["taskId", "templateId"] }, "clusterv2ConfigTemplate": { "type": "object", "properties": { "templateId": { "type": "string", "title": "The tidb_version" }, "component": { "type": "string", "title": "The component" }, "componentKey": { "type": "string", "title": "The tidb_version" }, "componentValue": { "type": "string", "title": "The tidb_version" } }, "title": "ConfigTemplate", "required": ["templateId", "component", "componentKey", "componentValue"] }, "clusterv2TakeoverCluster": { "type": "object", "properties": { "clusterName": { "type": "string", "title": "The cluster_name for cluster" }, "user": { "type": "string", "title": "The user for cluster" }, "password": { "type": "string", "title": "The password for cluster" } }, "title": "TakeoverCluster", "required": ["clusterName", "user"] }, "cmserverv2CreateCMServer": { "type": "object", "properties": { "ip": { "type": "string", "title": "The ip of the CMServer Host" }, "sshPort": { "type": "integer", "format": "int32", "title": "The SSHPort of the CMServer Host" }, "credentialId": { "type": "string", "title": "The credential_id of the CMServer Host" }, "tagIds": { "type": "array", "items": { "type": "string" }, "title": "The tagIds of the CMServer" }, "serverName": { "type": "string", "title": "The CMServer name of the CMServer Host" }, "domainId": { "type": "integer", "format": "int32", "title": "The domain id of the CMServer" }, "servicePort": { "type": "integer", "format": "int32", "title": "The proxy port of the CMServer Host" }, "serviceHome": { "type": "string", "title": "The CMServer home of the CMServer Host" }, "tiupHome": { "type": "string", "title": "The Tiup home of the CMServer Host" } }, "title": "CreateCMServer", "required": [ "ip", "sshPort", "credentialId", "serverName", "domainId", "servicePort", "serviceHome", "tiupHome" ] }, "hostv2Report": { "type": "object", "properties": { "reportId": { "type": "string", "title": "The report_id of the Report" }, "hostId": { "type": "string", "title": "The host_id of the Report" }, "checkId": { "type": "string", "title": "The checkId of the Report" }, "checkName": { "type": "string", "title": "The checkName of the Report" }, "checkOut": { "type": "string", "title": "The checkOut of the Report" }, "checkDesc": { "type": "string", "title": "The checkDesc of the Report" }, "checkResult": { "type": "string", "enum": ["passed", "failed", "warned"], "title": "check optional (e.g., \"passed\", \"failed\")" }, "optional": { "type": "boolean", "enum": ["true", "false"], "title": "check optional (e.g., \"true\", \"false\")" }, "fixable": { "type": "boolean", "enum": ["true", "false"], "title": "check fixable (e.g., \"true\", \"false\")" }, "checkBody": { "type": "string", "title": "The checkBody of the Report" } }, "title": "Report", "required": ["reportId"] }, "hostv2UpdateHost": { "type": "object", "properties": { "hostId": { "type": "string", "title": "The host_id of the Host" }, "sshPort": { "type": "integer", "format": "int32", "title": "The SSHPort of the Host" }, "credentialId": { "type": "string", "title": "The Credential_Id of the Host" }, "locationId": { "type": "string", "title": "The locationId of the Host" }, "tagIds": { "type": "array", "items": { "type": "string" }, "title": "The tagIds of the Host" }, "comment": { "type": "string", "title": "The comment of the Host" } }, "title": "UpdateHost" }, "licensev2License": { "type": "object", "properties": { "licenseId": { "type": "string", "title": "License ID" }, "version": { "type": "string", "title": "Version represents the supported version of TEM" }, "licenseType": { "$ref": "#/definitions/v2LicenseTypeEnumData", "title": "LicenseTypeEnum represents the type of license: free, ultimate" }, "allow": { "type": "array", "items": { "type": "string" }, "title": "Array of supported features in url prefix" }, "deny": { "type": "array", "items": { "type": "string" }, "title": "Array of unsupported features in url prefix" }, "activateAt": { "type": "string", "format": "date-time", "title": "Activation date of license" }, "expirationAt": { "type": "string", "format": "date-time", "title": "Expiration date of license" }, "signature": { "type": "string", "format": "byte", "title": "Signature of license" }, "hosts": { "type": "string", "format": "int64", "title": "Number of hosts" }, "vcpu": { "type": "string", "format": "int64", "title": "Number of vcpu" }, "alerts": { "type": "string", "format": "int64", "title": "Number of alters" }, "customerCode": { "type": "string", "title": "Customer code to restrict the range of features" }, "deviceCode": { "type": "string", "title": "Device code which is bound to the license" }, "status": { "$ref": "#/definitions/v2LicenseStatusEnumData", "title": "License status" } }, "title": "License" }, "metricsv2Value": { "type": "object", "properties": { "timestamp": { "type": "number", "format": "double", "title": "Timestamp of the value" }, "value": { "type": "string", "title": "The actual value" } }, "title": "Value represents a single value in the query result" }, "protobufAny": { "type": "object", "properties": { "@type": { "type": "string", "description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com. As of May 2023, there are no widely used type server\nimplementations and no plans to implement one.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." } }, "additionalProperties": {}, "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n // or ...\n if (any.isSameTypeAs(Foo.getDefaultInstance())) {\n foo = any.unpack(Foo.getDefaultInstance());\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := anypb.New(foo)\n if err != nil {\n ...\n }\n ...\n foo := \u0026pb.Foo{}\n if err := any.UnmarshalTo(foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" }, "rpcStatus": { "properties": { "error": { "properties": { "code": { "format": "int32", "type": "integer" }, "details": { "items": { "$ref": "#/definitions/v2ErrorDetail", "type": "object" }, "type": "array" }, "message": { "type": "string" }, "status": { "type": "string" } }, "type": "object" } }, "type": "object" }, "tagv2Tag": { "type": "object", "properties": { "tagId": { "type": "string", "title": "the tag id of the tag" }, "tagKey": { "type": "string", "title": "the tag key of the tag" }, "tagValue": { "type": "string", "title": "the tag value of the tag" } }, "title": "Tag basic resource", "required": ["tagValue"] }, "taskv2Response": { "type": "object", "properties": { "Reply": { "type": "string", "format": "byte", "description": "The response content." }, "ReturnCode": { "type": "integer", "format": "int32", "description": "The return code." } }, "description": "Response represents an action response." }, "tiupv2CreateTiups": { "type": "object", "properties": { "hostId": { "type": "string", "title": "host_id" }, "tiupHome": { "type": "string", "title": "The SSHPort of the Host" }, "description": { "type": "string", "title": "The Credential_Id of the Host" }, "name": { "type": "string", "title": "The locationId of the Host" }, "tagIds": { "type": "array", "items": { "type": "string" }, "title": "The tagIds of the Host" } }, "title": "CreateTiups" }, "tiupv2UpdateTiups": { "type": "object", "properties": { "tagIds": { "type": "array", "items": { "type": "string" }, "title": "The tagIds of the Host" }, "description": { "type": "string", "title": "The Credential_Id of the Host" }, "name": { "type": "string", "title": "The locationId of the Host" } }, "title": "UpdateTiups" }, "v2Abstract": { "type": "object", "properties": { "TaskId": { "type": "string", "description": "The task identifier." }, "Status": { "type": "string", "description": "The task status." }, "Message": { "type": "string", "description": "The task message." }, "CreateTime": { "type": "string", "format": "date-time", "description": "The task creation time.", "readOnly": true }, "Retry": { "type": "integer", "format": "int32", "description": "The retry count." } }, "description": "Abstract represents the abstract information of a task.", "required": ["TaskId"] }, "v2Action": { "type": "object", "properties": { "step": { "$ref": "#/definitions/v2Step", "description": "The step information." }, "TaskId": { "type": "string", "description": "The task identifier." }, "response": { "$ref": "#/definitions/taskv2Response", "description": "The response information." }, "indegree": { "type": "integer", "format": "int32", "description": "The indegree count." } }, "description": "Action represents a task action." }, "v2ActivateLicenseRequest": { "type": "object", "properties": { "fileName": { "type": "string", "title": "The File name of the license" }, "content": { "type": "string", "format": "binary", "description": "The license file to upload to activate the license", "title": "The content of the license file" }, "headers": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The header of the license file" } }, "title": "ActivateLicenseRequest is the request message for ActivateLicense", "required": ["fileName", "content", "headers"] }, "v2AlertmanagerSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "webPort": { "type": "integer", "format": "int32", "title": "port" }, "clusterPort": { "type": "integer", "format": "int32", "title": "ng_port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" } }, "title": "AlertmanagerSpec represents the AlertManager topology specification in topology.yaml" }, "v2ApiKey": { "type": "object", "properties": { "accessKey": { "type": "string", "title": "The access_key of apikey" }, "secretKey": { "type": "string", "title": "The secret_key of apikey" }, "creator": { "type": "string", "title": "The creator of apikey" }, "status": { "type": "string", "enum": ["disable", "enable"], "title": "The apikey status" }, "description": { "type": "string", "title": "The description of apikey" }, "createTime": { "type": "string", "format": "date-time", "title": "The create time of the apikey", "readOnly": true }, "updateTime": { "type": "string", "format": "date-time", "title": "The update time of the role", "readOnly": true } }, "title": "ApiKey resource", "required": ["accessKey"] }, "v2AssociatedClusters": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "the tag id of the tag" }, "clusterName": { "type": "string", "title": "the tag key of the tag" } }, "title": "AssociatedClusters" }, "v2AuditConfigs": { "type": "object", "properties": { "enabled": { "type": "boolean", "title": "Whether auditing is enabled" }, "retentionDays": { "type": "integer", "format": "int32", "title": "Log retention period in days" } }, "title": "Audit configuration" }, "v2AuditLogEntry": { "type": "object", "properties": { "endsAt": { "type": "string", "format": "date-time", "title": "Creation time" }, "operatorId": { "type": "string", "title": "Operator id" }, "operatorType": { "type": "string", "title": "Operator type (e.g., USER, API_KEY)" }, "event": { "type": "string", "title": "Event (e.g., cluster, host, user, parameter group)" }, "operation": { "type": "string", "title": "Specific operation (e.g., create, delete, start, restart)" }, "detail": { "type": "string", "title": "Details (including full URL and request/response)" }, "traceId": { "type": "string", "title": "Trace ID" }, "clientIp": { "type": "string", "title": "Client IP address" }, "result": { "type": "string", "title": "Operation result (e.g., success, failure)" } }, "title": "Audit log entry" }, "v2AuditLogs": { "type": "object", "properties": { "logs": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2AuditLogEntry" }, "title": "List of audit log entries" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "Get audit logs response" }, "v2BRSummary": { "type": "object", "properties": { "topClustersWithBrSize": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ClusterWithBRSize" }, "title": "List of top BR size clusters" }, "topClustersWithBrAlert": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ClusterWithBRAlert" }, "title": "List of top BR alert clusters" }, "topClustersWithoutBrPolicy": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ClusterWithoutBRPolicy" }, "title": "List of top size clusters without BR policy" } }, "title": "GetBRSummaryResp represents the response to get br summary" }, "v2BRTask": { "type": "object", "properties": { "taskId": { "type": "string", "title": "The br task ID" }, "type": { "$ref": "#/definitions/v2TypeEnumData", "title": "Type of the br task" }, "triggerType": { "$ref": "#/definitions/v2TriggerTypeEnumData", "title": "Trigger type of the br task" }, "name": { "type": "string", "title": "Name of the br task" }, "status": { "$ref": "#/definitions/v2StatusEnumData", "title": "Status of the br task" }, "restoredTs": { "type": "string", "title": "Restored ts" }, "startTime": { "type": "string", "title": "Start time" }, "endTime": { "type": "string", "title": "End time" }, "clusterId": { "type": "string", "title": "The cluster ID" }, "clusterName": { "type": "string", "title": "The cluster name" }, "destination": { "type": "string", "title": "Destination of the br task" }, "size": { "type": "string", "title": "Size of the br task, 10TB/100GB" }, "sizeByte": { "type": "string", "format": "int64", "title": "Size of the br task in byte" }, "errorMessage": { "type": "string", "title": "Error message" }, "policyId": { "type": "string", "title": "Policy ID of the br task" }, "policyName": { "type": "string", "title": "Policy name of the br task" }, "log": { "type": "string", "title": "Log" }, "accessKeyId": { "type": "string", "title": "AccessKeyID" }, "secretAccessKey": { "type": "string", "title": "SecretAccessKey" }, "rateLimit": { "type": "integer", "format": "int32", "title": "RateLimit" }, "concurrency": { "type": "integer", "format": "int32", "title": "Concurrency" }, "logFile": { "type": "string", "title": "LogFile" }, "expireTime": { "type": "string", "title": "ExpireTime" } }, "title": "BRTask represents a br task" }, "v2BackupCycleEnumData": { "type": "string", "enum": ["week", "month"], "default": "week", "description": "- week: \nWeek\n - month: \nMonth", "title": "Data of CycleEnum" }, "v2BackupPolicy": { "type": "object", "properties": { "policyId": { "type": "string", "title": "The policy ID" }, "name": { "type": "string", "title": "The policy name" }, "logBackup": { "type": "boolean", "title": "LogBackup means whether to backup log" }, "destination": { "type": "string", "title": "Destination of the backup" }, "accessKeyId": { "type": "string", "title": "AccessKeyID" }, "secretAccessKey": { "type": "string", "title": "SecretAccessKey" }, "rateLimit": { "type": "integer", "format": "int32", "title": "RateLimit" }, "concurrency": { "type": "integer", "format": "int32", "title": "Concurrency" }, "logFile": { "type": "string", "title": "LogFile" }, "cycle": { "$ref": "#/definitions/v2CycleEnumData", "title": "Cycle of backup" }, "frequency": { "type": "string", "title": "Frequency: week:0~6 for Sunday To Saturday, month:1~31 for Date.example:1,2,3,4,5,6,7" }, "time": { "type": "string", "title": "Time" }, "retention": { "type": "integer", "format": "int32", "title": "Retention" }, "clusters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Cluster" }, "title": "Clusters" }, "clusterIds": { "type": "array", "items": { "type": "string" }, "title": "ClusterIDs" } }, "title": "BackupPolicy represents a backup policy", "required": [ "name", "logBackup", "destination", "cycle", "frequency", "time", "retention" ] }, "v2BasicClusterInfo": { "type": "object", "properties": { "id": { "type": "string", "title": "The cluster ID" }, "name": { "type": "string", "title": "The cluster name" } }, "title": "BasicCluster represents a cluster basic info" }, "v2BatchCreateTagsRequest": { "type": "object", "properties": { "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/tagv2Tag" }, "title": "the tags with basic resource" } }, "title": "Batch Create Tags Request", "required": ["tags"] }, "v2BatchCreateTagsResponse": { "type": "object", "properties": { "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/tagv2Tag" }, "title": "the tags" } }, "title": "Create Tags Response" }, "v2BatchDeleteRequest": { "type": "object", "properties": { "hostId": { "type": "array", "items": { "type": "string" }, "title": "The host_id of the host" } }, "title": "Delete BatchDeleteRequest", "required": ["hostId"] }, "v2BatchPauseResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the cluster" } }, "title": "BatchPauseResponse", "required": ["clusterId", "taskId"] }, "v2BatchReloadResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "BatchReloadResponse", "required": ["clusterId", "taskId"] }, "v2BatchRestartResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "BatchStartResponse", "required": ["clusterId", "taskId"] }, "v2BatchResumeResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the cluster" } }, "title": "BatchResumeResponse", "required": ["clusterId", "taskId"] }, "v2BindCMServer": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "the cm server id" }, "hostIp": { "type": "string", "title": "the host ip of the cm server" } }, "title": "BindCMServer" }, "v2BindObject": { "type": "object", "properties": { "resourceType": { "$ref": "#/definitions/v2TagBindResourceType", "title": "the resource type of the bind object" }, "resources": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ResourceObject" }, "title": "the resources of the resource type" } }, "title": "Bind object resource", "required": ["resourceType", "resources"] }, "v2BindResourceRequest": { "type": "object", "properties": { "resourceType": { "$ref": "#/definitions/v2TagBindResourceType", "title": "the resource type of the bind object" }, "resourceId": { "type": "string", "title": "the resource id of the resource type" }, "tagIds": { "type": "array", "items": { "type": "string" }, "title": "the tag ids to be bound" } }, "title": "BindResource Request", "required": ["resourceType", "resourceId"] }, "v2BindResourceResponse": { "type": "object", "properties": { "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/tagv2Tag" }, "title": "the tags bound with the bind object" } }, "title": "BindResource Response" }, "v2BindTagRequest": { "type": "object", "properties": { "tagId": { "type": "string", "title": "the tag id of the tag" }, "bindObjects": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2BindObject" }, "title": "the bind objects to be bound" } }, "title": "BindTag Request", "required": ["tagId"] }, "v2BindTagResponse": { "type": "object", "properties": { "tag": { "$ref": "#/definitions/v2TagWithBindObject", "title": "the tag resource with bound objects" } }, "title": "BindTag Response" }, "v2CDCSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "CDCSpec represents the CDC topology specification in topology.yaml" }, "v2CMServer": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "CMServer id" }, "name": { "type": "string", "title": "name" }, "createTime": { "type": "string", "format": "date-time", "title": "create time" }, "updateTime": { "type": "string", "format": "date-time", "title": "update time" }, "hostId": { "type": "string", "title": "host id" }, "hostIp": { "type": "string", "title": "host ip" }, "domainId": { "type": "integer", "format": "int32", "title": "domain id" }, "domainName": { "type": "string", "title": "domain name" }, "status": { "$ref": "#/definitions/v2State", "title": "server state", "readOnly": true }, "serviceStatus": { "$ref": "#/definitions/v2ServiceState", "title": "service status", "readOnly": true }, "credentialId": { "type": "string", "title": "credential_id" }, "serviceVersion": { "type": "string", "title": "CMServer version" }, "servicePort": { "type": "integer", "format": "int32", "title": "The proxy port of the CMServer Host" }, "serviceHome": { "type": "string", "title": "deploy path" }, "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/tagv2Tag" }, "title": "tags" } }, "title": "CMServer basic resource", "required": [ "createTime", "updateTime", "credentialId", "servicePort", "serviceHome" ] }, "v2CMServerClusters": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "cluster_id" }, "clusterName": { "type": "string", "title": "cluster_name" }, "user": { "type": "string", "title": "user" }, "version": { "type": "string", "title": "version" }, "metaPath": { "type": "string", "title": "patch" }, "privateKeyPath": { "type": "string", "title": "private_key" }, "managed": { "type": "boolean", "title": "managed" } }, "title": "CMServerClusters" }, "v2CMServerClustersResponse": { "type": "object", "properties": { "clusters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2CMServerClusters" }, "title": "list of clusters" } }, "title": "CMServerClustersResponse" }, "v2CMServerHost": { "type": "object", "properties": { "hostId": { "type": "string", "title": "The host_id of the Host" }, "ip": { "type": "string", "title": "The ip of the Host" }, "sshPort": { "type": "integer", "format": "int32", "title": "The ssh_port of the Host" }, "osName": { "type": "string", "title": "os_name" }, "cpuModel": { "type": "string", "title": "cpu_model" }, "cpus": { "type": "integer", "format": "int32", "title": "cpus" }, "cpuCores": { "type": "integer", "format": "int32", "title": "cpu_cores" }, "cpuThreads": { "type": "integer", "format": "int32", "title": "cpu_threads" }, "cpuArch": { "type": "string", "title": "cpu_arch" }, "memorySize": { "type": "integer", "format": "int32", "title": "memory_size" }, "memoryUnit": { "type": "string", "title": "memory_unit" }, "storageUnit": { "type": "string", "title": "storage_unit" }, "cpuNumaNodes": { "type": "integer", "format": "int32", "title": "The CpuNumaNodes of the Host" }, "storageTotalSize": { "type": "integer", "format": "int32", "title": "The Storage of the Host" }, "diskType": { "type": "string", "title": "The DiskType of the Host" }, "hostType": { "type": "string", "enum": ["VM", "PM"], "title": "host Type (e.g., \"VM\", \"PM\")" } }, "title": "CMServerHost", "required": ["hostId"] }, "v2CMServerWithTiup": { "type": "object", "properties": { "server": { "$ref": "#/definitions/v2CMServer", "title": "CM Server basic resource" }, "tiup": { "$ref": "#/definitions/v2Tiup", "title": "Tiup Resource" }, "host": { "$ref": "#/definitions/v2CMServerHost", "title": "Host Resource" } }, "title": "CMServerWithTiup" }, "v2CancelTaskFlowResponse": { "type": "object", "properties": { "taskId": { "type": "string", "title": "The task_id" } }, "title": "CancelTaskFlowResponse", "required": ["taskId"] }, "v2CategoryMetricDetail": { "type": "object", "properties": { "class": { "type": "string", "title": "Level 1 classification" }, "group": { "type": "string", "title": "Level 2 grouping" }, "type": { "type": "string", "title": "Level 3 type" }, "order": { "type": "integer", "format": "int32", "title": "Display order of charts" }, "displayName": { "type": "string", "title": "Display name of the metric" }, "name": { "type": "string", "title": "Metric Name" }, "description": { "type": "string", "title": "Description of the metric" }, "metric": { "$ref": "#/definitions/v2MetricWithExpressions", "title": "Metric with its expressions" } }, "title": "CategoryMetricDetail represents the details of a metric category" }, "v2ChangePasswordRequest": { "type": "object", "properties": { "userId": { "type": "string", "title": "The id of the user" }, "oldPassword": { "type": "string", "title": "User old password" }, "newPassword": { "type": "string", "title": "User new password" } }, "title": "ChangePasswordRequest Request", "required": ["userId", "newPassword"] }, "v2Channel": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "ID", "readOnly": true }, "createTime": { "type": "string", "format": "date-time", "title": "Creation time of the channel", "readOnly": true }, "updateTime": { "type": "string", "format": "date-time", "title": "Last update time of the channel", "readOnly": true }, "name": { "type": "string", "title": "Name of the channel" }, "type": { "type": "string", "enum": ["email", "webhook"], "title": "The channel type" }, "creator": { "type": "string", "title": "Creator of the channel", "readOnly": true }, "emailConfig": { "$ref": "#/definitions/v2EmailConfig", "title": "Email configuration, required if type is EMAIL" }, "webhookConfig": { "$ref": "#/definitions/v2WebhookConfig", "title": "Webhook configuration, required if type is WEBHOOK" }, "channelMonitorObjects": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ChannelMonitorObject" }, "title": "Channel Object List" }, "updateChannelObjects": { "type": "boolean", "title": "update channel object" } }, "title": "AlertChannel represents an alert channel configuration", "required": ["name", "type"] }, "v2ChannelMonitorObject": { "type": "object", "properties": { "application": { "type": "string", "enum": ["Cluster"], "title": "The monitor object application" }, "alertObject": { "type": "string", "description": "Object, such as cluster id, host id." } }, "title": "ChannelObject represents an object to monitor in an alert channel", "required": ["application", "alertObject"] }, "v2CheckSupportResponse": { "type": "object", "properties": { "isSupport": { "type": "boolean", "title": "Is support sql plan binding" } }, "title": "Response of checking whether cluster support sql plan binding" }, "v2CheckUnsupportedParamsRequest": { "type": "object", "properties": { "configs": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ParamBase" }, "title": "The configs to check" }, "checkClusterVersion": { "type": "string", "title": "The version of the cluster to check" } }, "title": "CheckUnsupportedParamsRequest represents a check unsupported params request", "required": ["checkClusterVersion"] }, "v2CheckUnsupportedParamsResponse": { "type": "object", "properties": { "unsupportedParams": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ParamBase" }, "title": "The unsupported params" } }, "title": "CheckUnsupportedParamsResponse represents a check unsupported params response" }, "v2ClassEnumData": { "type": "string", "enum": ["unspecified", "cluster", "host", "overview"], "default": "unspecified", "description": "- unspecified: Unspecified\n - cluster: Cluster metrics\n - host: Host metrics\n - overview: Overview metrics", "title": "Data of ClassEnum" }, "v2Cluster": { "type": "object", "properties": { "id": { "type": "string", "title": "The cluster ID" }, "name": { "type": "string", "title": "The cluster name" } }, "title": "Cluster represents a cluster basic info" }, "v2ClusterAvailableConfigComponents": { "type": "object", "properties": { "components": { "type": "array", "items": { "type": "string" }, "title": "Components is the list of components" } }, "title": "GetClusterAvailableConfigComponentsResponse is the response message for GetClusterAvailableConfigComponents", "required": ["components"] }, "v2ClusterBRStatusEnumData": { "type": "string", "enum": ["running", "finished", "abnormal", "stopped"], "default": "running", "description": "- running: Running\n - finished: Finished\n - abnormal: Abnormal\n - stopped: Stopped", "title": "Data of StatusEnum" }, "v2ClusterBRTask": { "type": "object", "properties": { "taskId": { "type": "string", "title": "The br task ID" }, "type": { "$ref": "#/definitions/v2ClusterBRTypeEnumData", "title": "Type of the br task" }, "triggerType": { "$ref": "#/definitions/v2ClusterBRTriggerTypeEnumData", "title": "Trigger type of the br task" }, "name": { "type": "string", "title": "Name of the br task" }, "status": { "$ref": "#/definitions/v2ClusterBRStatusEnumData", "title": "Status of the br task" }, "restoredTs": { "type": "string", "title": "Restored ts" }, "startTime": { "type": "string", "title": "Start time" }, "endTime": { "type": "string", "title": "End time" }, "clusterId": { "type": "string", "title": "The cluster ID" }, "clusterName": { "type": "string", "title": "The cluster name" }, "destination": { "type": "string", "title": "Destination of the br task" }, "size": { "type": "string", "title": "Size of the br task, 10TB/100GB" }, "sizeByte": { "type": "string", "format": "int64", "title": "Size of the br task in byte" }, "errorMessage": { "type": "string", "title": "Error message" }, "policyId": { "type": "string", "title": "Policy ID of the br task" }, "policyName": { "type": "string", "title": "Policy name of the br task" }, "log": { "type": "string", "title": "Log" }, "accessKeyId": { "type": "string", "title": "AccessKeyID" }, "secretAccessKey": { "type": "string", "title": "SecretAccessKey" }, "rateLimit": { "type": "integer", "format": "int32", "title": "RateLimit" }, "concurrency": { "type": "integer", "format": "int32", "title": "Concurrency" }, "logFile": { "type": "string", "title": "LogFile" }, "expireTime": { "type": "string", "title": "ExpireTime" } }, "title": "ClusterBRTask represents a br task" }, "v2ClusterBRTriggerTypeEnumData": { "type": "string", "enum": ["automatic", "manual"], "default": "automatic", "description": "- automatic: automatic\n - manual: manual", "title": "Data of TriggerTypeEnum" }, "v2ClusterBRTypeEnumData": { "type": "string", "enum": [ "full_backup", "log_backup", "restore_by_file", "restore_by_time", "all_backup", "all_restore" ], "default": "full_backup", "description": "- full_backup: Full backup\n - log_backup: Log backup\n - restore_by_file: Restore by file\n - restore_by_time: Restore by time\n - all_backup: All backup\n - all_restore: All restore", "title": "Data of TypeEnum" }, "v2ClusterBackupPolicy": { "type": "object", "properties": { "policyId": { "type": "string", "title": "The policy ID" }, "name": { "type": "string", "title": "The policy name" }, "logBackup": { "type": "boolean", "title": "LogBackup means whether to backup log" }, "destination": { "type": "string", "title": "Destination of the backup" }, "accessKeyId": { "type": "string", "title": "AccessKeyID" }, "secretAccessKey": { "type": "string", "title": "SecretAccessKey" }, "rateLimit": { "type": "integer", "format": "int32", "title": "RateLimit" }, "concurrency": { "type": "integer", "format": "int32", "title": "Concurrency" }, "logFile": { "type": "string", "title": "LogFile" }, "cycle": { "$ref": "#/definitions/v2BackupCycleEnumData", "title": "Cycle of backup" }, "frequency": { "type": "string", "title": "Frequency: week:0~6 for Sunday To Saturday, month:1~31 for Date.example:1,2,3,4,5,6,7" }, "time": { "type": "string", "title": "Time" }, "retention": { "type": "integer", "format": "int32", "title": "Retention" }, "clusters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2BasicClusterInfo" }, "title": "Clusters" }, "lastBackupTime": { "type": "string", "title": "LastBackupTime" }, "size": { "type": "string", "title": "Size" }, "sizeByte": { "type": "string", "format": "int64", "title": "SizeByte" }, "lastLogBackupTime": { "type": "string", "title": "LastLogBackupTime" }, "logBackupDelay": { "type": "string", "title": "LogBackupDelay" } }, "title": "ClusterBackupPolicy represents a backup policy and backup status", "required": [ "name", "logBackup", "destination", "cycle", "frequency", "time", "retention" ] }, "v2ClusterConfigRequest": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id" }, "tidbVersion": { "type": "string", "title": "The tidb_version" }, "tiupVersion": { "type": "string", "title": "The tiup_version" }, "clusterName": { "type": "string", "title": "The cluster_name" }, "dbUser": { "type": "string", "title": "The cluster_name" }, "dbPassword": { "type": "string", "title": "The db_password" }, "arch": { "type": "string", "title": "The arch" }, "tiupId": { "type": "string", "title": "The cm_id" }, "cmId": { "type": "string", "title": "The cm_id" }, "locations": { "type": "array", "items": { "type": "string" }, "title": "The locations" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "deployUser": { "type": "string", "title": "deploy_dir" }, "spec": { "$ref": "#/definitions/v2Spec", "title": "The spec" } }, "title": "ClusterConfigRequest", "required": [ "clusterId", "tidbVersion", "tiupVersion", "clusterName", "dbUser", "dbPassword", "arch", "tiupId", "cmId", "locations", "spec" ] }, "v2ClusterConfigResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id" }, "tiupId": { "type": "string", "title": "The cm_id" }, "cmId": { "type": "string", "title": "The cm_id" }, "spec": { "$ref": "#/definitions/v2Spec", "title": "The spec" } }, "title": "ClusterConfigResponse", "required": ["clusterId", "tiupId", "cmId", "spec"] }, "v2ClusterInstances": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The tidb_version" }, "component": { "type": "string", "enum": [ "grafana", "tikv", "tidb", "prometheus", "pd", "tiflash", "pump", "drainer", "monitor", "alertManager", "cdc", "dashboard" ], "title": "Cluster Instances type (e.g., \"tikv\", \"tidb\")" }, "instancesId": { "type": "string", "title": "The instances_id" }, "componentValue": { "type": "string", "title": "The tidb_version" }, "ip": { "type": "string", "title": "ip" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "hostId": { "type": "string", "title": "host" }, "status": { "type": "string", "enum": [ "Up", "Down", "Unreachable", "Tombstone", "GoingOffline", "unknow" ], "title": "Cluster Instances status (e.g., \"Up\", \"Down\")" }, "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Tags" }, "title": "The Tag of the Host" }, "locationMappings": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2LocationMappings" }, "title": "location_mappings" }, "ports": { "type": "string", "title": "ports" }, "arch": { "type": "string", "title": "arch" }, "version": { "type": "string", "title": "version" }, "os": { "type": "string", "title": "os" }, "dataDir": { "type": "string", "title": "os" }, "deployDir": { "type": "string", "title": "os" }, "logDir": { "type": "string", "title": "os" }, "numaNode": { "type": "string", "title": "os" }, "numaCores": { "type": "string", "title": "os" }, "runtimeDuration": { "type": "string", "title": "The runtime_duration" } }, "title": "ClusterInstances", "required": [ "clusterId", "instancesId", "componentValue", "ip", "port", "hostId", "ports", "arch", "version", "os", "dataDir", "deployDir", "logDir", "numaNode", "numaCores", "runtimeDuration" ] }, "v2ClusterMetricData": { "type": "object", "properties": { "status": { "type": "string", "title": "Response Status (e.g., \"success\", \"error\")" }, "data": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ExprQueryData" }, "title": "Response Data containing the queried metrics" } }, "title": "ClusterMetricData represents the response for querying cluster metric data" }, "v2ClusterMetricInstance": { "type": "object", "properties": { "type": { "type": "string", "title": "Target type (e.g., tikv, tidb, host)" }, "instanceList": { "type": "array", "items": { "type": "string" }, "title": "List of instances for the specified metric" } }, "title": "QueryClusterMetricInstanceResponse represents the response for querying cluster metric instances" }, "v2ClusterNodeTopology": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The tidb_version" }, "component": { "type": "string", "enum": [ "grafana", "tikv", "tidb", "prometheus", "pd", "tiflash", "pump", "drainer", "monitor", "alertManager", "cdc", "dashboard" ], "title": "Cluster Instances type (e.g., \"tikv\", \"tidb\")" }, "nodes": { "type": "integer", "format": "int32", "title": "nodes" }, "nodesDown": { "type": "integer", "format": "int32", "title": "nodes_down" }, "versions": { "type": "array", "items": { "type": "string" }, "title": "versions" }, "endpoints": { "type": "array", "items": { "type": "string" }, "title": "endpoints" } }, "title": "Nodes", "required": ["clusterId", "nodes", "nodesDown"] }, "v2ClusterProcess": { "type": "object", "properties": { "instance": { "type": "string", "title": "TiDB instance identifier running this process" }, "id": { "type": "string", "title": "Unique process ID within the cluster" }, "user": { "type": "string", "title": "user that started the process" }, "host": { "type": "string", "title": "Client host information" }, "db": { "type": "string", "title": "Database being accessed by the process" }, "command": { "type": "string", "enum": [ "Sleep", "Quit", "Init DB", "Query", "Field List", "Create DB", "Drop DB", "Refresh", "Shutdown", "Statistics", "Processlist", "Connect", "Kill", "Debug", "Ping", "Time", "Delayed Insert", "Change User", "Binlog Dump", "Table Dump", "Connect out", "Register Slave", "Prepare", "Execute", "Long Data", "Close stmt", "Reset stmt", "Set option", "Fetch", "Daemon", "Reset connect" ], "title": "Current command being executed (e.g., \"Query\", \"Sleep\")" }, "time": { "type": "string", "format": "int64", "title": "Time in seconds that the process has been running" }, "state": { "type": "string", "title": "Current state of the process" }, "info": { "type": "string", "title": "SQL statement or other information about the process" }, "digest": { "type": "string", "title": "Query digest for identifying similar queries" }, "mem": { "type": "string", "title": "Memory usage of the process" }, "disk": { "type": "string", "title": "Disk usage of the process" }, "txnStart": { "type": "string", "title": "Transaction start timestamp" }, "resourceGroup": { "type": "string", "title": "Resource group assigned to the process" }, "sessionAlias": { "type": "string", "title": "Session alias" }, "rowsAffected": { "type": "string", "format": "int64", "title": "Rows affected" }, "tidbCpu": { "type": "string", "format": "int64", "title": "tidb cpu" }, "tikvCpu": { "type": "string", "format": "int64", "title": "tikv cpu" } }, "title": "ClusterProcess represents a single process in the cluster" }, "v2ClusterResourceUsage": { "type": "object", "properties": { "tikv": { "$ref": "#/definitions/v2ComponentStorageUsage", "title": "TiKV storage resource usage information" }, "tiflash": { "$ref": "#/definitions/v2ComponentStorageUsage", "title": "TiFlash storage resource usage information" } }, "title": "ClusterResourceUsage represents storage resource usage information for TiKV and TiFlash" }, "v2ClusterStatus": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id" }, "statusType": { "type": "string", "enum": ["parameter_changed", "node_down", "routing", "rule_changed"], "title": "Cluster run status type (e.g., \"parameter_changed\", \"node_down\")" }, "status": { "type": "string", "title": "The status" }, "statusInfo": { "type": "string", "title": "The status" } }, "title": "ClusterStatus", "required": ["clusterId", "status", "statusInfo"] }, "v2ClusterTaskFlowResponse": { "type": "object", "properties": { "clusterTaskFlow": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/clusterv2ClusterTaskFlow" }, "title": "The cluster_task_flow" } }, "title": "ClusterTaskFlowResponse", "required": ["clusterTaskFlow"] }, "v2ClusterTopology": { "type": "object", "properties": { "id": { "type": "string", "title": "id" }, "role": { "type": "string", "title": "role" }, "host": { "type": "string", "title": "host" }, "ports": { "type": "string", "title": "ports" }, "osArch": { "type": "string", "title": "os_arch" }, "status": { "type": "string", "title": "status" }, "dataDir": { "type": "string", "title": "data_dir" }, "deployDir": { "type": "string", "title": "deploy_dir" } }, "title": "ClusterTopology", "required": [ "id", "role", "host", "ports", "osArch", "status", "dataDir", "deployDir" ] }, "v2ClusterVersionsResponse": { "type": "object", "properties": { "tidbVersion": { "type": "string", "title": "The tidb_version" }, "tiupId": { "type": "string", "title": "The tiup_id" }, "versions": { "type": "array", "items": { "type": "string" }, "title": "The versions" }, "errMsg": { "type": "string", "title": "err_msg" } }, "title": "ClusterVersionsResponse", "required": ["tidbVersion", "tiupId"] }, "v2ClusterWithBRAlert": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "Cluster ID" }, "clusterName": { "type": "string", "title": "Cluster Name" }, "alertCount": { "type": "string", "format": "int64", "title": "Alert count" } }, "title": "ClusterWithBRAlert represents a cluster with BR alert" }, "v2ClusterWithBRSize": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "Cluster ID" }, "clusterName": { "type": "string", "title": "Cluster Name" }, "totalSizeByte": { "type": "string", "format": "int64", "title": "total size in byte" }, "totalSize": { "type": "string", "title": "Total size with unit, e.g. 10TB, other form of TotalSizeByte" } }, "title": "ClusterWithBRSize represents a cluster with BR size" }, "v2ClusterWithoutBRPolicy": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "Cluster ID" }, "clusterName": { "type": "string", "title": "Cluster Name" }, "lastBackupTime": { "type": "string", "title": "Last backup time" }, "sizeByte": { "type": "string", "format": "int64", "title": "Backup size in byte" } }, "title": "ClusterWithoutBRPolicy represents a cluster without BR policy" }, "v2ClusterYamlResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id" }, "yaml": { "type": "string", "title": "The yaml" } }, "title": "ClusterYamlResponse", "required": ["clusterId", "yaml"] }, "v2Clusters": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "Cluster ID uniquely identifies the target cluster" }, "clusterName": { "type": "string", "title": "Cluster Name uniquely identifies the target cluster" }, "version": { "type": "string", "title": "Cluster version uniquely identifies the target cluster" }, "arch": { "type": "string", "title": "Cluster arch uniquely identifies the target cluster" }, "tiupId": { "type": "string", "title": "Cluster tiup_id uniquely identifies the target cluster" }, "tiupName": { "type": "string", "title": "Cluster tiup_name uniquely identifies the target cluster" }, "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/tagv2Tag" }, "title": "The Tag of the Cluster" }, "nodes": { "type": "integer", "format": "int32", "title": "Cluster nodes uniquely identifies the target cluster" }, "cpu": { "type": "integer", "format": "int32", "title": "Cluster cpu uniquely identifies the target cluster" }, "memory": { "type": "integer", "format": "int32", "title": "Cluster memory uniquely identifies the target cluster" }, "storage": { "type": "integer", "format": "int32", "title": "Cluster storage uniquely identifies the target cluster" }, "status": { "type": "string", "enum": [ "created", "destroyed", "stopped", "running", "offlining", "offlined" ], "title": "Cluster run status (e.g., \"created\", \"running\")" }, "clusterStatus": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ClusterStatus" }, "title": "Cluster status uniquely identifies the target cluster" }, "pdNodes": { "type": "integer", "format": "int32", "title": "Cluster nodes uniquely identifies the target cluster" }, "tidbNodes": { "type": "integer", "format": "int32", "title": "Cluster nodes uniquely identifies the target cluster" }, "tikvNodes": { "type": "integer", "format": "int32", "title": "Cluster nodes uniquely identifies the target cluster" }, "tiflashNodes": { "type": "integer", "format": "int32", "title": "Cluster nodes uniquely identifies the target cluster" }, "alertAll": { "type": "integer", "format": "int32", "title": "alert_all" }, "alertWarning": { "type": "integer", "format": "int32", "title": "alert_warning" }, "alertCritical": { "type": "integer", "format": "int32", "title": "alert_critical" }, "alertEmergency": { "type": "integer", "format": "int32", "title": "alert_emergency" }, "taskStatus": { "type": "string", "enum": [ "running", "creating", "scaling", "destroying", "reloading", "taking", "stopping", "restarting", "starting", "offlining", "offlined" ], "title": "Cluster run status (e.g., \"running\", \"creating\")" }, "taskId": { "type": "string", "title": "Cluster task_id uniquely identifies the target cluster" }, "source": { "type": "string", "enum": ["create", "takeover"], "title": "Cluster source (e.g., \"create\", \"takeover\")" }, "creator": { "type": "string", "title": "creator" }, "createTime": { "type": "string", "format": "date-time", "title": "create_time" }, "tiup": { "$ref": "#/definitions/v2Tiups", "title": "tiup" } }, "title": "Clusters", "required": ["clusterId"] }, "v2ComponentStorageUsage": { "type": "object", "properties": { "storageFreeGb": { "type": "number", "format": "float", "title": "Free storage space in GB" }, "storageCapacityGb": { "type": "number", "format": "float", "title": "Total storage capacity in GB" }, "storageUsedGb": { "type": "number", "format": "float", "title": "Used storage space in GB" }, "storageDailyGrowthGb": { "type": "number", "format": "float", "title": "Daily storage growth in GB" } }, "title": "ComponentStorageUsage represents storage resource usage for a specific component" }, "v2Config": { "type": "object", "properties": { "instanceType": { "type": "string", "title": "InstanceType is the instance type of the parameter" }, "instance": { "type": "string", "title": "Instance is the instance of the parameter" }, "name": { "type": "string", "title": "Name is the name of the parameter" }, "currentValue": { "type": "string", "title": "CurrentValue is the current value of the parameter" }, "settingValue": { "type": "string", "title": "SettingValue is the setting value of the parameter" }, "settingValueValid": { "type": "boolean", "title": "SettingValueValid is if the setting value is valid" }, "type": { "$ref": "#/definitions/v2ParamTypeEnumData", "title": "Type is the type of the parameter" }, "edited": { "type": "boolean", "title": "Edited is if the parameter is edited" }, "defaultValue": { "type": "string", "title": "DefaultValue is the default value of the parameter" }, "dynamic": { "type": "boolean", "title": "Dynamic is if the parameter is dynamic" }, "willBeResetDefault": { "type": "boolean", "title": "WillBeResetDefault is if the parameter will be reset to default" } }, "title": "Config is the config of the parameter", "required": [ "instanceType", "instance", "name", "currentValue", "settingValue", "settingValueValid", "type", "edited", "defaultValue", "dynamic", "willBeResetDefault" ] }, "v2ConfigTemplateRequest": { "type": "object", "properties": { "tiupVersion": { "type": "string", "title": "The tiup_version" } }, "title": "ConfigTemplateRequest", "required": ["tiupVersion"] }, "v2ConfigTemplateResponse": { "type": "object", "properties": { "tiupVersion": { "type": "string", "title": "The tiup_version" }, "configTemplate": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/clusterv2ConfigTemplate" }, "title": "The ConfigTemplate" } }, "title": "ConfigTemplateResponse", "required": ["tiupVersion", "configTemplate"] }, "v2ConfirmResponse": { "type": "object", "properties": { "taskId": { "type": "string", "title": "task_id" } }, "title": "ConfirmResponse", "required": ["taskId"] }, "v2CreateApiKeyRequest": { "type": "object", "properties": { "description": { "type": "string", "title": "The description of the apiKey" } }, "title": "CreateApiKey Request", "required": ["description"] }, "v2CreateDomainBody": { "type": "object", "properties": { "name": { "type": "string", "title": "the domain name of the domain" }, "description": { "type": "string", "title": "the domain description of the domain" } }, "title": "CreateDomainBody", "required": ["name"] }, "v2CreateHost": { "type": "object", "properties": { "ips": { "type": "array", "items": { "type": "string" }, "title": "ips" }, "sshPort": { "type": "integer", "format": "int32", "title": "The SSHPort of the Host" }, "credentialId": { "type": "string", "title": "The credential_id of the Host" }, "locationId": { "type": "string", "title": "The locationId of the Host" }, "tagIds": { "type": "array", "items": { "type": "string" }, "title": "The tagIds of the Host" }, "comment": { "type": "string", "title": "The comment of the Host" }, "domainId": { "type": "integer", "format": "int32", "title": "The domain id of the host" } }, "title": "CreateHost" }, "v2CreateParameterTemplateRequest": { "type": "object", "properties": { "parameterTemplate": { "$ref": "#/definitions/v2ParameterTemplate", "title": "The parameter template" }, "templateParameterMappings": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TemplateParameterMapping" }, "title": "The mappings of the parameter template" } }, "title": "CreateParameterTemplate creates a new parameter template", "required": ["parameterTemplate", "templateParameterMappings"] }, "v2Credential": { "type": "object", "properties": { "credentialId": { "type": "string", "title": "the credential id of the credential" }, "userName": { "type": "string", "title": "the user name of the credential" }, "credentialType": { "$ref": "#/definitions/v2CredentialType", "title": "the credential type of the credential" }, "validateType": { "$ref": "#/definitions/v2CredentialValidateType", "title": "the validate type of the credential" }, "credentialName": { "type": "string", "title": "the credential name of the credential" }, "description": { "type": "string", "title": "the description of the credential" }, "hostCredential": { "$ref": "#/definitions/v2HostCredentialObject", "title": "the host credential object" }, "tidbCredential": { "$ref": "#/definitions/v2TiDBCredentialObject", "title": "the tidb cluster credential object" } }, "title": "Credential basic resource", "required": ["userName", "credentialType", "validateType"] }, "v2CredentialType": { "type": "string", "enum": ["CREDENTIAL_TYPE_UNSPECIFIED", "HOST", "TIDB"], "default": "CREDENTIAL_TYPE_UNSPECIFIED", "description": "- CREDENTIAL_TYPE_UNSPECIFIED: resource type unspecified\n - HOST: credential type host\n - TIDB: credential type tidb", "title": "define credential type" }, "v2CredentialValidateType": { "type": "string", "enum": ["CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED", "PASSWORD", "RSAKEY"], "default": "CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED", "description": "- CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED: validate type unspecified\n - PASSWORD: validate by password\n - RSAKEY: validate by rsa key", "title": "define validate type of credential" }, "v2CycleEnumData": { "type": "string", "enum": ["week", "month"], "default": "week", "description": "- week: \nWeek\n - month: \nMonth", "title": "Data of CycleEnum" }, "v2DashboardSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "DashboardSpec represents the Dashboard topology specification in topology.yam" }, "v2DeployClusterResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "DeployResponse represents a request to get process list", "required": ["clusterId", "taskId"] }, "v2DestroyClusterResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "DestroyClusterResponse represents a request to get process list", "required": ["clusterId", "taskId"] }, "v2DetectClusterResponse": { "type": "object", "properties": { "exist": { "type": "boolean", "title": "Whether the cluster exist" } }, "title": "DetectClusterResponse represents the response to detect the if the cluster exist" }, "v2DeviceCode": { "type": "object", "properties": { "deviceCode": { "type": "string", "title": "Device code, separated by comma" } }, "title": "DeviceCode" }, "v2Disk": { "type": "object", "properties": { "path": { "type": "string", "title": "host resource\nThe Name of the Host Disk" }, "totalSize": { "type": "integer", "format": "int32", "title": "The Size of the Host Disk" }, "usedSpace": { "type": "integer", "format": "int32", "title": "The Used of the Host Disk" }, "availableSpace": { "type": "integer", "format": "int32", "title": "The Avail of the Host Disk" }, "mountingDir": { "type": "string", "title": "The Mounted of the Host Disk" }, "diskType": { "type": "string", "enum": ["HDD", "SSD"], "title": "disk type (e.g., \"HDD\", \"SSD\")" } }, "title": "Disk" }, "v2Domain": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "the domain id of the domain" }, "name": { "type": "string", "title": "the domain name of the domain" }, "description": { "type": "string", "title": "the domain description of the domain" }, "createTime": { "type": "string", "format": "date-time", "title": "create time" }, "updateTime": { "type": "string", "format": "date-time", "title": "update time" } }, "title": "Domain", "required": ["name", "createTime", "updateTime"] }, "v2DomainWithCMServer": { "type": "object", "properties": { "domain": { "$ref": "#/definitions/v2Domain", "title": "domain basic resource" }, "servers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2BindCMServer" }, "title": "cm servers in the domain" } }, "title": "DomainWithCMServer" }, "v2DownloadAuditLogsResponse": { "type": "object", "properties": { "data": { "type": "string", "format": "byte", "title": "data" } }, "title": "Response for audit log download" }, "v2DownloadHostTemplateResponse": { "type": "object", "properties": { "data": { "type": "string", "format": "byte", "title": "data" } }, "title": "DownloadHostTemplateResponse" }, "v2DownloadListClustersResponse": { "type": "object", "properties": { "data": { "type": "string", "format": "byte", "title": "data" } }, "title": "DownloadListClustersResponse" }, "v2DownloadListHostResponse": { "type": "object", "properties": { "data": { "type": "string", "format": "byte", "title": "data" } }, "title": "DownloadListHostResponse" }, "v2DownloadRSAKeyResponse": { "type": "object", "properties": { "data": { "type": "string", "format": "byte", "title": "the data of file" } }, "title": "DownLoadRSAKey Response" }, "v2DrainerSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "DrainerSpec represents the Dashboard topology specification in topology.yam" }, "v2EmailConfig": { "type": "object", "properties": { "smtpAddress": { "type": "string", "title": "SMTP server address" }, "smtpUsername": { "type": "string", "title": "SMTP username" }, "smtpPassword": { "type": "string", "title": "SMTP password" }, "emailFrom": { "type": "string", "title": "Email sender address" }, "emailTo": { "type": "string", "title": "Email recipient address" }, "sender": { "type": "string", "title": "Sender" }, "emailSubject": { "type": "string", "title": "Email subject template" }, "emailTemplate": { "type": "string", "title": "Email content template" } }, "title": "EmailConfig represents email channel configuration", "required": [ "smtpAddress", "smtpUsername", "smtpPassword", "emailFrom", "emailTo", "sender", "emailSubject", "emailTemplate" ] }, "v2ErrorDetail": { "type": "object", "properties": { "type": { "type": "string", "title": "The error detail type" }, "locale": { "type": "string", "title": "the languages used in i18n" }, "message": { "type": "string", "title": "The i18n message of the error" } }, "title": "ErrorDetail" }, "v2Event": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "id" }, "alertName": { "type": "string", "title": "Name of the alert" }, "level": { "type": "string", "title": "Severity level of the alert" }, "instance": { "type": "string", "title": "Instance where the alert occurred" }, "status": { "type": "string", "title": "Current status of the alert" }, "summary": { "type": "string", "title": "Brief summary of the alert" }, "description": { "type": "string", "title": "Detailed description of the alert" }, "startTime": { "type": "string", "format": "date-time", "title": "Time when the alert started" }, "resolvedTime": { "type": "string", "format": "date-time", "title": "Time when the alert was resolved" }, "updateTime": { "type": "string", "format": "date-time", "title": "Time when the alert was last updated" }, "silenceStartTime": { "type": "string", "format": "date-time", "title": "Silence start time" }, "silenceEndTime": { "type": "string", "format": "date-time", "title": "Silence end time" }, "expr": { "type": "string", "title": "promql of the alert" }, "value": { "type": "string", "title": "Value of the alert" }, "operator": { "type": "string", "title": "Operator of the alert" }, "application": { "type": "string", "title": "Application" }, "objectType": { "type": "string", "title": "Object Type" }, "alertObject": { "type": "string", "title": "Alert Object" } }, "title": "AlertEvent represents an alert event" }, "v2EventsOverview": { "type": "object", "properties": { "levelStats": { "$ref": "#/definitions/v2LevelStatistics", "title": "Statistics by alert level" }, "statusStats": { "$ref": "#/definitions/v2StatusStatistics", "title": "Statistics by alert status" } }, "title": "GetAlertEventsOverviewResponse represents the response containing alert events overview" }, "v2ExprQueryData": { "type": "object", "properties": { "expr": { "type": "string", "title": "The expression used in the query" }, "legend": { "type": "string", "title": "The legend associated with the expression" }, "prometheusAddress": { "type": "string", "title": "prometheus address" }, "result": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2QueryResult" }, "title": "The results of the query" } }, "title": "ExprQueryData represents the data for an expression query" }, "v2ExpressionWithLegend": { "type": "object", "properties": { "name": { "type": "string", "title": "Expression name" }, "promql": { "type": "string", "title": "PromQL expression" }, "promMetric": { "type": "string", "title": "Prometheus metric name" }, "labels": { "type": "array", "items": { "type": "string" }, "title": "List of labels associated with the expression" }, "type": { "type": "string", "title": "Type of the expression" }, "legend": { "type": "string", "title": "Legend name for the expression" }, "minTidbVersion": { "type": "string", "title": "Minimum supported TiDB version" }, "maxTidbVersion": { "type": "string", "title": "Maximum supported TiDB version" } }, "title": "ExpressionWithLegend represents an expression with its legend" }, "v2GenerateRSAKeyRequest": { "type": "object", "title": "GenerateRSAKey Request" }, "v2GenerateRSAKeyResponse": { "type": "object", "properties": { "publicKey": { "type": "string", "title": "the public key of the rsa key" }, "privateKey": { "type": "string", "title": "the private key of the rsa key" } }, "title": "GenerateRSAKey response" }, "v2GetCMServerWithTiupResponse": { "type": "object", "properties": { "cmServer": { "$ref": "#/definitions/v2CMServerWithTiup", "title": "CM server with tiup" } }, "title": "GetCMServerWithTiupResponse" }, "v2GetClusterTopologyResponse": { "type": "object", "properties": { "clusterTopology": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ClusterTopology" }, "title": "the cluster_topology" }, "clusterName": { "type": "string", "title": "cluster_name" } }, "title": "GetClusterTopologyResponse", "required": ["clusterTopology", "clusterName"] }, "v2GetParameterTemplateResponse": { "type": "object", "properties": { "parameterTemplate": { "$ref": "#/definitions/v2ParameterTemplate", "title": "The parameter template" }, "templateParameterMappings": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TemplateParameterMapping" }, "title": "The mappings of the parameter template" } }, "title": "GetParameterTemplateResponse represents a get parameter template response", "required": ["parameterTemplate", "templateParameterMappings"] }, "v2GetTagWithBindingsResponse": { "type": "object", "properties": { "tag": { "$ref": "#/definitions/v2TagWithBindObject", "title": "tag info" } }, "title": "GetTagWithBindings Response" }, "v2GetTidbVersionsResponse": { "type": "object", "properties": { "tidbVersions": { "type": "array", "items": { "type": "string" }, "title": "the cluster_topology" }, "tiupId": { "type": "string", "title": "tiup_id" }, "ip": { "type": "string", "title": "tiup_id" }, "tiupHome": { "type": "string", "title": "tiup_home" } }, "title": "GetTidbVersionsResponse", "required": ["tiupId"] }, "v2GlobalOptions": { "type": "object", "properties": { "user": { "type": "string", "title": "user" }, "group": { "type": "string", "title": "group" }, "sshPort": { "type": "integer", "format": "int32", "title": "ssh_port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "os": { "type": "string", "title": "os" }, "arch": { "type": "string", "title": "arch" } }, "title": "GlobalOptions" }, "v2GrafanaSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "GrafanaSpec" }, "v2GroupEnumData": { "type": "string", "enum": [ "unspecified", "overview", "basic", "advanced", "resource", "performance", "process" ], "default": "unspecified", "description": "- unspecified: Unspecified group\n - overview: Overview group\n - basic: Basic group\n - advanced: Advanced group\n - resource: Resource group\n - performance: Performance group\n - process: Process group", "title": "Data of GroupEnum" }, "v2Host": { "type": "object", "properties": { "hostId": { "type": "string", "title": "The host_id of the Host" }, "ip": { "type": "string", "title": "The ip of the Host" }, "hostName": { "type": "string", "title": "The host_name of the Host" }, "sshPort": { "type": "integer", "format": "int32", "title": "The ssh_port of the Host" }, "status": { "type": "string", "enum": ["initializing", "deleting", "deleted", "used", "idle"], "title": "host Status (e.g., \"initializing\", \"used\")" }, "connectionStatus": { "type": "string", "enum": ["online", "offline"], "title": "host connection Status (e.g., \"online\", \"offline\")" }, "checkStatus": { "type": "string", "enum": ["checking", "failed", "warning", "succeeded"], "title": "host check (e.g., \"checking\", \"failed\")" }, "credentialId": { "type": "string", "title": "The credential_id of the Host" }, "reportId": { "type": "string", "title": "The report_id of the Host" }, "osName": { "type": "string", "title": "os_name" }, "osVendor": { "type": "string", "title": "os_vendor" }, "osVersion": { "type": "string", "title": "os_version" }, "osRelease": { "type": "string", "title": "os_release" }, "osArchitecture": { "type": "string", "title": "os_architecture" }, "cpuVendor": { "type": "string", "title": "cpu_vendor" }, "cpuModel": { "type": "string", "title": "cpu_model" }, "cpuSpeed": { "type": "integer", "format": "int32", "title": "cpu_speed" }, "cpuCache": { "type": "integer", "format": "int32", "title": "cpu_cache" }, "cpus": { "type": "integer", "format": "int32", "title": "cpus" }, "cpuThreads": { "type": "integer", "format": "int32", "title": "cpu_threads" }, "cpuGovernor": { "type": "string", "title": "cpu_governor" }, "cpuArch": { "type": "string", "title": "cpu_arch" }, "memoryType": { "type": "string", "title": "memory_type" }, "memorySpeed": { "type": "integer", "format": "int32", "title": "memory_speed" }, "memorySize": { "type": "integer", "format": "int32", "title": "memory_size" }, "memorySwap": { "type": "integer", "format": "int32", "title": "memory_swap" }, "cpuNumaNodes": { "type": "integer", "format": "int32", "title": "The CpuNumaNodes of the Host" }, "storageTotalSize": { "type": "integer", "format": "int32", "title": "The Storage of the Host" }, "storageAvailable": { "type": "integer", "format": "int32", "title": "storage_available" }, "storageUsed": { "type": "integer", "format": "int32", "title": "storage_used" }, "diskType": { "type": "string", "title": "The DiskType of the Host" }, "clusters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2AssociatedClusters" }, "title": "The Clusters of the Host" }, "nodeExporterPort": { "type": "integer", "format": "int32", "title": "Other info\nThe node_exporter_port of the Host" }, "tiupIds": { "type": "array", "items": { "type": "string" }, "title": "The tiup_ids of the Host" }, "hostType": { "type": "string", "enum": ["VM", "PM"], "title": "host Type (e.g., \"VM\", \"PM\")" }, "comment": { "type": "string", "title": "The HostComment_Id of the Host" }, "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Tags" }, "title": "The Tag of the Host" }, "createdTime": { "type": "string", "format": "date-time", "title": "created_time" }, "updatedTime": { "type": "string", "format": "date-time", "title": "updated_time" }, "credential": { "$ref": "#/definitions/v2Credential", "title": "credential" }, "locationMappings": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2LocationMappings" }, "title": "The Status of the Host\nlocation_mappings" }, "memoryUnit": { "type": "string", "title": "memory_unit" }, "storageUnit": { "type": "string", "title": "storage_unit" }, "locationId": { "type": "string", "title": "location_id" }, "cpuCores": { "type": "integer", "format": "int32", "title": "cpu_cores" }, "domainId": { "type": "integer", "format": "int32", "title": "domain id" }, "domainName": { "type": "string", "title": "domain name" } }, "title": "Hosts resource", "required": ["hostId"] }, "v2HostCheckResponse": { "type": "object", "properties": { "hostId": { "type": "string", "title": "task_id" }, "taskId": { "type": "string", "title": "task_id" }, "reportId": { "type": "string", "title": "importId" } }, "title": "HostCheckResponse", "required": ["hostId", "taskId", "reportId"] }, "v2HostCreateResponse": { "type": "object", "properties": { "taskId": { "type": "string", "title": "task_id" } }, "title": "HostCreate Response" }, "v2HostCredentialObject": { "type": "object", "properties": { "password": { "type": "string", "title": "the password of the user" }, "publicKey": { "type": "string", "title": "the public key of the user" }, "privateKey": { "type": "string", "title": "the private key of the user" }, "hostIps": { "type": "array", "items": { "type": "string" }, "title": "the list of host ips bound with current credential" } }, "title": "Host credential object" }, "v2HostDiskResponse": { "type": "object", "properties": { "disk": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Disk" }, "title": "Disk" } }, "title": "Disk Response" }, "v2HostFixResponse": { "type": "object", "properties": { "hostId": { "type": "string", "title": "task_id" }, "taskId": { "type": "string", "title": "task_id" }, "reportId": { "type": "string", "title": "importId" } }, "title": "HostFixResponse", "required": ["hostId", "taskId", "reportId"] }, "v2HostMetricData": { "type": "object", "properties": { "status": { "type": "string", "title": "Response Status (e.g., \"success\", \"error\")" }, "data": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ExprQueryData" }, "title": "Response Data containing the queried metrics" } }, "title": "HostMetricData represents the response for querying cluster metric data" }, "v2HostServiceUpdateHostBody": { "type": "object", "properties": { "host": { "$ref": "#/definitions/hostv2UpdateHost", "title": "host resource" } }, "title": "Create Request" }, "v2HostTask": { "type": "object", "properties": { "taskId": { "type": "string", "title": "importId" }, "hostId": { "type": "string", "title": "host_id" }, "reportId": { "type": "string", "title": "report_id" }, "ip": { "type": "string", "title": "ip" }, "userName": { "type": "string", "title": "user_name" }, "sshPort": { "type": "integer", "format": "int32", "title": "ssh_port" }, "status": { "type": "string", "enum": ["init", "existed", "succeeded", "failed"], "title": "create task state (e.g., \"init\", \"existed\")" }, "tags": { "type": "string", "title": "tags" }, "locationId": { "type": "string", "title": "location_id" }, "credentialId": { "type": "string", "title": "credential_id" }, "hostName": { "type": "string", "title": "host_name" }, "tagsList": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Tags" }, "title": "tags" }, "locationMappings": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2LocationMappings" }, "title": "location_mappings" }, "credential": { "$ref": "#/definitions/v2Credential", "title": "Credential" } }, "title": "HostTask", "required": ["taskId", "hostId", "ip", "sshPort"] }, "v2HostTiDBProcessesResponse": { "type": "object", "properties": { "tiDBProcesses": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TiDBProcesses" }, "title": "tiDBProcesses" } }, "title": "HostTiDBProcessesResponse Response" }, "v2ImportRequest": { "type": "object", "properties": { "hostData": { "type": "string", "format": "binary", "description": "Upload a csv form data to host.", "title": "host_data" }, "fileName": { "type": "string", "title": "file_name" }, "credentialId": { "type": "string", "title": "The credential_id of the Import" }, "domainId": { "type": "integer", "format": "int32", "title": "domain id" }, "headers": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The header of the license file" } }, "title": "Import Request", "required": ["hostData", "headers"] }, "v2ImportTaskResponse": { "type": "object", "properties": { "task": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2HostTask" }, "title": "List of users" }, "taskId": { "type": "string", "title": "importId" } }, "title": "ImportTaskResponse", "required": ["task", "taskId"] }, "v2LevelStatistics": { "type": "object", "properties": { "emergencyCount": { "type": "integer", "format": "int32", "title": "Number of emergency alerts" }, "criticalCount": { "type": "integer", "format": "int32", "title": "Number of critical alerts" }, "warningCount": { "type": "integer", "format": "int32", "title": "Number of warning alerts" }, "totalCount": { "type": "integer", "format": "int32", "title": "Total number of alerts" } }, "title": "LevelStatistics represents statistics by alert level" }, "v2LicenseStatusEnumData": { "type": "string", "enum": ["active", "expired", "expiring", "invalid", "revoked"], "default": "active", "description": "- active: active\n - expired: inactive\n - expiring: expired\n - invalid: invalid\n - revoked: revoked", "title": "Data of LicenseStatusEnum" }, "v2LicenseTypeEnumData": { "type": "string", "enum": ["free", "ultimate"], "default": "free", "description": "- free: free\n - ultimate: ultimate", "title": "Data of TriggerTypeEnum" }, "v2ListApiKeysResponse": { "type": "object", "properties": { "apikeys": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ApiKey" }, "title": "List of users" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListApiKeyRequest" }, "v2ListBRTasksResponse": { "type": "object", "properties": { "brTasks": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2BRTask" }, "title": "List of br tasks" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListBRTasksResponse represents the response to list br tasks" }, "v2ListBackupPoliciesResponse": { "type": "object", "properties": { "backupPolicies": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2BackupPolicy" }, "title": "List of br policies" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListBRPoliciesResponse represents the response to get br policies" }, "v2ListCMServersResponse": { "type": "object", "properties": { "servers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2CMServer" }, "title": "list of tiups" }, "nextPageToken": { "type": "string", "title": "next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "total size" } }, "title": "ListCMServers Response" }, "v2ListChannelsResponse": { "type": "object", "properties": { "channels": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Channel" }, "title": "List of alert channels" }, "nextPageToken": { "type": "string", "description": "Token for the next page of results." }, "totalSize": { "type": "integer", "format": "int32", "description": "The total number of users that match the filter criteria." } }, "title": "GetAlertChannelsResponse represents a response containing alert channels" }, "v2ListClusterBRTasksResponse": { "type": "object", "properties": { "brTasks": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ClusterBRTask" }, "title": "List of br tasks" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListClusterBRTasksResponse represents the response to list br tasks for a specific cluster" }, "v2ListClusterBackupRecordsResponse": { "type": "object", "properties": { "backupRecords": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ClusterBRTask" }, "title": "List of valid full backup records" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListClusterBackupRecordsResponse represents the response to list valid full backup records for a specific cluster" }, "v2ListClusterConfigsResponse": { "type": "object", "properties": { "configs": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Config" }, "title": "Configs is the list of configs" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListClusterConfigsResponse is the response message for ListClusterConfigs", "required": ["configs"] }, "v2ListClusterVariablesResponse": { "type": "object", "properties": { "variables": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Variable" }, "title": "Variables is the list of variables" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListClusterVariablesResponse is the response message for ListClusterVariables", "required": ["variables"] }, "v2ListClustersResponse": { "type": "object", "properties": { "clusters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Clusters" }, "title": "clusters uniquely identifies the target cluster" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListClustersRequest represents a request to get process list", "required": ["clusters", "nextPageToken", "totalSize"] }, "v2ListCredentialsResponse": { "type": "object", "properties": { "credentials": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Credential" }, "title": "list of credentials" }, "nextPageToken": { "type": "string", "title": "next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "total size" } }, "title": "ListCredentials Response" }, "v2ListDomainsResponse": { "type": "object", "properties": { "domains": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Domain" }, "title": "list of domains" }, "nextPageToken": { "type": "string", "title": "next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "total size" } }, "title": "ListDomains Response" }, "v2ListDomainsWithCMServerResponse": { "type": "object", "properties": { "domains": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2DomainWithCMServer" }, "title": "list of domains" }, "nextPageToken": { "type": "string", "title": "next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "total size" } }, "title": "ListDomainsWithCMServer Response" }, "v2ListEventsResponse": { "type": "object", "properties": { "events": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Event" }, "title": "List of alert events" }, "nextPageToken": { "type": "string", "description": "Token for the next page of results." }, "totalSize": { "type": "integer", "format": "int32", "description": "The total number of users that match the filter criteria." } }, "title": "GetAlertEventsResponse represents a response containing alert events" }, "v2ListHostsResponse": { "type": "object", "properties": { "hosts": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Host" }, "title": "List of users" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListResponse", "required": ["hosts", "nextPageToken", "totalSize"] }, "v2ListLocationsResponse": { "type": "object", "properties": { "locations": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Locations" }, "title": "list of Locations" }, "nextPageToken": { "type": "string", "title": "next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "total size" } }, "title": "ListLocations Response" }, "v2ListMonitorObjectsResponse": { "type": "object", "properties": { "monitorObjects": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2MonitorObject" }, "title": "The list of monitor objects" } }, "title": "ListMonitorObjectsResponse defines the response containing all monitor objects" }, "v2ListObjectRulesResponse": { "type": "object", "properties": { "rules": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ObjectRule" }, "title": "The list of object rules" }, "nextPageToken": { "type": "string", "title": "Token for the next page of results" }, "totalSize": { "type": "integer", "format": "int32", "title": "The total number of rules" } }, "title": "ListObjectRulesResponse defines the response containing a list of object rules" }, "v2ListParameterTemplatesResponse": { "type": "object", "properties": { "parameterTemplates": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ParameterTemplate" }, "title": "The parameter templates" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListParameterTemplatesResponse represents a list parameter templates response", "required": ["parameterTemplates"] }, "v2ListParametersResponse": { "type": "object", "properties": { "parameters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Parameter" }, "title": "The parameters" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListParametersResponse represents a list parameters response", "required": ["parameters"] }, "v2ListRolesResponse": { "type": "object", "properties": { "roles": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Role" }, "title": "List of users" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "ListRolesResponse" }, "v2ListRulesResponse": { "type": "object", "properties": { "rules": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Rule" }, "title": "The list of rules" }, "nextPageToken": { "type": "string", "title": "Token for the next page of results" }, "totalSize": { "type": "integer", "format": "int32", "title": "The total number of rules" } }, "title": "ListRulesResponse defines the response containing a list of rules" }, "v2ListTagKeysResponse": { "type": "object", "properties": { "tagKeys": { "type": "array", "items": { "type": "string" }, "title": "list of tag keys" }, "nextPageToken": { "type": "string", "title": "next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "total size" } }, "title": "ListTagKeys Response" }, "v2ListTagsByResourceTypeResponse": { "type": "object", "properties": { "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/tagv2Tag" }, "title": "list of tags" }, "nextPageToken": { "type": "string", "title": "next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "total size" } }, "title": "ListTagsByResourceType Response" }, "v2ListTagsResponse": { "type": "object", "properties": { "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/tagv2Tag" }, "title": "list of tags" }, "nextPageToken": { "type": "string", "title": "next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "total size" } }, "title": "ListTags Response" }, "v2ListTagsWithBindingsResponse": { "type": "object", "properties": { "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TagWithBindObject" }, "title": "list of tags" }, "nextPageToken": { "type": "string", "title": "next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "total size" } }, "title": "ListTagsWithBindings Response" }, "v2ListTaskFlowsResponse": { "type": "object", "properties": { "taskFlows": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TaskFlow" }, "description": "The list of tasks retrieved." }, "nextPageToken": { "type": "string", "description": "Token for the next page of results." }, "totalSize": { "type": "integer", "format": "int32", "description": "The total number of tasks that match the filter criteria." } }, "description": "ListTasksResponse defines the response containing a list of tasks and pagination information." }, "v2ListTemplateRulesResponse": { "type": "object", "properties": { "templateRules": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TemplateRule" }, "title": "Alert rules" }, "nextPageToken": { "type": "string", "title": "Token for retrieving the next page of results" }, "totalSize": { "type": "integer", "format": "int32", "title": "The total number of rules that match the filter criteria" }, "templateId": { "type": "integer", "format": "int32", "title": "Template ID" } }, "title": "AlertTemplateRules represents a response containing alert template rules" }, "v2ListTemplatesResponse": { "type": "object", "properties": { "templates": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TemplateWithOutRules" }, "title": "List of alert templates" }, "nextPageToken": { "type": "string", "title": "Token for retrieving the next page of results" }, "totalSize": { "type": "integer", "format": "int32", "title": "The total number of templates that match the filter criteria" } }, "title": "GetTemplatesResponse represents a response containing alert templates" }, "v2ListTiupsResponse": { "type": "object", "properties": { "tiups": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Tiups" }, "title": "list of tiups" }, "nextPageToken": { "type": "string", "title": "next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "total size" } }, "title": "ListTiups Response" }, "v2ListUsersResponse": { "type": "object", "properties": { "users": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2User" }, "description": "The list of users retrieved." }, "nextPageToken": { "type": "string", "description": "Token for the next page of results." }, "totalSize": { "type": "integer", "format": "int32", "description": "The total number of users that match the filter criteria." } }, "description": "ListUsersResponse defines the response containing a list of users and pagination information." }, "v2LocationMappings": { "type": "object", "properties": { "locationId": { "type": "string", "title": "the Location id of the Location" }, "parentId": { "type": "string", "title": "the Location key of the Location" }, "locationKey": { "type": "string", "title": "the Location value of the Location" }, "locationValue": { "type": "string", "title": "the Location value of the Location" } }, "title": "LocationMappings", "required": ["locationKey", "locationValue"] }, "v2Locations": { "type": "object", "properties": { "locationId": { "type": "string", "title": "the Location id of the Location" }, "parentId": { "type": "string", "title": "the Location key of the Location" }, "locationKey": { "type": "string", "enum": ["zone", "dc", "rack"], "title": "location key (e.g., \"zone\", \"dc\")" }, "locationValue": { "type": "string", "title": "the Location value of the Location" } }, "title": "Location basic resource" }, "v2LoginRequest": { "type": "object", "properties": { "userId": { "type": "string", "title": "The id of the user" }, "password": { "type": "string", "title": "The password of the user" } }, "title": "Login Request", "required": ["userId"] }, "v2MetricWithExpressions": { "type": "object", "properties": { "name": { "type": "string", "title": "Metric name" }, "unit": { "type": "string", "title": "Unit of the metric" }, "description": { "type": "string", "title": "Description of the metric" }, "minTidbVersion": { "type": "string", "title": "Minimum supported TiDB version" }, "maxTidbVersion": { "type": "string", "title": "Maximum supported TiDB version" }, "isBuiltin": { "type": "boolean", "title": "Whether it is a built-in metric" }, "expressions": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ExpressionWithLegend" }, "title": "List of associated expressions" } }, "title": "MetricWithExpressions represents a metric with its expressions" }, "v2Metrics": { "type": "object", "properties": { "metrics": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2CategoryMetricDetail" }, "title": "List of metrics info" } }, "title": "Metrics represents the list of metrics info" }, "v2MonitorObject": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "The unique identifier of the alert rule" }, "application": { "type": "string", "title": "The monitor object application" }, "objectType": { "type": "string", "title": "The type of the monitor object" } }, "title": "MonitorObject resource" }, "v2NodesResponse": { "type": "object", "properties": { "clusterInstances": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ClusterInstances" }, "title": "The cluster_instances list" }, "clusterId": { "type": "string", "title": "The cluster_id" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "// NodesResponse", "required": [ "clusterInstances", "clusterId", "nextPageToken", "totalSize" ] }, "v2ObjectRule": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "The unique identifier of the alert rule" }, "createTime": { "type": "string", "format": "date-time", "title": "The creation timestamp of the alert rule", "readOnly": true }, "updateTime": { "type": "string", "format": "date-time", "title": "The last updated timestamp of the alert rule", "readOnly": true }, "name": { "type": "string", "title": "The name of the alert rule" }, "expr": { "type": "string", "title": "The Prometheus expression for the rule" }, "status": { "type": "string", "title": "The status of the alert rule" }, "metricId": { "type": "integer", "format": "int32", "title": "The associated metric ID" }, "monitorCategoryIds": { "type": "array", "items": { "type": "integer", "format": "int32" }, "title": "the category id of the rule" }, "monitorObject": { "$ref": "#/definitions/v2MonitorObject", "title": "MonitorObject resource" }, "duration": { "type": "integer", "format": "int32", "title": "The rule duration in seconds" }, "labels": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The labels of the Prometheus rule" }, "annotations": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The annotations of the Prometheus rule" }, "level": { "type": "string", "enum": ["warning", "critical", "emergency"], "title": "The alert level" }, "alertObject": { "type": "string", "title": "The TiDB version for which the rule is applicable" }, "creator": { "type": "string", "title": "The creator of the rule" } }, "title": "AlertRule resource", "required": [ "id", "name", "expr", "labels", "annotations", "level", "alertObject" ] }, "v2OfflineClusterResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" } }, "title": "OfflineClusterResponse represents a request to get process list", "required": ["clusterId"] }, "v2OverviewStatus": { "type": "object", "properties": { "clusters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2StatusCount" }, "title": "List of clusters' status" }, "hosts": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2StatusCount" }, "title": "List of hosts' status" }, "alerts": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2StatusCount" }, "title": "List of alerts' status" }, "alertLevels": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2StatusCount" }, "title": "List of alert levels' status" }, "brTasks": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2StatusCount" }, "title": "List of backup \u0026 restore tasks' status" }, "sysTasks": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2StatusCount" }, "title": "List of system tasks' status" }, "otherTasks": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2StatusCount" }, "title": "List of other tasks' status" } }, "title": "OverviewStatus represents the response for querying overview data" }, "v2PDSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "clientPort": { "type": "integer", "format": "int32", "title": "port" }, "peerPort": { "type": "integer", "format": "int32", "title": "status_port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "numaCores": { "type": "string", "title": "numa_cores" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" }, "name": { "type": "string", "title": "name" } }, "title": "PDSpec represents the PD topology specification in topology.yaml" }, "v2ParamBase": { "type": "object", "properties": { "name": { "type": "string", "title": "The name of the variable" }, "instanceType": { "type": "string", "title": "The instance type of the variable" }, "configSource": { "type": "string", "title": "The parameter instance type. e.g.: CLUSTER_CONFIG, GLOBAL_VARIABLES" } }, "title": "ConfigBase represents a config base" }, "v2ParamTypeEnumData": { "type": "string", "enum": ["INT", "STRING", "BOOL", "FLOAT", "ARRAY"], "default": "INT", "description": "- INT: int\n - STRING: string\n - BOOL: bool\n - FLOAT: float\n - ARRAY: array", "title": "Data is the type of the parameter" }, "v2Parameter": { "type": "object", "properties": { "id": { "type": "string", "format": "int64", "title": "The ID of the parameter" }, "name": { "type": "string", "title": "The name of the parameter" }, "configSource": { "type": "string", "title": "The parameter instance type. e.g.: CLUSTER_CONFIG, GLOBAL_VARIABLES" }, "instanceType": { "type": "string", "title": "The parameter instance type. e.g.: TiDB, TiKV, PD" }, "type": { "type": "integer", "format": "int32", "title": "The type of the parameter" }, "defaultValue": { "type": "string", "title": "The default value of the parameter" }, "range": { "type": "string", "title": "The range of the parameter" }, "version": { "type": "string", "title": "The version of the parameter" }, "dynamic": { "type": "boolean", "title": "Whether the parameter is dynamic" } }, "title": "Parameter represents a parameter", "required": [ "id", "name", "configSource", "instanceType", "type", "defaultValue", "range", "version", "dynamic" ] }, "v2ParameterTemplate": { "type": "object", "properties": { "id": { "type": "string", "format": "int64", "title": "The ID of the parameter template" }, "name": { "type": "string", "title": "The name of the parameter template" }, "type": { "type": "string", "title": "The type of the parameter template, e.g.: TiDB" }, "parentId": { "type": "string", "title": "The parent ID of the parameter template" }, "clusterSpec": { "type": "string", "title": "The cluster specification of the parameter template" }, "hasDefault": { "type": "integer", "format": "int32", "title": "Whether the parameter template is the default" }, "dbType": { "type": "integer", "format": "int32", "title": "The database type of the parameter template" }, "templateType": { "type": "integer", "format": "int32", "title": "The parameter template type, e.g.: 1: system, 2: custom" }, "clusterVersion": { "type": "string", "title": "The cluster version of the parameter template, e.g.: v6.1" }, "note": { "type": "string", "title": "The note of the parameter template" } }, "title": "ParameterTemplate represents a parameter template", "required": ["name", "type", "templateType", "clusterVersion", "note"] }, "v2PauseClusterResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "DeployResponse represents a request to get process list", "required": ["clusterId", "taskId"] }, "v2PreCheckBackupPolicyResponse": { "type": "object", "properties": { "clusters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2Cluster" }, "title": "Clusters that already have backup policies" } }, "title": "PreCheckBackupPolicyResponse represents the conflict clusters which already have backup policies" }, "v2ProcessList": { "type": "object", "properties": { "clusterProcessList": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ClusterProcess" }, "title": "List of cluster processes containing detailed information about each process" }, "isSupportKill": { "type": "boolean", "title": "Whether kill operation is supported by the cluster\nDepends on TiDB version and cluster configuration" }, "totalProcessCount": { "type": "string", "format": "int64", "title": "Total number of processes in the cluster\nIncludes both active and sleeping processes" }, "activeProcessCount": { "type": "string", "format": "int64", "title": "Number of active processes in the cluster\nExcludes processes in \"sleep\" state" } }, "title": "ProcessList represents the response of process list query" }, "v2PrometheusSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "ngPort": { "type": "integer", "format": "int32", "title": "ng_port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "remoteConfig": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "PrometheusSpec" }, "v2PumpSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "PumpSpec represents the Dashboard topology specification in topology.yam" }, "v2QueryMetric": { "type": "object", "properties": { "instance": { "type": "string", "title": "Instance of the metric" }, "sqlType": { "type": "string", "title": "SQL type of the metric" }, "type": { "type": "string", "title": "Type of the metric" }, "result": { "type": "string", "title": "Result of the metric" }, "txnMode": { "type": "string", "title": "Transaction mode of the metric" }, "job": { "type": "string", "title": "Job type of the metric" }, "device": { "type": "string", "title": "Device type of the metric" }, "fstype": { "type": "string", "title": "FSType of the metric" }, "mountpoint": { "type": "string", "title": "MountPoint of the metric" }, "module": { "type": "string", "title": "Module of the metric" }, "kind": { "type": "string", "title": "Kind of the metric" }, "ping": { "type": "string", "title": "Ping of the metric" }, "le": { "type": "string", "title": "Le of the metric" }, "to": { "type": "string", "title": "To of the metric" }, "cf": { "type": "string", "title": "cf of the metric" }, "store": { "type": "string", "title": "store of the metric" } }, "title": "QueryMetric represents the metric details in the query result" }, "v2QueryResult": { "type": "object", "properties": { "metric": { "$ref": "#/definitions/v2QueryMetric", "title": "Metric details" }, "values": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/metricsv2Value" }, "title": "Values associated with the metric" } }, "title": "QueryResult represents the result of a query" }, "v2ReloadClusterResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "ReloadClusterResponse represents a request to get process list", "required": ["clusterId", "taskId"] }, "v2ReportResponse": { "type": "object", "properties": { "taskId": { "type": "string", "title": "task_id" }, "taskState": { "type": "string", "enum": ["init", "running", "success", "fail"], "title": "check task state (e.g., \"init\", \"running\")" }, "reports": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/hostv2Report" }, "title": "List of reports" } }, "title": "ReportResponse", "required": ["taskId"] }, "v2ResetSecretKeyResponse": { "type": "object", "properties": { "accessKey": { "type": "string", "title": "The access key of the apiKey" }, "secretKey": { "type": "string", "title": "The secret key of the apiKey" } }, "title": "ResetSecretKey Request", "required": ["accessKey", "secretKey"] }, "v2ResourceGroup": { "type": "object", "properties": { "name": { "type": "string", "title": "Resource group name" }, "ruPerSec": { "type": "string", "title": "RU per second" }, "priority": { "type": "string", "title": "Priority" }, "burstable": { "type": "string", "title": "Burstable" } }, "title": "Resource group information" }, "v2ResourceGroupList": { "type": "object", "properties": { "resourceGroups": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ResourceGroup" }, "title": "List of resource groups" } }, "title": "Response message for GetResourceGroupList" }, "v2ResourceObject": { "type": "object", "properties": { "resourceId": { "type": "string", "title": "the resource id of the resource object" }, "resourceName": { "type": "string", "title": "the resource name of the resource object" } }, "title": "Resource object", "required": ["resourceName"] }, "v2RestartClusterResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "RestartCluster represents a request to get process list", "required": ["clusterId", "taskId"] }, "v2RestartTaskFlowResponse": { "type": "object", "properties": { "taskId": { "type": "string", "description": "The task_id of the task flow." }, "status": { "type": "boolean", "description": "The status of the task flow." } }, "title": "RestartTaskFlowResponse", "required": ["taskId", "status"] }, "v2ResumeClusterResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "ResumeCluster represents a request to get process list", "required": ["clusterId", "taskId"] }, "v2RetryTaskFlowResponse": { "type": "object", "properties": { "taskId": { "type": "string", "title": "The task_id" } }, "title": "RetryTaskFlowResponse", "required": ["taskId"] }, "v2Role": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "The id of the role" }, "roleName": { "type": "string", "title": "The name of the role" }, "roleType": { "type": "number", "format": "double", "title": "The id of the role" }, "roleTypeDesc": { "type": "string", "title": "The id of the role" }, "detail": { "type": "string", "title": "The note of the role" }, "note": { "type": "string", "title": "The note of the role" }, "createTime": { "type": "string", "format": "date-time", "title": "The create time of the role", "readOnly": true }, "updateTime": { "type": "string", "format": "date-time", "title": "The update time of the role", "readOnly": true } }, "title": "the role resource" }, "v2Rule": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "The unique identifier of the alert rule" }, "createTime": { "type": "string", "format": "date-time", "title": "The creation timestamp of the alert rule", "readOnly": true }, "updateTime": { "type": "string", "format": "date-time", "title": "The last updated timestamp of the alert rule", "readOnly": true }, "name": { "type": "string", "title": "The name of the alert rule" }, "expr": { "type": "string", "title": "The Prometheus expression for the rule" }, "metricId": { "type": "integer", "format": "int32", "title": "The associated metric ID" }, "monitorObject": { "$ref": "#/definitions/v2MonitorObject", "title": "MonitorObject resource" }, "monitorCategoryIds": { "type": "array", "items": { "type": "integer", "format": "int32" }, "title": "the category id of the rule" }, "duration": { "type": "integer", "format": "int32", "title": "The rule duration in seconds" }, "labels": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The labels of the Prometheus rule" }, "annotations": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The annotations of the Prometheus rule" }, "level": { "type": "string", "enum": ["warning", "critical", "emergency"], "title": "The alert level" }, "creator": { "type": "string", "title": "The creator of the rule" } }, "title": "AlertRule resource", "required": ["id", "name", "expr", "annotations", "level"] }, "v2ScaleInClusterResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "ScaleInClusterResponse represents a request to get process list", "required": ["clusterId", "taskId"] }, "v2ScaleOutClusterResponse": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the cluster" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "ScaleOutClusterResponse represents a request to get process list", "required": ["clusterId", "taskId"] }, "v2ServerConfigs": { "type": "object", "properties": { "tidb": { "type": "object", "additionalProperties": { "type": "string" }, "title": "tidb" }, "tikv": { "type": "object", "additionalProperties": { "type": "string" }, "title": "tidb" }, "pd": { "type": "object", "additionalProperties": { "type": "string" }, "title": "tidb" }, "grafana": { "type": "object", "additionalProperties": { "type": "string" }, "title": "grafana" }, "tidbDashboard": { "type": "object", "additionalProperties": { "type": "string" }, "title": "tidb_dashboard" }, "tiflash": { "type": "object", "additionalProperties": { "type": "string" }, "title": "tiflash" }, "tiproxy": { "type": "object", "additionalProperties": { "type": "string" }, "title": "tiproxy" }, "tiflashLearner": { "type": "object", "additionalProperties": { "type": "string" }, "title": "tiflash_learner" }, "pump": { "type": "object", "additionalProperties": { "type": "string" }, "title": "pump" }, "drainer": { "type": "object", "additionalProperties": { "type": "string" }, "title": "drainer" }, "cdc": { "type": "object", "additionalProperties": { "type": "string" }, "title": "cdc" }, "kvcdc": { "type": "object", "additionalProperties": { "type": "string" }, "title": "kvcdc" } }, "title": "ServerConfigs" }, "v2ServiceState": { "type": "string", "enum": [ "SERVICE_STATE_UNSPECIFIED", "RUNNING", "STARTING", "RESTARTING", "UPGRADING", "ABNORMAL", "STOPPED" ], "default": "SERVICE_STATE_UNSPECIFIED", "description": "- SERVICE_STATE_UNSPECIFIED: unspecified\n - RUNNING: running\n - STARTING: starting\n - RESTARTING: starting\n - UPGRADING: upgrading\n - ABNORMAL: abnormal\n - STOPPED: stopped", "title": "CM server state" }, "v2SlowQueryAvailableAdvancedFilterInfo": { "type": "object", "properties": { "name": { "type": "string", "title": "Filter name" }, "unit": { "type": "string", "title": "Filter unit" }, "valueList": { "type": "array", "items": { "type": "string" }, "title": "Filter value list" }, "type": { "type": "string", "title": "Type" } }, "title": "Response message for available filter info" }, "v2SlowQueryAvailableAdvancedFilters": { "type": "object", "properties": { "filters": { "type": "array", "items": { "type": "string" }, "title": "List of available fields" } }, "title": "Response message for available filters" }, "v2SlowQueryAvailableFields": { "type": "object", "properties": { "fields": { "type": "array", "items": { "type": "string" }, "title": "List of available fields" } }, "title": "Response message for available fields" }, "v2SlowQueryDetail": { "type": "object", "properties": { "digest": { "type": "string", "title": "Digest" }, "query": { "type": "string", "title": "Query" }, "instance": { "type": "string", "title": "Instance" }, "db": { "type": "string", "title": "Database" }, "connection_id": { "type": "string", "title": "Connection ID" }, "success": { "type": "integer", "format": "int32", "title": "Success" }, "timestamp": { "type": "number", "format": "double", "title": "Finish time" }, "query_time": { "type": "number", "format": "double", "title": "Latency" }, "parse_time": { "type": "number", "format": "double", "title": "Parse time" }, "compile_time": { "type": "number", "format": "double", "title": "Compile time" }, "rewrite_time": { "type": "number", "format": "double", "title": "Rewrite time" }, "preproc_subqueries_time": { "type": "number", "format": "double", "title": "Preprocessing subqueries time" }, "optimize_time": { "type": "number", "format": "double", "title": "Optimize time" }, "wait_ts": { "type": "number", "format": "double", "title": "Wait timestamp" }, "cop_time": { "type": "number", "format": "double", "title": "Coprocessor time" }, "lock_keys_time": { "type": "number", "format": "double", "title": "Lock keys time" }, "write_sql_response_total": { "type": "number", "format": "double", "title": "Write SQL response time" }, "exec_retry_time": { "type": "number", "format": "double", "title": "Execution retry time" }, "memory_max": { "type": "number", "format": "double", "title": "Max memory" }, "disk_max": { "type": "number", "format": "double", "title": "Max disk" }, "txn_start_ts": { "type": "string", "title": "Transaction start timestamp" }, "prev_stmt": { "type": "string", "title": "Previous statement" }, "plan": { "type": "string", "title": "Execution plan" }, "binary_plan": { "type": "string", "title": "Binary execution plan" }, "warnings": { "type": "string", "title": "Warnings" }, "is_internal": { "type": "integer", "format": "int32", "title": "Is internal" }, "index_names": { "type": "string", "title": "Index names" }, "stats": { "type": "string", "title": "Stats" }, "backoff_types": { "type": "string", "title": "Backoff types" }, "prepared": { "type": "integer", "format": "int32", "title": "Prepared statement" }, "plan_from_cache": { "type": "integer", "format": "int32", "title": "Plan from cache" }, "plan_from_binding": { "type": "integer", "format": "int32", "title": "Plan from binding" }, "user": { "type": "string", "title": "User" }, "host": { "type": "string", "title": "Host" }, "process_time": { "type": "number", "format": "double", "title": "Process time" }, "wait_time": { "type": "number", "format": "double", "title": "Wait time" }, "backoff_time": { "type": "number", "format": "double", "title": "Backoff time" }, "get_commit_ts_time": { "type": "number", "format": "double", "title": "Get commit timestamp time" }, "local_latch_wait_time": { "type": "number", "format": "double", "title": "Local latch wait time" }, "resolve_lock_time": { "type": "number", "format": "double", "title": "Resolve lock time" }, "prewrite_time": { "type": "number", "format": "double", "title": "Prewrite time" }, "wait_prewrite_binlog_time": { "type": "number", "format": "double", "title": "Wait for prewrite binlog time" }, "commit_time": { "type": "number", "format": "double", "title": "Commit time" }, "commit_backoff_time": { "type": "number", "format": "double", "title": "Commit backoff time" }, "cop_proc_avg": { "type": "number", "format": "double", "title": "Coprocessor process average" }, "cop_proc_p90": { "type": "number", "format": "double", "title": "Coprocessor process P90" }, "cop_proc_max": { "type": "number", "format": "double", "title": "Coprocessor process max" }, "cop_wait_avg": { "type": "number", "format": "double", "title": "Coprocessor wait average" }, "cop_wait_p90": { "type": "number", "format": "double", "title": "Coprocessor wait P90" }, "cop_wait_max": { "type": "number", "format": "double", "title": "Coprocessor wait max" }, "write_keys": { "type": "number", "format": "double", "title": "Write keys" }, "write_size": { "type": "number", "format": "double", "title": "Write size" }, "prewrite_region": { "type": "number", "format": "double", "title": "Prewrite region" }, "txn_retry": { "type": "number", "format": "double", "title": "Transaction retry" }, "request_count": { "type": "number", "format": "double", "title": "Request count" }, "process_keys": { "type": "number", "format": "double", "title": "Process keys" }, "total_keys": { "type": "number", "format": "double", "title": "Total keys" }, "cop_proc_addr": { "type": "string", "title": "Coprocessor address" }, "cop_wait_addr": { "type": "string", "title": "Coprocessor wait address" }, "rocksdb_delete_skipped_count": { "type": "number", "format": "double", "title": "RocksDB delete skipped count" }, "rocksdb_key_skipped_count": { "type": "number", "format": "double", "title": "RocksDB key skipped count" }, "rocksdb_block_cache_hit_count": { "type": "number", "format": "double", "title": "RocksDB block cache hit count" }, "rocksdb_block_read_count": { "type": "number", "format": "double", "title": "RocksDB block read count" }, "rocksdb_block_read_byte": { "type": "number", "format": "double", "title": "RocksDB block read byte" }, "binary_plan_text": { "type": "string", "title": "Binary plan in plain text" }, "session_alias": { "type": "string", "title": "Session alias" }, "exec_retry_count": { "type": "number", "format": "double", "title": "Execution retry count" }, "preproc_subqueries": { "type": "number", "format": "double", "title": "Preprocessing subqueries" }, "kv_total": { "type": "number", "format": "double", "title": "KV total" }, "pd_total": { "type": "number", "format": "double", "title": "PD total" }, "backoff_total": { "type": "number", "format": "double", "title": "Backoff total" }, "time_queued_by_rc": { "type": "number", "format": "double", "title": "Time queued by RC" }, "tidb_cpu_time": { "type": "number", "format": "double", "title": "TiDB CPU time" }, "tikv_cpu_time": { "type": "number", "format": "double", "title": "TiKV CPU time" }, "backoff_detail": { "type": "string", "title": "Backoff detail" }, "is_explicit_txn": { "type": "integer", "format": "int32", "title": "Is explicit transaction" }, "plan_digest": { "type": "string", "title": "Plan digest" }, "has_more_results": { "type": "integer", "format": "int32", "title": "Has more results" }, "resource_group": { "type": "string", "title": "Resource group" }, "request_unit_read": { "type": "number", "format": "double", "title": "Request unit read" }, "request_unit_write": { "type": "number", "format": "double", "title": "Request unit write" }, "result_rows": { "type": "number", "format": "double", "title": "Result rows" }, "ru": { "type": "number", "format": "double", "title": "Resource unit" } }, "title": "Model message representing a slow query log" }, "v2SlowQueryDownloadResponse": { "type": "object", "properties": { "filename": { "type": "string", "title": "File name" }, "fileContent": { "type": "string", "title": "File content" } }, "title": "SlowQueryDownloadResponse represents the response for downloading slow query list" }, "v2SlowQueryList": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2SlowQueryDetail" }, "title": "List of slow sql models" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "Response message for querying slow log list" }, "v2Spec": { "type": "object", "properties": { "globalOptions": { "$ref": "#/definitions/v2GlobalOptions", "title": "GlobalOptions" }, "serverConfigs": { "$ref": "#/definitions/v2ServerConfigs", "title": "MonitoredOptions MonitoredOptions `yaml:\"monitored,omitempty\" validate:\"monitored:editable\"`\nComponentVersions ComponentVersions `yaml:\"component_versions,omitempty\" validate:\"component_versions:editable\"`\nComponentSources ComponentSources `yaml:\"component_sources,omitempty\" validate:\"component_sources:editable\"`\nServerConfigs" }, "tidbServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TiDBSpec" }, "title": "tidb_servers" }, "tikvServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TiKVSpec" }, "title": "tikv_servers" }, "tiflashServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TiFlashSpec" }, "title": "tiflash_servers" }, "tiproxyServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TiProxySpec" }, "title": "tiproxy_servers" }, "pdServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2PDSpec" }, "title": "pd_servers" }, "tidbDashboardServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2DashboardSpec" }, "title": "tidb_dashboard_servers" }, "pumpServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2PumpSpec" }, "title": "pump_servers" }, "drainerServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2DrainerSpec" }, "title": "drainer_servers" }, "cdcServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2CDCSpec" }, "title": "cdc_servers" }, "kvcdcServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TiKVCDCSpec" }, "title": "kvcdc_servers" }, "tisparkMasters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TiSparkMasterSpec" }, "title": "tispark_masters" }, "tisparkWorkers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TiSparkWorkerSpec" }, "title": "tispark_workers" }, "monitoringServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2PrometheusSpec" }, "title": "monitoring_servers" }, "grafanaServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2GrafanaSpec" }, "title": "grafana_servers" }, "alertmanagerServers": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2AlertmanagerSpec" }, "title": "alertmanager_servers" } }, "title": "Spec" }, "v2SqlLimit": { "type": "object", "properties": { "id": { "type": "string", "title": "ID" }, "resourceGroupName": { "type": "string", "title": "Resource group name" }, "startTime": { "type": "string", "title": "Start time" }, "endTime": { "type": "string", "title": "End time" }, "watch": { "type": "string", "title": "Watch" }, "watchText": { "type": "string", "title": "Watch text" }, "source": { "type": "string", "title": "Source" }, "action": { "type": "string", "enum": ["DRYRUN", "COOLDOWN", "KILL"], "title": "Action" } }, "title": "SQL limit model" }, "v2SqlLimitList": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2SqlLimit" }, "title": "List of SQL limit" } }, "title": "Response message for getting SQL limit" }, "v2SqlPlanBindingDetail": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "enabled", "using", "disabled", "deleted", "invalid", "rejected", "pending verify" ], "title": "SQL plan binding status" }, "source": { "type": "string", "enum": ["manual", "history", "capture", "evolve"], "title": "SQL plan binding source" }, "digest": { "type": "string", "title": "sql digest" }, "planDigest": { "type": "string", "title": "plan digest" } }, "title": "Detail of sql plan binding" }, "v2SqlPlanBindingList": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2SqlPlanBindingDetail" }, "title": "List of sql plan binding" } }, "title": "Response of get sql plan binding" }, "v2SqlPlanList": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TopSqlDetail" }, "title": "List of sql plan" } }, "title": "Response message for getting sql plan" }, "v2Stack": { "type": "object", "properties": { "error": { "type": "string", "description": "The error message if any." }, "Actives": { "type": "object", "additionalProperties": { "$ref": "#/definitions/v2Abstract" }, "description": "The active tasks information." }, "Actions": { "type": "object", "additionalProperties": { "$ref": "#/definitions/v2Action" }, "description": "The actions information." } }, "description": "Stack represents the execution stack of a task flow." }, "v2State": { "type": "string", "enum": ["STATE_UNSPECIFIED", "ONLINE", "OFFLINE"], "default": "STATE_UNSPECIFIED", "description": "- STATE_UNSPECIFIED: unspecified\n - ONLINE: online\n - OFFLINE: offline", "title": "Server state" }, "v2StatusCount": { "type": "object", "properties": { "status": { "type": "string", "title": "Targets status (e.g., \"healthy\", \"unhealthy\")" }, "count": { "type": "integer", "format": "int32", "title": "The number of targets with this status" } }, "title": "StatusCount represents the status of a target" }, "v2StatusEnumData": { "type": "string", "enum": ["all", "running", "finished", "abnormal", "stopped"], "default": "all", "description": "- all: All\n - running: Running\n - finished: Finished\n - abnormal: Abnormal\n - stopped: Stopped", "title": "Data of StatusEnum" }, "v2StatusStatistics": { "type": "object", "properties": { "alertingCount": { "type": "integer", "format": "int32", "title": "Number of alerting alerts" }, "resolvedCount": { "type": "integer", "format": "int32", "title": "Number of resolved alerts" }, "silencedCount": { "type": "integer", "format": "int32", "title": "Number of silenced alerts" }, "totalCount": { "type": "integer", "format": "int32", "title": "Total number of alerts" } }, "title": "StatusStatistics represents statistics by alert status" }, "v2Step": { "type": "object", "properties": { "Function": { "type": "string", "description": "The stepFunction." }, "ArgsTemplate": { "type": "string", "description": "The arguments template." }, "ReplyTemplate": { "type": "string", "description": "The reply template." }, "NextHops": { "type": "array", "items": { "type": "string" }, "description": "The next steps." }, "Requires": { "type": "array", "items": { "type": "string" }, "description": "The required steps." }, "TimeLimit": { "type": "integer", "format": "int32", "description": "The time limit in seconds." }, "MaxRetry": { "type": "integer", "format": "int32", "description": "The maximum retry count." }, "RetryWaitTime": { "type": "string", "format": "int64", "description": "The retry wait time in seconds." }, "Description": { "type": "string", "description": "The step description." } }, "description": "Step represents a task step.", "required": ["Function"] }, "v2SupportVersions": { "type": "object", "properties": { "versions": { "type": "array", "items": { "type": "string" }, "title": "The versions" } }, "title": "SupportVersions represents support version list", "required": ["versions"] }, "v2TagBindResourceType": { "type": "string", "enum": [ "TAG_BIND_RESOURCE_TYPE_UNSPECIFIED", "HOST", "TIUP", "CLUSTER", "CM_SERVER" ], "default": "TAG_BIND_RESOURCE_TYPE_UNSPECIFIED", "description": "- TAG_BIND_RESOURCE_TYPE_UNSPECIFIED: resource type unspecified\n - HOST: resource type host\n - TIUP: resource type tiup\n - CLUSTER: resource type cluster\n - CM_SERVER: resource type cm server", "title": "define tag bind resource type" }, "v2TagWithBindObject": { "type": "object", "properties": { "tagInfo": { "$ref": "#/definitions/tagv2Tag", "title": "the tag basic info of the tag" }, "bindObjects": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2BindObject" }, "title": "the bound objects of the tag" } }, "title": "Tag resource with bound object" }, "v2Tags": { "type": "object", "properties": { "tagId": { "type": "string", "title": "the tag id of the tag" }, "tagKey": { "type": "string", "title": "the tag key of the tag" }, "tagValue": { "type": "string", "title": "the tag value of the tag" } }, "title": "Tags", "required": ["tagValue"] }, "v2TakeoverClusterRequest": { "type": "object", "properties": { "tiupId": { "type": "string", "title": "tiup_id" }, "takeoverCluster": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/clusterv2TakeoverCluster" }, "title": "The TakeoverCluster" } }, "title": "TakeoverClusterRequest", "required": ["tiupId", "takeoverCluster"] }, "v2TakeoverClusterResp": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "The cluster_id of the host" }, "taskId": { "type": "string", "title": "The task_id of the host" } }, "title": "TakeoverCluster", "required": ["clusterId", "taskId"] }, "v2TakeoverClusterResponse": { "type": "object", "properties": { "takeoverClusterResp": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TakeoverClusterResp" }, "title": "The takeover_cluster_resp of the host" } }, "title": "DeployResponse represents a request to get process list", "required": ["takeoverClusterResp"] }, "v2TaskFlow": { "type": "object", "properties": { "taskId": { "type": "string", "description": "The unique identifier of the task." }, "templateId": { "type": "string", "description": "The template ID associated with the task." }, "creator": { "type": "string", "description": "The creator of the task." }, "parentId": { "type": "string", "enum": [ "tidb:deploy", "tidb:takeover", "tidb:scaleOut", "tidb:scaleIn", "tidb:destroy", "tidb:stop", "tidb:start", "tidb:restart", "tidb:reload", "tidb:install_sql_audit_plugin", "host:import", "host:delete", "host:pre:check" ], "description": "The parent task identifier.", "title": "The parent enum" }, "status": { "type": "string", "enum": [ "success", "abort", "timeout", "failed", "running", "pending" ], "description": "The status of the task.", "title": "The task status" }, "startTime": { "type": "string", "format": "date-time", "description": "The timestamp when the task started.", "readOnly": true }, "endTime": { "type": "string", "format": "date-time", "description": "The timestamp when the task ended.", "readOnly": true } }, "description": "Task represents a task resource containing detailed information.", "required": ["taskId", "templateId"] }, "v2TaskFlowDetail": { "type": "object", "properties": { "taskId": { "type": "string", "description": "The unique identifier of the task flow." }, "parentId": { "type": "string", "description": "The identifier of the parent task flow." }, "templateId": { "type": "string", "description": "The identifier of the template." }, "status": { "type": "string", "enum": ["pending", "running", "finished", "failed", "success"], "description": "The current status of the task flow.", "title": "The task flow status" }, "startTime": { "type": "string", "format": "date-time", "description": "The time when the task flow started.", "readOnly": true }, "endTime": { "type": "string", "format": "date-time", "description": "The time when the task flow ended.", "readOnly": true }, "args": { "type": "string", "description": "The args of task flow." }, "stack": { "type": "string", "description": "The execution stack information." }, "context": { "type": "object", "additionalProperties": { "type": "string" }, "description": "The context information." }, "creator": { "type": "string", "description": "The creator of the task flow." }, "ignoreField": { "$ref": "#/definitions/v2Stack", "description": "The execution stack information." } }, "description": "TaskFlowDetail represents detailed information about a task flow.", "required": ["taskId", "templateId"] }, "v2TaskFlowInfo": { "type": "object", "properties": { "status": { "type": "string", "enum": ["pending", "running", "finished", "failed", "success"], "description": "The status of the task flow.", "title": "The task flow status" }, "startTime": { "type": "string", "format": "date-time", "description": "The start_time when the task flow started.", "readOnly": true }, "taskMsg": { "type": "string", "description": "The task_msg of the task flow." }, "taskInput": { "type": "string", "description": "The task_input of the task flow." } }, "description": "TaskFlowDetail represents detailed information about a task flow." }, "v2TaskFlowInfoResponse": { "type": "object", "properties": { "taskId": { "type": "string", "description": "The task_id of the task flow." }, "nodeKey": { "type": "string", "description": "The parent_id of the task flow." }, "taskFlowInfo": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TaskFlowInfo" }, "description": "The task_flow_info of the task flow." } }, "description": "TaskFlowInfoResponse\nTaskFlowInfoResponse defines the response containing task flow information.", "required": ["taskId", "nodeKey"] }, "v2Template": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "ID", "readOnly": true }, "createTime": { "type": "string", "format": "date-time", "title": "Creation time of the template", "readOnly": true }, "updateTime": { "type": "string", "format": "date-time", "title": "Last update time of the template", "readOnly": true }, "name": { "type": "string", "title": "Template name" }, "tidbVersion": { "type": "string", "title": "TiDB version this template applies to" }, "description": { "type": "string", "title": "Template description" }, "creator": { "type": "string", "title": "Creator of template", "readOnly": true }, "templateRules": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TemplateRule" }, "title": "Alert rules" } }, "title": "AlertTemplate represents an alert rule template", "required": ["name", "tidbVersion"] }, "v2TemplateParameterMapping": { "type": "object", "properties": { "parameterTemplateId": { "type": "string", "format": "int64", "title": "The ID of the parameter template" }, "parameterId": { "type": "string", "format": "int64", "title": "The ID of the parameter" }, "defaultValue": { "type": "string", "title": "The default value of the parameter" }, "parameter": { "$ref": "#/definitions/v2Parameter", "title": "The parameter" }, "note": { "type": "string", "title": "The note of the parameter template mapping" } }, "title": "TemplateParameterMapping represents template to parameter mapping", "required": ["parameterId", "defaultValue"] }, "v2TemplateRule": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "ID of the rule", "readOnly": true }, "name": { "type": "string", "title": "The name of the alert rule" }, "expr": { "type": "string", "title": "The Prometheus expression for the rule" }, "metricId": { "type": "integer", "format": "int32", "title": "The associated metric ID" }, "monitorObjectId": { "type": "integer", "format": "int32", "title": "The associated monitor object ID" }, "duration": { "type": "integer", "format": "int32", "title": "The rule duration in seconds" }, "labels": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The labels of the Prometheus rule" }, "annotations": { "type": "object", "additionalProperties": { "type": "string" }, "title": "The annotations of the Prometheus rule" }, "level": { "type": "string", "enum": ["warning", "critical", "emergency"], "title": "The alert level" } }, "title": "TemplateRule represents a request to create a rule in a template", "required": ["name"] }, "v2TemplateWithOutRules": { "type": "object", "properties": { "id": { "type": "integer", "format": "int32", "title": "ID", "readOnly": true }, "createTime": { "type": "string", "format": "date-time", "title": "Creation time of the template", "readOnly": true }, "updateTime": { "type": "string", "format": "date-time", "title": "Last update time of the template", "readOnly": true }, "name": { "type": "string", "title": "Template name" }, "tidbVersion": { "type": "string", "title": "TiDB version this template applies to" }, "description": { "type": "string", "title": "Template description" }, "creator": { "type": "string", "title": "Creator of template", "readOnly": true } }, "title": "AlertTemplate represents an alert rule template", "required": ["name", "tidbVersion"] }, "v2TiDBCredentialObject": { "type": "object", "properties": { "password": { "type": "string", "title": "the password of the user" }, "clusterId": { "type": "string", "title": "the cluster id of cluster bound with current credential" }, "clusterName": { "type": "string", "title": "the cluster name of cluster bound with current credential" } }, "title": "TiDB cluster credential object", "required": ["password"] }, "v2TiDBProcesses": { "type": "object", "properties": { "uid": { "type": "string", "title": "host Processes\nThe Uid of the Host Processes" }, "pid": { "type": "integer", "format": "int32", "title": "The Pid of the Host Processes" }, "ppid": { "type": "integer", "format": "int32", "title": "The Ppid of the Host Processes" }, "startTime": { "type": "string", "title": "The stime of the Host Processes" }, "runningTime": { "type": "string", "title": "The time of the Host Processes" }, "cmd": { "type": "string", "title": "The cmd of the Host Processes" } }, "title": "TiDBProcesses" }, "v2TiDBSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "statusPort": { "type": "integer", "format": "int32", "title": "status_port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "numaCores": { "type": "string", "title": "numa_cores" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "TiDBSpec" }, "v2TiFlashSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "tcpPort": { "type": "integer", "format": "int32", "title": "tcp_port" }, "httpPort": { "type": "integer", "format": "int32", "title": "http_port" }, "flashServicePort": { "type": "integer", "format": "int32", "title": "flash_service_port" }, "flashProxyPort": { "type": "integer", "format": "int32", "title": "flash_proxy_port" }, "flashProxyStatusPort": { "type": "integer", "format": "int32", "title": "flash_proxy_status_port" }, "metricsPort": { "type": "integer", "format": "int32", "title": "metrics_port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "numaCores": { "type": "string", "title": "numa_cores" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" }, "learnerConfig": { "type": "object", "additionalProperties": { "type": "string" }, "title": "learner_config" } }, "title": "TiFlashSpec" }, "v2TiKVCDCSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "TiKVCDCSpec represents the CDC topology specification in topology.yaml" }, "v2TiKVSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "statusPort": { "type": "integer", "format": "int32", "title": "status_port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "dataDir": { "type": "string", "title": "data_dir" }, "logDir": { "type": "string", "title": "log_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "numaCores": { "type": "string", "title": "numa_cores" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "TiKVSpec" }, "v2TiProxySpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "statusPort": { "type": "integer", "format": "int32", "title": "status_port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "numaNode": { "type": "string", "title": "numa_node" }, "config": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "TiProxySpec" }, "v2TiSparkMasterSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "webPort": { "type": "integer", "format": "int32", "title": "web_port" }, "deployDir": { "type": "string", "title": "deploy_dir" }, "sparkConfig": { "type": "object", "additionalProperties": { "type": "string" }, "title": "config" } }, "title": "TiSparkMasterSpec is the topology specification for TiSpark master node" }, "v2TiSparkWorkerSpec": { "type": "object", "properties": { "host": { "type": "string", "title": "host" }, "port": { "type": "integer", "format": "int32", "title": "port" }, "webPort": { "type": "integer", "format": "int32", "title": "web_port" }, "deployDir": { "type": "string", "title": "deploy_dir" } }, "title": "TiSparkWorkerSpec is the topology specification for TiSpark master node" }, "v2Tiup": { "type": "object", "properties": { "tiupHome": { "type": "string", "title": "tiup_home" }, "version": { "type": "string", "title": "version" } }, "title": "Tiup Resource" }, "v2TiupCredential": { "type": "object", "properties": { "credentialId": { "type": "string", "title": "the credentialId value of the Credential" }, "credentialName": { "type": "string", "title": "the credentialName value of the Credential" }, "credentialType": { "type": "string", "title": "the credentialType value of the Credential" }, "userName": { "type": "string", "title": "the userName value of the Credential" } }, "title": "TiupCredential" }, "v2TiupHost": { "type": "object", "properties": { "hostId": { "type": "string", "title": "The Host_Id of the Host" }, "ip": { "type": "string", "title": "The IP of the Host" }, "hostName": { "type": "string", "title": "The HostName of the Host" }, "sshPort": { "type": "integer", "format": "int32", "title": "The SSHPort of the Host" }, "credentialId": { "type": "string", "title": "The Credential_Id of the Host" }, "osName": { "type": "string", "title": "os_name" }, "osVersion": { "type": "string", "title": "os_version" }, "osRelease": { "type": "string", "title": "os_release" }, "osArchitecture": { "type": "string", "title": "os_architecture" }, "hostType": { "type": "string", "enum": ["VM", "PM"], "title": "host Type (e.g., \"VM\", \"PM\")" }, "locationId": { "type": "string", "title": "location_id" }, "createdTime": { "type": "string", "format": "date-time", "title": "created_time" }, "updatedTime": { "type": "string", "format": "date-time", "title": "updated_time" }, "credential": { "$ref": "#/definitions/v2TiupCredential", "title": "credential" } }, "title": "Host resource", "required": ["hostId"] }, "v2TiupTags": { "type": "object", "properties": { "tagId": { "type": "string", "title": "the tag id of the tag" }, "tagKey": { "type": "string", "title": "the tag key of the tag" }, "tagValue": { "type": "string", "title": "the tag value of the tag" } }, "title": "TiupTags", "required": ["tagValue"] }, "v2Tiups": { "type": "object", "properties": { "tiupId": { "type": "string", "title": "tiup_id" }, "name": { "type": "string", "title": "name" }, "tiupHome": { "type": "string", "title": "tiup_home" }, "version": { "type": "string", "title": "version" }, "credentialId": { "type": "string", "title": "credential_id" }, "tags": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TiupTags" }, "title": "The tags of the Host" }, "description": { "type": "string", "title": "description" }, "hostId": { "type": "string", "title": "host_id" }, "host": { "$ref": "#/definitions/v2TiupHost", "title": "host" } }, "title": "Tiups basic resource" }, "v2TiupsClusters": { "type": "object", "properties": { "clusterId": { "type": "string", "title": "cluster_id" }, "clusterName": { "type": "string", "title": "cluster_id" }, "user": { "type": "string", "title": "user" }, "version": { "type": "string", "title": "version" }, "metaPath": { "type": "string", "title": "patch" }, "privateKeyPath": { "type": "string", "title": "private_key" }, "managed": { "type": "boolean", "title": "managed" } }, "title": "TiupsClusters" }, "v2TiupsClustersResponse": { "type": "object", "properties": { "tiupsClusters": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TiupsClusters" }, "title": "list of tiups" } }, "title": "ListTiups Response" }, "v2TiupsServiceUpdateTiupsBody": { "type": "object", "properties": { "tiups": { "$ref": "#/definitions/tiupv2UpdateTiups", "title": "the tiups resource" } }, "title": "UpdateTiupsRequest Request" }, "v2TopMetricConfig": { "type": "object", "properties": { "cacheFlushIntervalInMinutes": { "type": "integer", "format": "int32", "title": "overview cache flush interval in minute, 0 means no cache" } }, "title": "TopMetricConfig represents the response for querying top metric config" }, "v2TopMetricData": { "type": "object", "properties": { "status": { "type": "string", "title": "Response Status (e.g., \"success\", \"error\")" }, "data": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ExprQueryData" }, "title": "Response Data containing the top queried metrics" } }, "title": "TopMetricData represents the response for querying top metric data" }, "v2TopSqlAvailableAdvancedFilterInfo": { "type": "object", "properties": { "name": { "type": "string", "title": "Filter name" }, "unit": { "type": "string", "title": "Filter unit" }, "valueList": { "type": "array", "items": { "type": "string" }, "title": "Filter value list" }, "type": { "type": "string", "title": "Type" } }, "title": "Response message for available filter info" }, "v2TopSqlAvailableAdvancedFilters": { "type": "object", "properties": { "filters": { "type": "array", "items": { "type": "string" }, "title": "List of available fields" } }, "title": "Response message for available filters" }, "v2TopSqlAvailableFields": { "type": "object", "properties": { "fields": { "type": "array", "items": { "type": "string" }, "title": "List of available fields" } }, "title": "Response message for getting top sql available fields" }, "v2TopSqlConfigs": { "type": "object", "properties": { "enable": { "type": "boolean", "title": "tidb_enable_stmt_summary" }, "refreshInterval": { "type": "integer", "format": "int32", "title": "tidb_stmt_summary_refresh_interval" }, "historySize": { "type": "integer", "format": "int32", "title": "tidb_stmt_summary_history_size" }, "maxSize": { "type": "integer", "format": "int32", "title": "tidb_stmt_summary_max_stmt_count" }, "internalQuery": { "type": "boolean", "title": "tidb_stmt_summary_internal_query" } }, "title": "Response message for getting top sql configs" }, "v2TopSqlDetail": { "type": "object", "properties": { "summary_begin_time": { "type": "number", "format": "double", "title": "AggBeginTime" }, "summary_end_time": { "type": "number", "format": "double", "title": "AggEndTime" }, "digest_text": { "type": "string", "title": "AggDigestText" }, "digest": { "type": "string", "title": "AggDigest" }, "exec_count": { "type": "number", "format": "double", "title": "AggExecCount" }, "stmt_type": { "type": "string", "title": "AggStmtType" }, "sum_errors": { "type": "number", "format": "double", "title": "AggSumErrors" }, "sum_warnings": { "type": "number", "format": "double", "title": "AggSumWarnings" }, "sum_latency": { "type": "number", "format": "double", "title": "AggSumLatency" }, "max_latency": { "type": "number", "format": "double", "title": "AggMaxLatency" }, "min_latency": { "type": "number", "format": "double", "title": "AggMinLatency" }, "avg_latency": { "type": "number", "format": "double", "title": "AggAvgLatency" }, "avg_parse_latency": { "type": "number", "format": "double", "title": "AggAvgParseLatency" }, "max_parse_latency": { "type": "number", "format": "double", "title": "AggMaxParseLatency" }, "avg_compile_latency": { "type": "number", "format": "double", "title": "AggAvgCompileLatency" }, "max_compile_latency": { "type": "number", "format": "double", "title": "AggMaxCompileLatency" }, "sum_cop_task_num": { "type": "number", "format": "double", "title": "AggSumCopTaskNum" }, "avg_cop_process_time": { "type": "number", "format": "double", "title": "AggAvgCopProcessTime" }, "max_cop_process_time": { "type": "number", "format": "double", "title": "AggMaxCopProcessTime" }, "avg_cop_wait_time": { "type": "number", "format": "double", "title": "AggAvgCopWaitTime" }, "max_cop_wait_time": { "type": "number", "format": "double", "title": "AggMaxCopWaitTime" }, "avg_process_time": { "type": "number", "format": "double", "title": "AggAvgProcessTime" }, "max_process_time": { "type": "number", "format": "double", "title": "AggMaxProcessTime" }, "avg_wait_time": { "type": "number", "format": "double", "title": "AggAvgWaitTime" }, "max_wait_time": { "type": "number", "format": "double", "title": "AggMaxWaitTime" }, "avg_backoff_time": { "type": "number", "format": "double", "title": "AggAvgBackoffTime" }, "max_backoff_time": { "type": "number", "format": "double", "title": "AggMaxBackoffTime" }, "avg_total_keys": { "type": "number", "format": "double", "title": "AggAvgTotalKeys" }, "max_total_keys": { "type": "number", "format": "double", "title": "AggMaxTotalKeys" }, "avg_processed_keys": { "type": "number", "format": "double", "title": "AggAvgProcessedKeys" }, "max_processed_keys": { "type": "number", "format": "double", "title": "AggMaxProcessedKeys" }, "avg_prewrite_time": { "type": "number", "format": "double", "title": "AggAvgPrewriteTime" }, "max_prewrite_time": { "type": "number", "format": "double", "title": "AggMaxPrewriteTime" }, "avg_commit_time": { "type": "number", "format": "double", "title": "AggAvgCommitTime" }, "max_commit_time": { "type": "number", "format": "double", "title": "AggMaxCommitTime" }, "avg_get_commit_ts_time": { "type": "number", "format": "double", "title": "AggAvgGetCommitTsTime" }, "max_get_commit_ts_time": { "type": "number", "format": "double", "title": "AggMaxGetCommitTsTime" }, "avg_commit_backoff_time": { "type": "number", "format": "double", "title": "AggAvgCommitBackoffTime" }, "max_commit_backoff_time": { "type": "number", "format": "double", "title": "AggMaxCommitBackoffTime" }, "avg_resolve_lock_time": { "type": "number", "format": "double", "title": "AggAvgResolveLockTime" }, "max_resolve_lock_time": { "type": "number", "format": "double", "title": "AggMaxResolveLockTime" }, "avg_local_latch_wait_time": { "type": "number", "format": "double", "title": "AggAvgLocalLatchWaitTime" }, "max_local_latch_wait_time": { "type": "number", "format": "double", "title": "AggMaxLocalLatchWaitTime" }, "avg_write_keys": { "type": "number", "format": "double", "title": "AggAvgWriteKeys" }, "max_write_keys": { "type": "number", "format": "double", "title": "AggMaxWriteKeys" }, "avg_write_size": { "type": "number", "format": "double", "title": "AggAvgWriteSize" }, "max_write_size": { "type": "number", "format": "double", "title": "AggMaxWriteSize" }, "avg_prewrite_regions": { "type": "number", "format": "double", "title": "AggAvgPrewriteRegions" }, "max_prewrite_regions": { "type": "number", "format": "double", "title": "AggMaxPrewriteRegions" }, "avg_txn_retry": { "type": "number", "format": "double", "title": "AggAvgTxnRetry" }, "max_txn_retry": { "type": "number", "format": "double", "title": "AggMaxTxnRetry" }, "sum_backoff_times": { "type": "number", "format": "double", "title": "AggSumBackoffTimes" }, "avg_mem": { "type": "number", "format": "double", "title": "AggAvgMem" }, "max_mem": { "type": "number", "format": "double", "title": "AggMaxMem" }, "avg_disk": { "type": "number", "format": "double", "title": "AggAvgDisk" }, "max_disk": { "type": "number", "format": "double", "title": "AggMaxDisk" }, "avg_affected_rows": { "type": "number", "format": "double", "title": "AggAvgAffectedRows" }, "first_seen": { "type": "number", "format": "double", "title": "AggFirstSeen" }, "last_seen": { "type": "number", "format": "double", "title": "AggLastSeen" }, "sample_user": { "type": "string", "title": "AggSampleUser" }, "query_sample_text": { "type": "string", "title": "AggQuerySampleText" }, "prev_sample_text": { "type": "string", "title": "AggPrevSampleText" }, "schema_name": { "type": "string", "title": "AggSchemaName" }, "table_names": { "type": "string", "title": "AggTableNames" }, "index_names": { "type": "string", "title": "AggIndexNames" }, "plan_count": { "type": "number", "format": "double", "title": "AggPlanCount" }, "plan": { "type": "string", "title": "AggPlan (deprecated)" }, "binary_plan": { "type": "string", "title": "AggBinaryPlan" }, "plan_digest": { "type": "string", "title": "AggPlanDigest" }, "plan_hint": { "type": "string", "title": "AggPlanHint" }, "max_rocksdb_delete_skipped_count": { "type": "number", "format": "double", "title": "AggMaxRocksdbDeleteSkippedCount" }, "avg_rocksdb_delete_skipped_count": { "type": "number", "format": "double", "title": "AggAvgRocksdbDeleteSkippedCount" }, "max_rocksdb_key_skipped_count": { "type": "number", "format": "double", "title": "AggMaxRocksdbKeySkippedCount" }, "avg_rocksdb_key_skipped_count": { "type": "number", "format": "double", "title": "AggAvgRocksdbKeySkippedCount" }, "max_rocksdb_block_cache_hit_count": { "type": "number", "format": "double", "title": "AggMaxRocksdbBlockCacheHitCount" }, "avg_rocksdb_block_cache_hit_count": { "type": "number", "format": "double", "title": "AggAvgRocksdbBlockCacheHitCount" }, "max_rocksdb_block_read_count": { "type": "number", "format": "double", "title": "AggMaxRocksdbBlockReadCount" }, "avg_rocksdb_block_read_count": { "type": "number", "format": "double", "title": "AggAvgRocksdbBlockReadCount" }, "max_rocksdb_block_read_byte": { "type": "number", "format": "double", "title": "AggMaxRocksdbBlockReadByte" }, "avg_rocksdb_block_read_byte": { "type": "number", "format": "double", "title": "AggAvgRocksdbBlockReadByte" }, "related_schemas": { "type": "string", "title": "RelatedSchemas" }, "plan_can_be_bound": { "type": "boolean", "title": "PlanCanBeBound" }, "binary_plan_text": { "type": "string", "title": "BinaryPlanText" }, "resource_group": { "type": "string", "title": "ResourceGroup" }, "avg_ru": { "type": "number", "format": "double", "title": "AvgRu" }, "max_ru": { "type": "number", "format": "double", "title": "MaxRu" }, "sum_ru": { "type": "number", "format": "double", "title": "SumRu" }, "avg_time_queued_by_rc": { "type": "number", "format": "double", "title": "AvgTimeQueuedByRc" }, "max_time_queued_by_rc": { "type": "number", "format": "double", "title": "MaxTimeQueuedBNyRc" }, "avg_tidb_cpu_time": { "type": "number", "format": "double", "title": "TiDB CPU time" }, "avg_tikv_cpu_time": { "type": "number", "format": "double", "title": "TiKV CPU time" } }, "title": "Top SQL detail" }, "v2TopSqlList": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TopSqlDetail" }, "title": "List of top sql" }, "nextPageToken": { "type": "string", "title": "Next page token" }, "totalSize": { "type": "integer", "format": "int32", "title": "Total size" } }, "title": "Top SQL list" }, "v2TopologySummaryResponse": { "type": "object", "properties": { "clusterNodeTopology": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2ClusterNodeTopology" }, "title": "The cluster_topology list" }, "clusterId": { "type": "string", "title": "The cluster_id" } }, "title": "TopologySummaryResponse", "required": ["clusterNodeTopology", "clusterId"] }, "v2TriggerTypeEnumData": { "type": "string", "enum": ["automatic", "manual"], "default": "automatic", "description": "- automatic: automatic\n - manual: manual", "title": "Data of TriggerTypeEnum" }, "v2TypeEnumData": { "type": "string", "enum": [ "all", "full_backup", "log_backup", "restore_by_file", "restore_by_time", "all_backup", "all_restore" ], "default": "all", "description": "- all: All\n - full_backup: Full backup\n - log_backup: Log backup\n - restore_by_file: Restore by file\n - restore_by_time: Restore by time\n - all_backup: All backup\n - all_restore: All restore", "title": "Data of TypeEnum" }, "v2UpdateAuditConfigsRequest": { "type": "object", "properties": { "enabled": { "type": "boolean", "title": "Whether auditing is enabled" }, "retentionDays": { "type": "integer", "format": "int32", "title": "Log retention period in days" } }, "title": "Update audit configuration request" }, "v2UpdateParameterTemplateRequest": { "type": "object", "properties": { "parameterTemplate": { "$ref": "#/definitions/v2ParameterTemplate", "title": "The parameter template" }, "templateParameterMappings": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2TemplateParameterMapping" }, "title": "The mappings of the parameter template" }, "templateId": { "type": "string", "format": "int64", "title": "The ID of the parameter template" } }, "title": "UpdateParameterTemplateRequest represents an update parameter template request", "required": [ "parameterTemplate", "templateParameterMappings", "templateId" ] }, "v2User": { "type": "object", "properties": { "userId": { "type": "string", "description": "The unique user ID of the user." }, "name": { "type": "string", "description": "The full name of the user." }, "email": { "type": "string", "description": "The email address of the user." }, "note": { "type": "string", "description": "Additional notes about the user." }, "password": { "type": "string", "description": "The user's password (optional)." }, "userType": { "type": "integer", "format": "int32", "description": "The type of the user (e.g., admin, regular user)." }, "userTypeDesc": { "type": "string", "description": "A description of the user's type." }, "phone": { "type": "string", "description": "The user's phone number." }, "roles": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v2UserRole", "enum": [ "ADMIN", "ALERT_MANAGER", "ALERT_READER", "BACKUP_MANAGER", "BACKUP_READER", "CLUSTER_MANAGER", "CLUSTER_READER", "HOST_MANAGER", "HOST_READER", "USER_MANAGER", "AUDIT_MANAGER", "SYSTEM_MANAGER", "SYSTEM_READER" ] }, "description": "The roles assigned to the user.", "title": "The role name" }, "createTime": { "type": "string", "format": "date-time", "description": "The timestamp when the user was created.", "readOnly": true }, "updateTime": { "type": "string", "format": "date-time", "description": "The timestamp when the user was last updated.", "readOnly": true } }, "description": "User represents a user resource containing detailed information about a user.", "required": ["userId", "name"] }, "v2UserProfile": { "type": "object", "properties": { "userId": { "type": "string", "description": "The unique identifier of the user." }, "name": { "type": "string", "description": "The name of the user." }, "email": { "type": "string", "description": "The email address of the user." }, "note": { "type": "string", "description": "The note of the user." }, "phone": { "type": "string", "description": "The phone of the user." } }, "description": "UserProfile represents the profile information of the authenticated user.", "required": ["userId", "name", "email"] }, "v2UserRole": { "type": "object", "properties": { "roleName": { "type": "string", "title": "The name of the role" }, "roleId": { "type": "integer", "format": "int32", "title": "The id of the role" } }, "title": "the role of user", "required": ["roleId"] }, "v2ValidateConnectionRequest": { "type": "object", "properties": { "credentialId": { "type": "string", "title": "the credential id of the credential" } }, "title": "ValidateConnection Request", "required": ["credentialId"] }, "v2ValidateConnectionResponse": { "type": "object", "properties": { "connectionResult": { "type": "string", "title": "the connection result of the validate" }, "inaccessibleHosts": { "type": "array", "items": { "type": "string" }, "title": "the hosts which not accessible" } }, "title": "ValidateConnection Response" }, "v2ValidateSessionResponse": { "type": "object", "properties": { "userId": { "type": "string", "title": "The id of the user" } }, "title": "The Response of ValidateSession", "required": ["userId"] }, "v2Variable": { "type": "object", "properties": { "name": { "type": "string", "title": "Name is the name of the parameter" }, "scope": { "type": "string", "title": "Scope is the scope of the parameter" }, "currentValue": { "type": "string", "title": "CurrentValue is the current value of the parameter" }, "defaultValue": { "type": "string", "title": "DefaultValue is the default value of the parameter" }, "type": { "$ref": "#/definitions/v2ParamTypeEnumData", "title": "Type is the type of the parameter" }, "range": { "type": "string", "title": "Range is the range of the parameter" } }, "title": "Variable is the variable of the parameter", "required": [ "name", "scope", "currentValue", "defaultValue", "type", "range" ] }, "v2WebhookConfig": { "type": "object", "properties": { "url": { "type": "string", "title": "Webhook URL" } }, "title": "WebhookConfig represents webhook channel configuration", "required": ["url"] } } } ================================================ FILE: ui-v2/eslint.config.js ================================================ import js from "@eslint/js" import importPlugin from "eslint-plugin-import" import reactHooks from "eslint-plugin-react-hooks" import reactRefresh from "eslint-plugin-react-refresh" import globals from "globals" import tseslint from "typescript-eslint" export default tseslint.config( { ignores: ["**/dist/**", "packages/api/server/src/**"] }, { files: ["**/*.{ts,tsx}", "eslint.config.js"], extends: [js.configs.recommended, ...tseslint.configs.recommended], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { import: importPlugin, "react-hooks": reactHooks, "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, ], // `import/order` can't sort the multiple imported members from a lib, but `sort-imports` can // so we use them both "sort-imports": [ "error", { ignoreCase: false, ignoreDeclarationSort: true, ignoreMemberSort: false, allowSeparatedGroups: true, }, ], // prevents you from having duplicate import statements from the same module "import/no-duplicates": "error", "import/order": [ "error", { groups: [ "builtin", "external", "internal", "parent", "sibling", "index", "object", ], pathGroups: [ { pattern: "*.{css,svg}", group: "index", position: "after", }, ], pathGroupsExcludedImportTypes: ["builtin"], "newlines-between": "always", alphabetize: { order: "asc", caseInsensitive: true, }, }, ], "no-restricted-imports": [ "error", { paths: [ { name: "lodash", message: "Please use lodash-es instead.", }, ], }, ], "@typescript-eslint/no-unused-vars": [ "error", { args: "all", argsIgnorePattern: "^_", caughtErrors: "all", caughtErrorsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_", varsIgnorePattern: "^_", ignoreRestSiblings: true, }, ], }, }, ) ================================================ FILE: ui-v2/orval.config.ts ================================================ import { defineConfig } from "orval" export default defineConfig({ azores: { input: "./api-specs/azores.json", output: { target: "packages/api/client/src/azores/index.ts", schemas: "packages/api/client/src/azores/models", client: "react-query", mode: "tags-split", clean: true, prettier: true, override: { mutator: { path: "packages/api/client/src/http/client.ts", name: "httpClient", }, }, }, }, azoresHono: { input: "./api-specs/azores.json", output: { mode: "split", client: "hono", target: "packages/api/server/src/azores/index.ts", override: { hono: { handlers: "packages/api/server/src/azores/handlers", }, }, }, }, }) ================================================ FILE: ui-v2/package.json ================================================ { "name": "tidb-dashboard-ui-v2", "private": true, "version": "0.0.0", "type": "module", "scripts": { "fmt-check": "prettier --check .", "fmt-fix": "prettier --write .", "lint": "eslint .", "prepare": "cd .. && husky ui-v2/.husky", "dev": "pnpm -r --parallel dev", "build": "pnpm -r build", "dev:portals:test": "pnpm -r --parallel --filter test-tidb-dashboard-ui-lib... dev", "gen:locales": "tsx scripts/gen-locales.ts", "gen:api": "orval" }, "devDependencies": { "@changesets/cli": "^2.27.9", "@eslint/js": "^9.11.1", "concurrently": "^9.0.1", "eslint": "^9.11.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.12", "glob": "^11.0.0", "globals": "^15.9.0", "gogocode": "^1.0.55", "orval": "^7.3.0", "husky": "^9.1.6", "lint-staged": "^15.2.10", "prettier": "^3.3.3", "tsx": "^4.19.2", "typescript": "^5.5.3", "typescript-eslint": "^8.7.0" }, "lint-staged": { "*.+(ts|tsx|js)": [ "eslint --fix", "prettier --write" ], "*.+(json|css|md|html)": "prettier --write" }, "engines": { "node": ">=22.0.0" }, "packageManager": "pnpm@9.12.2" } ================================================ FILE: ui-v2/packages/api/client/.gitignore ================================================ src/azores ================================================ FILE: ui-v2/packages/api/client/CHANGELOG.md ================================================ # @pingcap-incubator/tidb-dashboard-lib-api-client ## 0.13.0 ### Minor Changes - bump version ## 0.12.0 ### Minor Changes - refactor: re-exports lib-uitls/lib-charts/lib-primitive-ui/lib-biz-ui from lib-apps ## 0.11.0 ### Minor Changes - fix time-range-picker, refine cols-multi-select ## 0.10.0 ### Minor Changes - i18n for slow query and statement app ## 0.9.0 ### Minor Changes - upgrade uikit ## 0.8.0 ### Minor Changes - support advanced filters for diagnosis ## 0.7.0 ### Minor Changes - refine pagination and sort url state ## 0.6.0 ### Minor Changes - refine ## 0.5.0 ### Minor Changes - refine slow-query and statement apps ================================================ FILE: ui-v2/packages/api/client/package.json ================================================ { "name": "@pingcap-incubator/tidb-dashboard-lib-api-client", "private": true, "version": "0.13.0", "description": "", "type": "module", "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist", "README.md", "CHANGELOG.md" ], "scripts": { "tsc:watch": "tsc --watch", "rollup:watch": "rollup -c --watch", "dev": "concurrently --kill-others \"pnpm tsc:watch\" \"pnpm rollup:watch\"", "build": "tsc && rollup -c" }, "keywords": [], "author": "", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^12.1.1", "@tanstack/react-query": "^5.59.16", "axios": "^1.7.9", "rollup": "^4.24.0", "tslib": "^2.8.0" }, "peerDependencies": { "@tanstack/react-query": "^5.59.16", "axios": "^1.7.9" } } ================================================ FILE: ui-v2/packages/api/client/rollup.config.js ================================================ import typescript from "@rollup/plugin-typescript" export default { input: "src/index.ts", output: { dir: "dist", format: "es", }, plugins: [typescript()], } ================================================ FILE: ui-v2/packages/api/client/src/http/client.ts ================================================ import axios, { AxiosRequestConfig } from "axios" declare module "axios" { interface AxiosRequestConfig { skipGlobalErrorHandling?: boolean } } const DEFAULT_TIMEOUT = 30 * 1000 export const axiosClient = axios.create({ baseURL: "", timeout: DEFAULT_TIMEOUT, }) /** * Add a second `options` argument to * pass extra options to each generated query */ export const httpClient = ( config: AxiosRequestConfig, options?: AxiosRequestConfig, ): Promise => { const promise = axiosClient({ ...config, ...options, }).then(({ data }) => data) return promise } ================================================ FILE: ui-v2/packages/api/client/src/index.ts ================================================ export * from "./http/client" export * from "./azores/models" export * from "./azores/metrics-service/metrics-service" export * from "./azores/diagnosis-service/diagnosis-service" ================================================ FILE: ui-v2/packages/api/client/tsconfig.json ================================================ { "extends": "../../tsconfig.app.json", "compilerOptions": { "outDir": "./dist" }, "include": ["./src"] } ================================================ FILE: ui-v2/packages/api/server/package.json ================================================ { "name": "tidb-dashboard-lib-api-server", "private": true, "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy --minify" }, "dependencies": { "@hono/zod-validator": "^0.4.2", "hono": "^4.6.14", "zod": "^3.24.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.20241112.0", "wrangler": "^3.88.0" } } ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/apiKeyServiceCreateApiKey.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ApiKeyServiceCreateApiKeyContext } from '../index.context'; import { apiKeyServiceCreateApiKeyBody, apiKeyServiceCreateApiKeyResponse } from '../index.zod'; const factory = createFactory(); export const apiKeyServiceCreateApiKeyHandlers = factory.createHandlers( zValidator('json', apiKeyServiceCreateApiKeyBody), zValidator('response', apiKeyServiceCreateApiKeyResponse), async (c: ApiKeyServiceCreateApiKeyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/apiKeyServiceDeleteApiKey.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ApiKeyServiceDeleteApiKeyContext } from '../index.context'; import { apiKeyServiceDeleteApiKeyParams, apiKeyServiceDeleteApiKeyResponse } from '../index.zod'; const factory = createFactory(); export const apiKeyServiceDeleteApiKeyHandlers = factory.createHandlers( zValidator('param', apiKeyServiceDeleteApiKeyParams), zValidator('response', apiKeyServiceDeleteApiKeyResponse), async (c: ApiKeyServiceDeleteApiKeyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/apiKeyServiceGetApiKey.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ApiKeyServiceGetApiKeyContext } from '../index.context'; import { apiKeyServiceGetApiKeyParams, apiKeyServiceGetApiKeyResponse } from '../index.zod'; const factory = createFactory(); export const apiKeyServiceGetApiKeyHandlers = factory.createHandlers( zValidator('param', apiKeyServiceGetApiKeyParams), zValidator('response', apiKeyServiceGetApiKeyResponse), async (c: ApiKeyServiceGetApiKeyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/apiKeyServiceGetTemErrorDetail.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ApiKeyServiceGetTemErrorDetailContext } from '../index.context'; import { apiKeyServiceGetTemErrorDetailResponse } from '../index.zod'; const factory = createFactory(); export const apiKeyServiceGetTemErrorDetailHandlers = factory.createHandlers( zValidator('response', apiKeyServiceGetTemErrorDetailResponse), async (c: ApiKeyServiceGetTemErrorDetailContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/apiKeyServiceListApiKeys.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ApiKeyServiceListApiKeysContext } from '../index.context'; import { apiKeyServiceListApiKeysQueryParams, apiKeyServiceListApiKeysResponse } from '../index.zod'; const factory = createFactory(); export const apiKeyServiceListApiKeysHandlers = factory.createHandlers( zValidator('query', apiKeyServiceListApiKeysQueryParams), zValidator('response', apiKeyServiceListApiKeysResponse), async (c: ApiKeyServiceListApiKeysContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/apiKeyServiceResetSecretKey.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ApiKeyServiceResetSecretKeyContext } from '../index.context'; import { apiKeyServiceResetSecretKeyParams, apiKeyServiceResetSecretKeyResponse } from '../index.zod'; const factory = createFactory(); export const apiKeyServiceResetSecretKeyHandlers = factory.createHandlers( zValidator('param', apiKeyServiceResetSecretKeyParams), zValidator('response', apiKeyServiceResetSecretKeyResponse), async (c: ApiKeyServiceResetSecretKeyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/apiKeyServiceUpdateApiKey.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ApiKeyServiceUpdateApiKeyContext } from '../index.context'; import { apiKeyServiceUpdateApiKeyParams, apiKeyServiceUpdateApiKeyBody, apiKeyServiceUpdateApiKeyResponse } from '../index.zod'; const factory = createFactory(); export const apiKeyServiceUpdateApiKeyHandlers = factory.createHandlers( zValidator('param', apiKeyServiceUpdateApiKeyParams), zValidator('json', apiKeyServiceUpdateApiKeyBody), zValidator('response', apiKeyServiceUpdateApiKeyResponse), async (c: ApiKeyServiceUpdateApiKeyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/clusterBRServiceCreateBackupTask.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ClusterBRServiceCreateBackupTaskContext } from '../index.context'; import { clusterBRServiceCreateBackupTaskParams, clusterBRServiceCreateBackupTaskBody, clusterBRServiceCreateBackupTaskResponse } from '../index.zod'; const factory = createFactory(); export const clusterBRServiceCreateBackupTaskHandlers = factory.createHandlers( zValidator('param', clusterBRServiceCreateBackupTaskParams), zValidator('json', clusterBRServiceCreateBackupTaskBody), zValidator('response', clusterBRServiceCreateBackupTaskResponse), async (c: ClusterBRServiceCreateBackupTaskContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/clusterBRServiceCreateRestoreTask.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ClusterBRServiceCreateRestoreTaskContext } from '../index.context'; import { clusterBRServiceCreateRestoreTaskParams, clusterBRServiceCreateRestoreTaskBody, clusterBRServiceCreateRestoreTaskResponse } from '../index.zod'; const factory = createFactory(); export const clusterBRServiceCreateRestoreTaskHandlers = factory.createHandlers( zValidator('param', clusterBRServiceCreateRestoreTaskParams), zValidator('json', clusterBRServiceCreateRestoreTaskBody), zValidator('response', clusterBRServiceCreateRestoreTaskResponse), async (c: ClusterBRServiceCreateRestoreTaskContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/clusterBRServiceDetectCluster.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ClusterBRServiceDetectClusterContext } from '../index.context'; import { clusterBRServiceDetectClusterParams, clusterBRServiceDetectClusterResponse } from '../index.zod'; const factory = createFactory(); export const clusterBRServiceDetectClusterHandlers = factory.createHandlers( zValidator('param', clusterBRServiceDetectClusterParams), zValidator('response', clusterBRServiceDetectClusterResponse), async (c: ClusterBRServiceDetectClusterContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/clusterBRServiceGetClusterBackupPolicy.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ClusterBRServiceGetClusterBackupPolicyContext } from '../index.context'; import { clusterBRServiceGetClusterBackupPolicyParams, clusterBRServiceGetClusterBackupPolicyResponse } from '../index.zod'; const factory = createFactory(); export const clusterBRServiceGetClusterBackupPolicyHandlers = factory.createHandlers( zValidator('param', clusterBRServiceGetClusterBackupPolicyParams), zValidator('response', clusterBRServiceGetClusterBackupPolicyResponse), async (c: ClusterBRServiceGetClusterBackupPolicyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/clusterBRServiceListClusterBRTasks.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ClusterBRServiceListClusterBRTasksContext } from '../index.context'; import { clusterBRServiceListClusterBRTasksParams, clusterBRServiceListClusterBRTasksQueryParams, clusterBRServiceListClusterBRTasksResponse } from '../index.zod'; const factory = createFactory(); export const clusterBRServiceListClusterBRTasksHandlers = factory.createHandlers( zValidator('param', clusterBRServiceListClusterBRTasksParams), zValidator('query', clusterBRServiceListClusterBRTasksQueryParams), zValidator('response', clusterBRServiceListClusterBRTasksResponse), async (c: ClusterBRServiceListClusterBRTasksContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/clusterBRServiceListClusterBackupRecords.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ClusterBRServiceListClusterBackupRecordsContext } from '../index.context'; import { clusterBRServiceListClusterBackupRecordsParams, clusterBRServiceListClusterBackupRecordsQueryParams, clusterBRServiceListClusterBackupRecordsResponse } from '../index.zod'; const factory = createFactory(); export const clusterBRServiceListClusterBackupRecordsHandlers = factory.createHandlers( zValidator('param', clusterBRServiceListClusterBackupRecordsParams), zValidator('query', clusterBRServiceListClusterBackupRecordsQueryParams), zValidator('response', clusterBRServiceListClusterBackupRecordsResponse), async (c: ClusterBRServiceListClusterBackupRecordsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/clusterServiceDeleteProcess.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ClusterServiceDeleteProcessContext } from '../index.context'; import { clusterServiceDeleteProcessParams, clusterServiceDeleteProcessResponse } from '../index.zod'; const factory = createFactory(); export const clusterServiceDeleteProcessHandlers = factory.createHandlers( zValidator('param', clusterServiceDeleteProcessParams), zValidator('response', clusterServiceDeleteProcessResponse), async (c: ClusterServiceDeleteProcessContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/clusterServiceGetProcessList.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { ClusterServiceGetProcessListContext } from '../index.context'; import { clusterServiceGetProcessListParams, clusterServiceGetProcessListResponse } from '../index.zod'; const factory = createFactory(); export const clusterServiceGetProcessListHandlers = factory.createHandlers( zValidator('param', clusterServiceGetProcessListParams), zValidator('response', clusterServiceGetProcessListResponse), async (c: ClusterServiceGetProcessListContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/credentialServiceCreateCredential.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { CredentialServiceCreateCredentialContext } from '../index.context'; import { credentialServiceCreateCredentialBody, credentialServiceCreateCredentialResponse } from '../index.zod'; const factory = createFactory(); export const credentialServiceCreateCredentialHandlers = factory.createHandlers( zValidator('json', credentialServiceCreateCredentialBody), zValidator('response', credentialServiceCreateCredentialResponse), async (c: CredentialServiceCreateCredentialContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/credentialServiceDeleteCredential.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { CredentialServiceDeleteCredentialContext } from '../index.context'; import { credentialServiceDeleteCredentialParams, credentialServiceDeleteCredentialResponse } from '../index.zod'; const factory = createFactory(); export const credentialServiceDeleteCredentialHandlers = factory.createHandlers( zValidator('param', credentialServiceDeleteCredentialParams), zValidator('response', credentialServiceDeleteCredentialResponse), async (c: CredentialServiceDeleteCredentialContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/credentialServiceDownloadRSAKey.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { CredentialServiceDownloadRSAKeyContext } from '../index.context'; import { credentialServiceDownloadRSAKeyBody, credentialServiceDownloadRSAKeyResponse } from '../index.zod'; const factory = createFactory(); export const credentialServiceDownloadRSAKeyHandlers = factory.createHandlers( zValidator('json', credentialServiceDownloadRSAKeyBody), zValidator('response', credentialServiceDownloadRSAKeyResponse), async (c: CredentialServiceDownloadRSAKeyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/credentialServiceGenerateRSAKey.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { CredentialServiceGenerateRSAKeyContext } from '../index.context'; import { credentialServiceGenerateRSAKeyBody, credentialServiceGenerateRSAKeyResponse } from '../index.zod'; const factory = createFactory(); export const credentialServiceGenerateRSAKeyHandlers = factory.createHandlers( zValidator('json', credentialServiceGenerateRSAKeyBody), zValidator('response', credentialServiceGenerateRSAKeyResponse), async (c: CredentialServiceGenerateRSAKeyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/credentialServiceGetCredential.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { CredentialServiceGetCredentialContext } from '../index.context'; import { credentialServiceGetCredentialParams, credentialServiceGetCredentialResponse } from '../index.zod'; const factory = createFactory(); export const credentialServiceGetCredentialHandlers = factory.createHandlers( zValidator('param', credentialServiceGetCredentialParams), zValidator('response', credentialServiceGetCredentialResponse), async (c: CredentialServiceGetCredentialContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/credentialServiceListCredentials.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { CredentialServiceListCredentialsContext } from '../index.context'; import { credentialServiceListCredentialsQueryParams, credentialServiceListCredentialsResponse } from '../index.zod'; const factory = createFactory(); export const credentialServiceListCredentialsHandlers = factory.createHandlers( zValidator('query', credentialServiceListCredentialsQueryParams), zValidator('response', credentialServiceListCredentialsResponse), async (c: CredentialServiceListCredentialsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/credentialServiceUpdateCredential.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { CredentialServiceUpdateCredentialContext } from '../index.context'; import { credentialServiceUpdateCredentialParams, credentialServiceUpdateCredentialBody, credentialServiceUpdateCredentialResponse } from '../index.zod'; const factory = createFactory(); export const credentialServiceUpdateCredentialHandlers = factory.createHandlers( zValidator('param', credentialServiceUpdateCredentialParams), zValidator('json', credentialServiceUpdateCredentialBody), zValidator('response', credentialServiceUpdateCredentialResponse), async (c: CredentialServiceUpdateCredentialContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/credentialServiceValidateConnection.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { CredentialServiceValidateConnectionContext } from '../index.context'; import { credentialServiceValidateConnectionBody, credentialServiceValidateConnectionResponse } from '../index.zod'; const factory = createFactory(); export const credentialServiceValidateConnectionHandlers = factory.createHandlers( zValidator('json', credentialServiceValidateConnectionBody), zValidator('response', credentialServiceValidateConnectionResponse), async (c: CredentialServiceValidateConnectionContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceAddSqlLimit.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceAddSqlLimitContext } from '../index.context'; import { diagnosisServiceAddSqlLimitParams, diagnosisServiceAddSqlLimitBody, diagnosisServiceAddSqlLimitResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceAddSqlLimitHandlers = factory.createHandlers( zValidator('param', diagnosisServiceAddSqlLimitParams), zValidator('json', diagnosisServiceAddSqlLimitBody), zValidator('response', diagnosisServiceAddSqlLimitResponse), async (c: DiagnosisServiceAddSqlLimitContext) => { return c.json({}) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceBindSqlPlan.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceBindSqlPlanContext } from '../index.context'; import { diagnosisServiceBindSqlPlanParams, diagnosisServiceBindSqlPlanResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceBindSqlPlanHandlers = factory.createHandlers( zValidator('param', diagnosisServiceBindSqlPlanParams), zValidator('response', diagnosisServiceBindSqlPlanResponse), async (c: DiagnosisServiceBindSqlPlanContext) => { return c.json({}) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceCheckSqlLimitSupport.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceCheckSqlLimitSupportContext } from '../index.context'; import { diagnosisServiceCheckSqlLimitSupportParams, diagnosisServiceCheckSqlLimitSupportResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceCheckSqlLimitSupportHandlers = factory.createHandlers( zValidator('param', diagnosisServiceCheckSqlLimitSupportParams), zValidator('response', diagnosisServiceCheckSqlLimitSupportResponse), async (c: DiagnosisServiceCheckSqlLimitSupportContext) => { return c.json({isSupport: true}) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceCheckSqlPlanSupport.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceCheckSqlPlanSupportContext } from '../index.context'; import { diagnosisServiceCheckSqlPlanSupportParams, diagnosisServiceCheckSqlPlanSupportResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceCheckSqlPlanSupportHandlers = factory.createHandlers( zValidator('param', diagnosisServiceCheckSqlPlanSupportParams), zValidator('response', diagnosisServiceCheckSqlPlanSupportResponse), async (c: DiagnosisServiceCheckSqlPlanSupportContext) => { return c.json({isSupport: true}) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceDownloadSlowQueryList.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceDownloadSlowQueryListContext } from '../index.context'; import { diagnosisServiceDownloadSlowQueryListParams, diagnosisServiceDownloadSlowQueryListQueryParams, diagnosisServiceDownloadSlowQueryListResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceDownloadSlowQueryListHandlers = factory.createHandlers( zValidator('param', diagnosisServiceDownloadSlowQueryListParams), zValidator('query', diagnosisServiceDownloadSlowQueryListQueryParams), zValidator('response', diagnosisServiceDownloadSlowQueryListResponse), async (c: DiagnosisServiceDownloadSlowQueryListContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetResourceGroupList.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetResourceGroupListContext } from '../index.context'; import { diagnosisServiceGetResourceGroupListParams, diagnosisServiceGetResourceGroupListResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceGetResourceGroupListHandlers = factory.createHandlers( zValidator('param', diagnosisServiceGetResourceGroupListParams), zValidator('response', diagnosisServiceGetResourceGroupListResponse), async (c: DiagnosisServiceGetResourceGroupListContext) => { return c.json({ resourceGroups: [ {name: "default"}, {name: "ru1"}, {name: "ru2"}, ] }) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfo.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoContext } from '../index.context'; import { diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoParams, diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoHandlers = factory.createHandlers( zValidator('param', diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoParams), zValidator('response', diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoResponse), async (c: DiagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetSlowQueryAvailableAdvancedFilters.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetSlowQueryAvailableAdvancedFiltersContext } from '../index.context'; import { diagnosisServiceGetSlowQueryAvailableAdvancedFiltersParams, diagnosisServiceGetSlowQueryAvailableAdvancedFiltersResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceGetSlowQueryAvailableAdvancedFiltersHandlers = factory.createHandlers( zValidator('param', diagnosisServiceGetSlowQueryAvailableAdvancedFiltersParams), zValidator('response', diagnosisServiceGetSlowQueryAvailableAdvancedFiltersResponse), async (c: DiagnosisServiceGetSlowQueryAvailableAdvancedFiltersContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetSlowQueryAvailableFields.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetSlowQueryAvailableFieldsContext } from '../index.context'; import { diagnosisServiceGetSlowQueryAvailableFieldsParams, diagnosisServiceGetSlowQueryAvailableFieldsResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceGetSlowQueryAvailableFieldsHandlers = factory.createHandlers( zValidator('param', diagnosisServiceGetSlowQueryAvailableFieldsParams), zValidator('response', diagnosisServiceGetSlowQueryAvailableFieldsResponse), async (c: DiagnosisServiceGetSlowQueryAvailableFieldsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetSlowQueryDetail.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetSlowQueryDetailContext } from '../index.context'; import { diagnosisServiceGetSlowQueryDetailParams, diagnosisServiceGetSlowQueryDetailQueryParams, diagnosisServiceGetSlowQueryDetailResponse } from '../index.zod'; import slowQueryDetailData from '../sample-res/slow-query-detail.json' const factory = createFactory(); export const diagnosisServiceGetSlowQueryDetailHandlers = factory.createHandlers( // zValidator('param', diagnosisServiceGetSlowQueryDetailParams), // zValidator('query', diagnosisServiceGetSlowQueryDetailQueryParams), // zValidator('response', diagnosisServiceGetSlowQueryDetailResponse), async (c: DiagnosisServiceGetSlowQueryDetailContext) => { return c.json(slowQueryDetailData) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetSlowQueryList.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetSlowQueryListContext } from '../index.context'; import { diagnosisServiceGetSlowQueryListParams, diagnosisServiceGetSlowQueryListQueryParams, diagnosisServiceGetSlowQueryListResponse } from '../index.zod'; import slowQueryListData from '../sample-res/slow-query-list.json' const factory = createFactory(); export const diagnosisServiceGetSlowQueryListHandlers = factory.createHandlers( // zValidator('param', diagnosisServiceGetSlowQueryListParams), // zValidator('query', diagnosisServiceGetSlowQueryListQueryParams), zValidator('response', diagnosisServiceGetSlowQueryListResponse), async (c: DiagnosisServiceGetSlowQueryListContext) => { return c.json(slowQueryListData) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetSqlLimitList.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetSqlLimitListContext } from '../index.context'; import { diagnosisServiceGetSqlLimitListParams, diagnosisServiceGetSqlLimitListQueryParams, diagnosisServiceGetSqlLimitListResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceGetSqlLimitListHandlers = factory.createHandlers( zValidator('param', diagnosisServiceGetSqlLimitListParams), zValidator('query', diagnosisServiceGetSqlLimitListQueryParams), zValidator('response', diagnosisServiceGetSqlLimitListResponse), async (c: DiagnosisServiceGetSqlLimitListContext) => { return c.json({ data: [ { resourceGroupName: "default", action: "DRYRUN", }, ] }) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetSqlPlanBindingList.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetSqlPlanBindingListContext } from '../index.context'; import { diagnosisServiceGetSqlPlanBindingListParams, diagnosisServiceGetSqlPlanBindingListQueryParams, diagnosisServiceGetSqlPlanBindingListResponse } from '../index.zod'; import plansListData from '../sample-res/statement-plans-list.json' const factory = createFactory(); export const diagnosisServiceGetSqlPlanBindingListHandlers = factory.createHandlers( zValidator('param', diagnosisServiceGetSqlPlanBindingListParams), zValidator('query', diagnosisServiceGetSqlPlanBindingListQueryParams), zValidator('response', diagnosisServiceGetSqlPlanBindingListResponse), async (c: DiagnosisServiceGetSqlPlanBindingListContext) => { const plansDigest = plansListData.data.map((plan) => { return { planDigest: plan.plan_digest } }) return c.json({ data: plansDigest }) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetSqlPlanList.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetSqlPlanListContext } from '../index.context'; import { diagnosisServiceGetSqlPlanListParams, diagnosisServiceGetSqlPlanListQueryParams, diagnosisServiceGetSqlPlanListResponse } from '../index.zod'; import statementPlansListData from '../sample-res/statement-plans-list.json' const factory = createFactory(); export const diagnosisServiceGetSqlPlanListHandlers = factory.createHandlers( // zValidator('param', diagnosisServiceGetSqlPlanListParams), // zValidator('query', diagnosisServiceGetSqlPlanListQueryParams), zValidator('response', diagnosisServiceGetSqlPlanListResponse), async (c: DiagnosisServiceGetSqlPlanListContext) => { return c.json(statementPlansListData) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetTopSqlAvailableAdvancedFilterInfo.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetTopSqlAvailableAdvancedFilterInfoContext } from '../index.context'; import { diagnosisServiceGetTopSqlAvailableAdvancedFilterInfoParams, diagnosisServiceGetTopSqlAvailableAdvancedFilterInfoResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceGetTopSqlAvailableAdvancedFilterInfoHandlers = factory.createHandlers( zValidator('param', diagnosisServiceGetTopSqlAvailableAdvancedFilterInfoParams), zValidator('response', diagnosisServiceGetTopSqlAvailableAdvancedFilterInfoResponse), async (c: DiagnosisServiceGetTopSqlAvailableAdvancedFilterInfoContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetTopSqlAvailableAdvancedFilters.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetTopSqlAvailableAdvancedFiltersContext } from '../index.context'; import { diagnosisServiceGetTopSqlAvailableAdvancedFiltersParams, diagnosisServiceGetTopSqlAvailableAdvancedFiltersResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceGetTopSqlAvailableAdvancedFiltersHandlers = factory.createHandlers( zValidator('param', diagnosisServiceGetTopSqlAvailableAdvancedFiltersParams), zValidator('response', diagnosisServiceGetTopSqlAvailableAdvancedFiltersResponse), async (c: DiagnosisServiceGetTopSqlAvailableAdvancedFiltersContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetTopSqlAvailableFields.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetTopSqlAvailableFieldsContext } from '../index.context'; import { diagnosisServiceGetTopSqlAvailableFieldsParams, diagnosisServiceGetTopSqlAvailableFieldsResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceGetTopSqlAvailableFieldsHandlers = factory.createHandlers( zValidator('param', diagnosisServiceGetTopSqlAvailableFieldsParams), zValidator('response', diagnosisServiceGetTopSqlAvailableFieldsResponse), async (c: DiagnosisServiceGetTopSqlAvailableFieldsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetTopSqlConfigs.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetTopSqlConfigsContext } from '../index.context'; import { diagnosisServiceGetTopSqlConfigsParams, diagnosisServiceGetTopSqlConfigsResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceGetTopSqlConfigsHandlers = factory.createHandlers( zValidator('param', diagnosisServiceGetTopSqlConfigsParams), zValidator('response', diagnosisServiceGetTopSqlConfigsResponse), async (c: DiagnosisServiceGetTopSqlConfigsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetTopSqlDetail.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetTopSqlDetailContext } from '../index.context'; import { diagnosisServiceGetTopSqlDetailParams, diagnosisServiceGetTopSqlDetailQueryParams, diagnosisServiceGetTopSqlDetailResponse } from '../index.zod'; import statementDetailData from '../sample-res/statement-plans-detail.json' const factory = createFactory(); export const diagnosisServiceGetTopSqlDetailHandlers = factory.createHandlers( // zValidator('param', diagnosisServiceGetTopSqlDetailParams), // zValidator('query', diagnosisServiceGetTopSqlDetailQueryParams), zValidator('response', diagnosisServiceGetTopSqlDetailResponse), async (c: DiagnosisServiceGetTopSqlDetailContext) => { return c.json(statementDetailData) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceGetTopSqlList.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceGetTopSqlListContext } from '../index.context'; import { diagnosisServiceGetTopSqlListParams, diagnosisServiceGetTopSqlListQueryParams, diagnosisServiceGetTopSqlListResponse } from '../index.zod'; import statementListData from '../sample-res/statement-list.json' const factory = createFactory(); export const diagnosisServiceGetTopSqlListHandlers = factory.createHandlers( // zValidator('param', diagnosisServiceGetTopSqlListParams), // zValidator('query', diagnosisServiceGetTopSqlListQueryParams), zValidator('response', diagnosisServiceGetTopSqlListResponse), async (c: DiagnosisServiceGetTopSqlListContext) => { return c.json(statementListData) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceRemoveSqlLimit.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceRemoveSqlLimitContext } from '../index.context'; import { diagnosisServiceRemoveSqlLimitParams, diagnosisServiceRemoveSqlLimitBody, diagnosisServiceRemoveSqlLimitResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceRemoveSqlLimitHandlers = factory.createHandlers( zValidator('param', diagnosisServiceRemoveSqlLimitParams), zValidator('json', diagnosisServiceRemoveSqlLimitBody), zValidator('response', diagnosisServiceRemoveSqlLimitResponse), async (c: DiagnosisServiceRemoveSqlLimitContext) => { return c.json({}) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceUnbindSqlPlan.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceUnbindSqlPlanContext } from '../index.context'; import { diagnosisServiceUnbindSqlPlanParams, diagnosisServiceUnbindSqlPlanQueryParams, diagnosisServiceUnbindSqlPlanResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceUnbindSqlPlanHandlers = factory.createHandlers( zValidator('param', diagnosisServiceUnbindSqlPlanParams), zValidator('query', diagnosisServiceUnbindSqlPlanQueryParams), zValidator('response', diagnosisServiceUnbindSqlPlanResponse), async (c: DiagnosisServiceUnbindSqlPlanContext) => { return c.json({}) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/diagnosisServiceUpdateTopSqlConfigs.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { DiagnosisServiceUpdateTopSqlConfigsContext } from '../index.context'; import { diagnosisServiceUpdateTopSqlConfigsParams, diagnosisServiceUpdateTopSqlConfigsBody, diagnosisServiceUpdateTopSqlConfigsResponse } from '../index.zod'; const factory = createFactory(); export const diagnosisServiceUpdateTopSqlConfigsHandlers = factory.createHandlers( zValidator('param', diagnosisServiceUpdateTopSqlConfigsParams), zValidator('json', diagnosisServiceUpdateTopSqlConfigsBody), zValidator('response', diagnosisServiceUpdateTopSqlConfigsResponse), async (c: DiagnosisServiceUpdateTopSqlConfigsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServiceCreateBackupPolicy.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServiceCreateBackupPolicyContext } from '../index.context'; import { globalBRServiceCreateBackupPolicyBody, globalBRServiceCreateBackupPolicyResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServiceCreateBackupPolicyHandlers = factory.createHandlers( zValidator('json', globalBRServiceCreateBackupPolicyBody), zValidator('response', globalBRServiceCreateBackupPolicyResponse), async (c: GlobalBRServiceCreateBackupPolicyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServiceDeleteBRTask.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServiceDeleteBRTaskContext } from '../index.context'; import { globalBRServiceDeleteBRTaskParams, globalBRServiceDeleteBRTaskQueryParams, globalBRServiceDeleteBRTaskResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServiceDeleteBRTaskHandlers = factory.createHandlers( zValidator('param', globalBRServiceDeleteBRTaskParams), zValidator('query', globalBRServiceDeleteBRTaskQueryParams), zValidator('response', globalBRServiceDeleteBRTaskResponse), async (c: GlobalBRServiceDeleteBRTaskContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServiceDeleteBackupPolicy.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServiceDeleteBackupPolicyContext } from '../index.context'; import { globalBRServiceDeleteBackupPolicyParams, globalBRServiceDeleteBackupPolicyResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServiceDeleteBackupPolicyHandlers = factory.createHandlers( zValidator('param', globalBRServiceDeleteBackupPolicyParams), zValidator('response', globalBRServiceDeleteBackupPolicyResponse), async (c: GlobalBRServiceDeleteBackupPolicyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServiceGetBRSummary.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServiceGetBRSummaryContext } from '../index.context'; import { globalBRServiceGetBRSummaryQueryParams, globalBRServiceGetBRSummaryResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServiceGetBRSummaryHandlers = factory.createHandlers( zValidator('query', globalBRServiceGetBRSummaryQueryParams), zValidator('response', globalBRServiceGetBRSummaryResponse), async (c: GlobalBRServiceGetBRSummaryContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServiceGetBackupPolicy.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServiceGetBackupPolicyContext } from '../index.context'; import { globalBRServiceGetBackupPolicyParams, globalBRServiceGetBackupPolicyResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServiceGetBackupPolicyHandlers = factory.createHandlers( zValidator('param', globalBRServiceGetBackupPolicyParams), zValidator('response', globalBRServiceGetBackupPolicyResponse), async (c: GlobalBRServiceGetBackupPolicyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServiceListBRTasks.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServiceListBRTasksContext } from '../index.context'; import { globalBRServiceListBRTasksQueryParams, globalBRServiceListBRTasksResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServiceListBRTasksHandlers = factory.createHandlers( zValidator('query', globalBRServiceListBRTasksQueryParams), zValidator('response', globalBRServiceListBRTasksResponse), async (c: GlobalBRServiceListBRTasksContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServiceListBackupPolicies.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServiceListBackupPoliciesContext } from '../index.context'; import { globalBRServiceListBackupPoliciesQueryParams, globalBRServiceListBackupPoliciesResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServiceListBackupPoliciesHandlers = factory.createHandlers( zValidator('query', globalBRServiceListBackupPoliciesQueryParams), zValidator('response', globalBRServiceListBackupPoliciesResponse), async (c: GlobalBRServiceListBackupPoliciesContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServicePreCheckBackupPolicy.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServicePreCheckBackupPolicyContext } from '../index.context'; import { globalBRServicePreCheckBackupPolicyBody, globalBRServicePreCheckBackupPolicyResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServicePreCheckBackupPolicyHandlers = factory.createHandlers( zValidator('json', globalBRServicePreCheckBackupPolicyBody), zValidator('response', globalBRServicePreCheckBackupPolicyResponse), async (c: GlobalBRServicePreCheckBackupPolicyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServiceStartBRTask.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServiceStartBRTaskContext } from '../index.context'; import { globalBRServiceStartBRTaskParams, globalBRServiceStartBRTaskResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServiceStartBRTaskHandlers = factory.createHandlers( zValidator('param', globalBRServiceStartBRTaskParams), zValidator('response', globalBRServiceStartBRTaskResponse), async (c: GlobalBRServiceStartBRTaskContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServiceStopBRTask.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServiceStopBRTaskContext } from '../index.context'; import { globalBRServiceStopBRTaskParams, globalBRServiceStopBRTaskResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServiceStopBRTaskHandlers = factory.createHandlers( zValidator('param', globalBRServiceStopBRTaskParams), zValidator('response', globalBRServiceStopBRTaskResponse), async (c: GlobalBRServiceStopBRTaskContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/globalBRServiceUpdateBackupPolicy.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { GlobalBRServiceUpdateBackupPolicyContext } from '../index.context'; import { globalBRServiceUpdateBackupPolicyParams, globalBRServiceUpdateBackupPolicyBody, globalBRServiceUpdateBackupPolicyResponse } from '../index.zod'; const factory = createFactory(); export const globalBRServiceUpdateBackupPolicyHandlers = factory.createHandlers( zValidator('param', globalBRServiceUpdateBackupPolicyParams), zValidator('json', globalBRServiceUpdateBackupPolicyBody), zValidator('response', globalBRServiceUpdateBackupPolicyResponse), async (c: GlobalBRServiceUpdateBackupPolicyContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceBatchDelete.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceBatchDeleteContext } from '../index.context'; import { hostServiceBatchDeleteBody, hostServiceBatchDeleteResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceBatchDeleteHandlers = factory.createHandlers( zValidator('json', hostServiceBatchDeleteBody), zValidator('response', hostServiceBatchDeleteResponse), async (c: HostServiceBatchDeleteContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceCheck.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceCheckContext } from '../index.context'; import { hostServiceCheckParams, hostServiceCheckResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceCheckHandlers = factory.createHandlers( zValidator('param', hostServiceCheckParams), zValidator('response', hostServiceCheckResponse), async (c: HostServiceCheckContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceCreateHosts.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceCreateHostsContext } from '../index.context'; import { hostServiceCreateHostsBody, hostServiceCreateHostsResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceCreateHostsHandlers = factory.createHandlers( zValidator('json', hostServiceCreateHostsBody), zValidator('response', hostServiceCreateHostsResponse), async (c: HostServiceCreateHostsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceDelete.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceDeleteContext } from '../index.context'; import { hostServiceDeleteParams, hostServiceDeleteResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceDeleteHandlers = factory.createHandlers( zValidator('param', hostServiceDeleteParams), zValidator('response', hostServiceDeleteResponse), async (c: HostServiceDeleteContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceDownloadHostTemplate.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceDownloadHostTemplateContext } from '../index.context'; import { hostServiceDownloadHostTemplateResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceDownloadHostTemplateHandlers = factory.createHandlers( zValidator('response', hostServiceDownloadHostTemplateResponse), async (c: HostServiceDownloadHostTemplateContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceDownloadListHosts.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceDownloadListHostsContext } from '../index.context'; import { hostServiceDownloadListHostsQueryParams, hostServiceDownloadListHostsResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceDownloadListHostsHandlers = factory.createHandlers( zValidator('query', hostServiceDownloadListHostsQueryParams), zValidator('response', hostServiceDownloadListHostsResponse), async (c: HostServiceDownloadListHostsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceFix.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceFixContext } from '../index.context'; import { hostServiceFixParams, hostServiceFixResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceFixHandlers = factory.createHandlers( zValidator('param', hostServiceFixParams), zValidator('response', hostServiceFixResponse), async (c: HostServiceFixContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceGetDisks.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceGetDisksContext } from '../index.context'; import { hostServiceGetDisksParams, hostServiceGetDisksResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceGetDisksHandlers = factory.createHandlers( zValidator('param', hostServiceGetDisksParams), zValidator('response', hostServiceGetDisksResponse), async (c: HostServiceGetDisksContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceGetHost.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceGetHostContext } from '../index.context'; import { hostServiceGetHostParams, hostServiceGetHostResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceGetHostHandlers = factory.createHandlers( zValidator('param', hostServiceGetHostParams), zValidator('response', hostServiceGetHostResponse), async (c: HostServiceGetHostContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceGetTiDBProcesses.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceGetTiDBProcessesContext } from '../index.context'; import { hostServiceGetTiDBProcessesParams, hostServiceGetTiDBProcessesResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceGetTiDBProcessesHandlers = factory.createHandlers( zValidator('param', hostServiceGetTiDBProcessesParams), zValidator('response', hostServiceGetTiDBProcessesResponse), async (c: HostServiceGetTiDBProcessesContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceHostConfirm.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceHostConfirmContext } from '../index.context'; import { hostServiceHostConfirmParams, hostServiceHostConfirmBody, hostServiceHostConfirmResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceHostConfirmHandlers = factory.createHandlers( zValidator('param', hostServiceHostConfirmParams), zValidator('json', hostServiceHostConfirmBody), zValidator('response', hostServiceHostConfirmResponse), async (c: HostServiceHostConfirmContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceImport.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceImportContext } from '../index.context'; import { hostServiceImportBody, hostServiceImportResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceImportHandlers = factory.createHandlers( zValidator('json', hostServiceImportBody), zValidator('response', hostServiceImportResponse), async (c: HostServiceImportContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceImportTask.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceImportTaskContext } from '../index.context'; import { hostServiceImportTaskParams, hostServiceImportTaskResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceImportTaskHandlers = factory.createHandlers( zValidator('param', hostServiceImportTaskParams), zValidator('response', hostServiceImportTaskResponse), async (c: HostServiceImportTaskContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceListHosts.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceListHostsContext } from '../index.context'; import { hostServiceListHostsQueryParams, hostServiceListHostsResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceListHostsHandlers = factory.createHandlers( zValidator('query', hostServiceListHostsQueryParams), zValidator('response', hostServiceListHostsResponse), async (c: HostServiceListHostsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceReport.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceReportContext } from '../index.context'; import { hostServiceReportParams, hostServiceReportResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceReportHandlers = factory.createHandlers( zValidator('param', hostServiceReportParams), zValidator('response', hostServiceReportResponse), async (c: HostServiceReportContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/hostServiceUpdateHost.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { HostServiceUpdateHostContext } from '../index.context'; import { hostServiceUpdateHostParams, hostServiceUpdateHostBody, hostServiceUpdateHostResponse } from '../index.zod'; const factory = createFactory(); export const hostServiceUpdateHostHandlers = factory.createHandlers( zValidator('param', hostServiceUpdateHostParams), zValidator('json', hostServiceUpdateHostBody), zValidator('response', hostServiceUpdateHostResponse), async (c: HostServiceUpdateHostContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceBatchCreateLabels.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceBatchCreateLabelsContext } from '../index.context'; import { labelServiceBatchCreateLabelsBody, labelServiceBatchCreateLabelsResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceBatchCreateLabelsHandlers = factory.createHandlers( zValidator('json', labelServiceBatchCreateLabelsBody), zValidator('response', labelServiceBatchCreateLabelsResponse), async (c: LabelServiceBatchCreateLabelsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceBindLabel.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceBindLabelContext } from '../index.context'; import { labelServiceBindLabelBody, labelServiceBindLabelResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceBindLabelHandlers = factory.createHandlers( zValidator('json', labelServiceBindLabelBody), zValidator('response', labelServiceBindLabelResponse), async (c: LabelServiceBindLabelContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceBindResource.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceBindResourceContext } from '../index.context'; import { labelServiceBindResourceBody, labelServiceBindResourceResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceBindResourceHandlers = factory.createHandlers( zValidator('json', labelServiceBindResourceBody), zValidator('response', labelServiceBindResourceResponse), async (c: LabelServiceBindResourceContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceCreateLabel.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceCreateLabelContext } from '../index.context'; import { labelServiceCreateLabelBody, labelServiceCreateLabelResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceCreateLabelHandlers = factory.createHandlers( zValidator('json', labelServiceCreateLabelBody), zValidator('response', labelServiceCreateLabelResponse), async (c: LabelServiceCreateLabelContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceDeleteLabel.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceDeleteLabelContext } from '../index.context'; import { labelServiceDeleteLabelParams, labelServiceDeleteLabelResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceDeleteLabelHandlers = factory.createHandlers( zValidator('param', labelServiceDeleteLabelParams), zValidator('response', labelServiceDeleteLabelResponse), async (c: LabelServiceDeleteLabelContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceGetLabel.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceGetLabelContext } from '../index.context'; import { labelServiceGetLabelParams, labelServiceGetLabelResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceGetLabelHandlers = factory.createHandlers( zValidator('param', labelServiceGetLabelParams), zValidator('response', labelServiceGetLabelResponse), async (c: LabelServiceGetLabelContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceGetLabelWithBindings.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceGetLabelWithBindingsContext } from '../index.context'; import { labelServiceGetLabelWithBindingsParams, labelServiceGetLabelWithBindingsResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceGetLabelWithBindingsHandlers = factory.createHandlers( zValidator('param', labelServiceGetLabelWithBindingsParams), zValidator('response', labelServiceGetLabelWithBindingsResponse), async (c: LabelServiceGetLabelWithBindingsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceListLabelKeys.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceListLabelKeysContext } from '../index.context'; import { labelServiceListLabelKeysQueryParams, labelServiceListLabelKeysResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceListLabelKeysHandlers = factory.createHandlers( zValidator('query', labelServiceListLabelKeysQueryParams), zValidator('response', labelServiceListLabelKeysResponse), async (c: LabelServiceListLabelKeysContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceListLabels.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceListLabelsContext } from '../index.context'; import { labelServiceListLabelsQueryParams, labelServiceListLabelsResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceListLabelsHandlers = factory.createHandlers( zValidator('query', labelServiceListLabelsQueryParams), zValidator('response', labelServiceListLabelsResponse), async (c: LabelServiceListLabelsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceListLabelsByResourceType.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceListLabelsByResourceTypeContext } from '../index.context'; import { labelServiceListLabelsByResourceTypeQueryParams, labelServiceListLabelsByResourceTypeResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceListLabelsByResourceTypeHandlers = factory.createHandlers( zValidator('query', labelServiceListLabelsByResourceTypeQueryParams), zValidator('response', labelServiceListLabelsByResourceTypeResponse), async (c: LabelServiceListLabelsByResourceTypeContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceListLabelsWithBindings.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceListLabelsWithBindingsContext } from '../index.context'; import { labelServiceListLabelsWithBindingsQueryParams, labelServiceListLabelsWithBindingsResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceListLabelsWithBindingsHandlers = factory.createHandlers( zValidator('query', labelServiceListLabelsWithBindingsQueryParams), zValidator('response', labelServiceListLabelsWithBindingsResponse), async (c: LabelServiceListLabelsWithBindingsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/labelServiceUpdateLabel.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LabelServiceUpdateLabelContext } from '../index.context'; import { labelServiceUpdateLabelParams, labelServiceUpdateLabelBody, labelServiceUpdateLabelResponse } from '../index.zod'; const factory = createFactory(); export const labelServiceUpdateLabelHandlers = factory.createHandlers( zValidator('param', labelServiceUpdateLabelParams), zValidator('json', labelServiceUpdateLabelBody), zValidator('response', labelServiceUpdateLabelResponse), async (c: LabelServiceUpdateLabelContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/licenseServiceActivateFreeLicense.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LicenseServiceActivateFreeLicenseContext } from '../index.context'; import { licenseServiceActivateFreeLicenseResponse } from '../index.zod'; const factory = createFactory(); export const licenseServiceActivateFreeLicenseHandlers = factory.createHandlers( zValidator('response', licenseServiceActivateFreeLicenseResponse), async (c: LicenseServiceActivateFreeLicenseContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/licenseServiceActivateLicense.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LicenseServiceActivateLicenseContext } from '../index.context'; import { licenseServiceActivateLicenseBody, licenseServiceActivateLicenseResponse } from '../index.zod'; const factory = createFactory(); export const licenseServiceActivateLicenseHandlers = factory.createHandlers( zValidator('json', licenseServiceActivateLicenseBody), zValidator('response', licenseServiceActivateLicenseResponse), async (c: LicenseServiceActivateLicenseContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/licenseServiceGetDeviceCode.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LicenseServiceGetDeviceCodeContext } from '../index.context'; import { licenseServiceGetDeviceCodeResponse } from '../index.zod'; const factory = createFactory(); export const licenseServiceGetDeviceCodeHandlers = factory.createHandlers( zValidator('response', licenseServiceGetDeviceCodeResponse), async (c: LicenseServiceGetDeviceCodeContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/licenseServiceGetLicense.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LicenseServiceGetLicenseContext } from '../index.context'; import { licenseServiceGetLicenseResponse } from '../index.zod'; const factory = createFactory(); export const licenseServiceGetLicenseHandlers = factory.createHandlers( zValidator('response', licenseServiceGetLicenseResponse), async (c: LicenseServiceGetLicenseContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/locationServiceCreateLocations.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LocationServiceCreateLocationsContext } from '../index.context'; import { locationServiceCreateLocationsBody, locationServiceCreateLocationsResponse } from '../index.zod'; const factory = createFactory(); export const locationServiceCreateLocationsHandlers = factory.createHandlers( zValidator('json', locationServiceCreateLocationsBody), zValidator('response', locationServiceCreateLocationsResponse), async (c: LocationServiceCreateLocationsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/locationServiceDeleteLocation.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LocationServiceDeleteLocationContext } from '../index.context'; import { locationServiceDeleteLocationParams, locationServiceDeleteLocationResponse } from '../index.zod'; const factory = createFactory(); export const locationServiceDeleteLocationHandlers = factory.createHandlers( zValidator('param', locationServiceDeleteLocationParams), zValidator('response', locationServiceDeleteLocationResponse), async (c: LocationServiceDeleteLocationContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/locationServiceGetLocations.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LocationServiceGetLocationsContext } from '../index.context'; import { locationServiceGetLocationsParams, locationServiceGetLocationsResponse } from '../index.zod'; const factory = createFactory(); export const locationServiceGetLocationsHandlers = factory.createHandlers( zValidator('param', locationServiceGetLocationsParams), zValidator('response', locationServiceGetLocationsResponse), async (c: LocationServiceGetLocationsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/locationServiceListLocations.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LocationServiceListLocationsContext } from '../index.context'; import { locationServiceListLocationsQueryParams, locationServiceListLocationsResponse } from '../index.zod'; const factory = createFactory(); export const locationServiceListLocationsHandlers = factory.createHandlers( zValidator('query', locationServiceListLocationsQueryParams), zValidator('response', locationServiceListLocationsResponse), async (c: LocationServiceListLocationsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/locationServiceUpdateLocations.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { LocationServiceUpdateLocationsContext } from '../index.context'; import { locationServiceUpdateLocationsParams, locationServiceUpdateLocationsBody, locationServiceUpdateLocationsResponse } from '../index.zod'; const factory = createFactory(); export const locationServiceUpdateLocationsHandlers = factory.createHandlers( zValidator('param', locationServiceUpdateLocationsParams), zValidator('json', locationServiceUpdateLocationsBody), zValidator('response', locationServiceUpdateLocationsResponse), async (c: LocationServiceUpdateLocationsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/metricsServiceGetClusterMetricData.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { MetricsServiceGetClusterMetricDataContext } from '../index.context'; import { metricsServiceGetClusterMetricDataParams, metricsServiceGetClusterMetricDataQueryParams, metricsServiceGetClusterMetricDataResponse } from '../index.zod'; import metricsData from "../sample-res/metrics-data-cpu-usage.json" const factory = createFactory() export const metricsServiceGetClusterMetricDataHandlers = factory.createHandlers( zValidator("param", metricsServiceGetClusterMetricDataParams), zValidator("query", metricsServiceGetClusterMetricDataQueryParams), zValidator("response", metricsServiceGetClusterMetricDataResponse), async (c: MetricsServiceGetClusterMetricDataContext) => { return c.json(metricsData) }, ) ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/metricsServiceGetClusterMetricInstance.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { MetricsServiceGetClusterMetricInstanceContext } from '../index.context'; import { metricsServiceGetClusterMetricInstanceParams, metricsServiceGetClusterMetricInstanceResponse } from '../index.zod'; const factory = createFactory(); export const metricsServiceGetClusterMetricInstanceHandlers = factory.createHandlers( zValidator('param', metricsServiceGetClusterMetricInstanceParams), zValidator('response', metricsServiceGetClusterMetricInstanceResponse), async (c: MetricsServiceGetClusterMetricInstanceContext) => { return c.json({ type: "tidb", instanceList: ["10.2.12.107:10081", "10.2.12.107:10082"], }) }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/metricsServiceGetHostMetricData.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { MetricsServiceGetHostMetricDataContext } from '../index.context'; import { metricsServiceGetHostMetricDataParams, metricsServiceGetHostMetricDataQueryParams, metricsServiceGetHostMetricDataResponse } from '../index.zod'; import metricsData from "../sample-res/metrics-data-cpu-usage.json" const factory = createFactory() export const metricsServiceGetHostMetricDataHandlers = factory.createHandlers( zValidator("param", metricsServiceGetHostMetricDataParams), zValidator("query", metricsServiceGetHostMetricDataQueryParams), zValidator("response", metricsServiceGetHostMetricDataResponse), async (c: MetricsServiceGetHostMetricDataContext) => { return c.json(metricsData) }, ) ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/metricsServiceGetMetrics.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { MetricsServiceGetMetricsContext } from '../index.context'; import { metricsServiceGetMetricsQueryParams, metricsServiceGetMetricsResponse } from '../index.zod'; import metricsConfigOverview from "../sample-res/metrics-config-overview.json" import metricsConfigHost from "../sample-res/metrics-config-host.json" import metricsConfigCluster from "../sample-res/metrics-config-cluster.json" import metricsConfigClusterOverview from "../sample-res/metrics-config-cluster-overview.json" const factory = createFactory() export const metricsServiceGetMetricsHandlers = factory.createHandlers( zValidator("query", metricsServiceGetMetricsQueryParams), zValidator("response", metricsServiceGetMetricsResponse), async (c: MetricsServiceGetMetricsContext) => { const classType = c.req.query("class") const group = c.req.query("group") if (classType === "overview") { return c.json(metricsConfigOverview) } else if (classType === "host") { return c.json(metricsConfigHost) } else if (classType === "cluster") { if (group === "overview") { return c.json(metricsConfigClusterOverview) } else { return c.json(metricsConfigCluster) } } }, ) ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/metricsServiceGetOverviewStatus.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { MetricsServiceGetOverviewStatusContext } from '../index.context'; import { metricsServiceGetOverviewStatusQueryParams, metricsServiceGetOverviewStatusResponse } from '../index.zod'; const factory = createFactory(); export const metricsServiceGetOverviewStatusHandlers = factory.createHandlers( zValidator('query', metricsServiceGetOverviewStatusQueryParams), zValidator('response', metricsServiceGetOverviewStatusResponse), async (c: MetricsServiceGetOverviewStatusContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/metricsServiceGetTopMetricConfig.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { MetricsServiceGetTopMetricConfigContext } from '../index.context'; import { metricsServiceGetTopMetricConfigResponse } from '../index.zod'; const factory = createFactory(); export const metricsServiceGetTopMetricConfigHandlers = factory.createHandlers( zValidator('response', metricsServiceGetTopMetricConfigResponse), async (c: MetricsServiceGetTopMetricConfigContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/metricsServiceGetTopMetricData.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { MetricsServiceGetTopMetricDataContext } from '../index.context'; import { metricsServiceGetTopMetricDataParams, metricsServiceGetTopMetricDataQueryParams, metricsServiceGetTopMetricDataResponse } from '../index.zod'; import metricsData from "../sample-res/metrics-data-cpu-usage.json" const factory = createFactory() export const metricsServiceGetTopMetricDataHandlers = factory.createHandlers( zValidator("param", metricsServiceGetTopMetricDataParams), zValidator("query", metricsServiceGetTopMetricDataQueryParams), zValidator("response", metricsServiceGetTopMetricDataResponse), async (c: MetricsServiceGetTopMetricDataContext) => { return c.json(metricsData) }, ) ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/roleServiceCreateRole.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { RoleServiceCreateRoleContext } from '../index.context'; import { roleServiceCreateRoleBody, roleServiceCreateRoleResponse } from '../index.zod'; const factory = createFactory(); export const roleServiceCreateRoleHandlers = factory.createHandlers( zValidator('json', roleServiceCreateRoleBody), zValidator('response', roleServiceCreateRoleResponse), async (c: RoleServiceCreateRoleContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/roleServiceDeleteRole.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { RoleServiceDeleteRoleContext } from '../index.context'; import { roleServiceDeleteRoleParams, roleServiceDeleteRoleResponse } from '../index.zod'; const factory = createFactory(); export const roleServiceDeleteRoleHandlers = factory.createHandlers( zValidator('param', roleServiceDeleteRoleParams), zValidator('response', roleServiceDeleteRoleResponse), async (c: RoleServiceDeleteRoleContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/roleServiceListRoles.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { RoleServiceListRolesContext } from '../index.context'; import { roleServiceListRolesQueryParams, roleServiceListRolesResponse } from '../index.zod'; const factory = createFactory(); export const roleServiceListRolesHandlers = factory.createHandlers( zValidator('query', roleServiceListRolesQueryParams), zValidator('response', roleServiceListRolesResponse), async (c: RoleServiceListRolesContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/roleServiceUpdateRole.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { RoleServiceUpdateRoleContext } from '../index.context'; import { roleServiceUpdateRoleParams, roleServiceUpdateRoleBody, roleServiceUpdateRoleResponse } from '../index.zod'; const factory = createFactory(); export const roleServiceUpdateRoleHandlers = factory.createHandlers( zValidator('param', roleServiceUpdateRoleParams), zValidator('json', roleServiceUpdateRoleBody), zValidator('response', roleServiceUpdateRoleResponse), async (c: RoleServiceUpdateRoleContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceBatchCreateTags.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceBatchCreateTagsContext } from '../index.context'; import { tagServiceBatchCreateTagsBody, tagServiceBatchCreateTagsResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceBatchCreateTagsHandlers = factory.createHandlers( zValidator('json', tagServiceBatchCreateTagsBody), zValidator('response', tagServiceBatchCreateTagsResponse), async (c: TagServiceBatchCreateTagsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceBindResource.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceBindResourceContext } from '../index.context'; import { tagServiceBindResourceBody, tagServiceBindResourceResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceBindResourceHandlers = factory.createHandlers( zValidator('json', tagServiceBindResourceBody), zValidator('response', tagServiceBindResourceResponse), async (c: TagServiceBindResourceContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceBindTag.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceBindTagContext } from '../index.context'; import { tagServiceBindTagBody, tagServiceBindTagResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceBindTagHandlers = factory.createHandlers( zValidator('json', tagServiceBindTagBody), zValidator('response', tagServiceBindTagResponse), async (c: TagServiceBindTagContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceCreateTag.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceCreateTagContext } from '../index.context'; import { tagServiceCreateTagBody, tagServiceCreateTagResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceCreateTagHandlers = factory.createHandlers( zValidator('json', tagServiceCreateTagBody), zValidator('response', tagServiceCreateTagResponse), async (c: TagServiceCreateTagContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceDeleteTag.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceDeleteTagContext } from '../index.context'; import { tagServiceDeleteTagParams, tagServiceDeleteTagResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceDeleteTagHandlers = factory.createHandlers( zValidator('param', tagServiceDeleteTagParams), zValidator('response', tagServiceDeleteTagResponse), async (c: TagServiceDeleteTagContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceGetTag.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceGetTagContext } from '../index.context'; import { tagServiceGetTagParams, tagServiceGetTagResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceGetTagHandlers = factory.createHandlers( zValidator('param', tagServiceGetTagParams), zValidator('response', tagServiceGetTagResponse), async (c: TagServiceGetTagContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceGetTagWithBindings.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceGetTagWithBindingsContext } from '../index.context'; import { tagServiceGetTagWithBindingsParams, tagServiceGetTagWithBindingsResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceGetTagWithBindingsHandlers = factory.createHandlers( zValidator('param', tagServiceGetTagWithBindingsParams), zValidator('response', tagServiceGetTagWithBindingsResponse), async (c: TagServiceGetTagWithBindingsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceListTagKeys.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceListTagKeysContext } from '../index.context'; import { tagServiceListTagKeysQueryParams, tagServiceListTagKeysResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceListTagKeysHandlers = factory.createHandlers( zValidator('query', tagServiceListTagKeysQueryParams), zValidator('response', tagServiceListTagKeysResponse), async (c: TagServiceListTagKeysContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceListTags.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceListTagsContext } from '../index.context'; import { tagServiceListTagsQueryParams, tagServiceListTagsResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceListTagsHandlers = factory.createHandlers( zValidator('query', tagServiceListTagsQueryParams), zValidator('response', tagServiceListTagsResponse), async (c: TagServiceListTagsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceListTagsByResourceType.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceListTagsByResourceTypeContext } from '../index.context'; import { tagServiceListTagsByResourceTypeQueryParams, tagServiceListTagsByResourceTypeResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceListTagsByResourceTypeHandlers = factory.createHandlers( zValidator('query', tagServiceListTagsByResourceTypeQueryParams), zValidator('response', tagServiceListTagsByResourceTypeResponse), async (c: TagServiceListTagsByResourceTypeContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceListTagsWithBindings.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceListTagsWithBindingsContext } from '../index.context'; import { tagServiceListTagsWithBindingsQueryParams, tagServiceListTagsWithBindingsResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceListTagsWithBindingsHandlers = factory.createHandlers( zValidator('query', tagServiceListTagsWithBindingsQueryParams), zValidator('response', tagServiceListTagsWithBindingsResponse), async (c: TagServiceListTagsWithBindingsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tagServiceUpdateTag.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TagServiceUpdateTagContext } from '../index.context'; import { tagServiceUpdateTagParams, tagServiceUpdateTagBody, tagServiceUpdateTagResponse } from '../index.zod'; const factory = createFactory(); export const tagServiceUpdateTagHandlers = factory.createHandlers( zValidator('param', tagServiceUpdateTagParams), zValidator('json', tagServiceUpdateTagBody), zValidator('response', tagServiceUpdateTagResponse), async (c: TagServiceUpdateTagContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tiupsServiceCreateTiups.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TiupsServiceCreateTiupsContext } from '../index.context'; import { tiupsServiceCreateTiupsBody, tiupsServiceCreateTiupsResponse } from '../index.zod'; const factory = createFactory(); export const tiupsServiceCreateTiupsHandlers = factory.createHandlers( zValidator('json', tiupsServiceCreateTiupsBody), zValidator('response', tiupsServiceCreateTiupsResponse), async (c: TiupsServiceCreateTiupsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tiupsServiceDeleteTiups.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TiupsServiceDeleteTiupsContext } from '../index.context'; import { tiupsServiceDeleteTiupsParams, tiupsServiceDeleteTiupsResponse } from '../index.zod'; const factory = createFactory(); export const tiupsServiceDeleteTiupsHandlers = factory.createHandlers( zValidator('param', tiupsServiceDeleteTiupsParams), zValidator('response', tiupsServiceDeleteTiupsResponse), async (c: TiupsServiceDeleteTiupsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tiupsServiceGetTiups.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TiupsServiceGetTiupsContext } from '../index.context'; import { tiupsServiceGetTiupsParams, tiupsServiceGetTiupsResponse } from '../index.zod'; const factory = createFactory(); export const tiupsServiceGetTiupsHandlers = factory.createHandlers( zValidator('param', tiupsServiceGetTiupsParams), zValidator('response', tiupsServiceGetTiupsResponse), async (c: TiupsServiceGetTiupsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tiupsServiceGetTiupsCluster.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TiupsServiceGetTiupsClusterContext } from '../index.context'; import { tiupsServiceGetTiupsClusterParams, tiupsServiceGetTiupsClusterResponse } from '../index.zod'; const factory = createFactory(); export const tiupsServiceGetTiupsClusterHandlers = factory.createHandlers( zValidator('param', tiupsServiceGetTiupsClusterParams), zValidator('response', tiupsServiceGetTiupsClusterResponse), async (c: TiupsServiceGetTiupsClusterContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tiupsServiceListTiups.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TiupsServiceListTiupsContext } from '../index.context'; import { tiupsServiceListTiupsQueryParams, tiupsServiceListTiupsResponse } from '../index.zod'; const factory = createFactory(); export const tiupsServiceListTiupsHandlers = factory.createHandlers( zValidator('query', tiupsServiceListTiupsQueryParams), zValidator('response', tiupsServiceListTiupsResponse), async (c: TiupsServiceListTiupsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/tiupsServiceUpdateTiups.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { TiupsServiceUpdateTiupsContext } from '../index.context'; import { tiupsServiceUpdateTiupsParams, tiupsServiceUpdateTiupsBody, tiupsServiceUpdateTiupsResponse } from '../index.zod'; const factory = createFactory(); export const tiupsServiceUpdateTiupsHandlers = factory.createHandlers( zValidator('param', tiupsServiceUpdateTiupsParams), zValidator('json', tiupsServiceUpdateTiupsBody), zValidator('response', tiupsServiceUpdateTiupsResponse), async (c: TiupsServiceUpdateTiupsContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceChangePassword.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceChangePasswordContext } from '../index.context'; import { userServiceChangePasswordBody, userServiceChangePasswordResponse } from '../index.zod'; const factory = createFactory(); export const userServiceChangePasswordHandlers = factory.createHandlers( zValidator('json', userServiceChangePasswordBody), zValidator('response', userServiceChangePasswordResponse), async (c: UserServiceChangePasswordContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceCreateUser.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceCreateUserContext } from '../index.context'; import { userServiceCreateUserBody, userServiceCreateUserResponse } from '../index.zod'; const factory = createFactory(); export const userServiceCreateUserHandlers = factory.createHandlers( zValidator('json', userServiceCreateUserBody), zValidator('response', userServiceCreateUserResponse), async (c: UserServiceCreateUserContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceDeleteUser.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceDeleteUserContext } from '../index.context'; import { userServiceDeleteUserParams, userServiceDeleteUserResponse } from '../index.zod'; const factory = createFactory(); export const userServiceDeleteUserHandlers = factory.createHandlers( zValidator('param', userServiceDeleteUserParams), zValidator('response', userServiceDeleteUserResponse), async (c: UserServiceDeleteUserContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceGetUser.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceGetUserContext } from '../index.context'; import { userServiceGetUserParams, userServiceGetUserResponse } from '../index.zod'; const factory = createFactory(); export const userServiceGetUserHandlers = factory.createHandlers( zValidator('param', userServiceGetUserParams), zValidator('response', userServiceGetUserResponse), async (c: UserServiceGetUserContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceGetUserProfile.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceGetUserProfileContext } from '../index.context'; import { userServiceGetUserProfileResponse } from '../index.zod'; const factory = createFactory(); export const userServiceGetUserProfileHandlers = factory.createHandlers( zValidator('response', userServiceGetUserProfileResponse), async (c: UserServiceGetUserProfileContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceListUserRoles.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceListUserRolesContext } from '../index.context'; import { userServiceListUserRolesQueryParams, userServiceListUserRolesResponse } from '../index.zod'; const factory = createFactory(); export const userServiceListUserRolesHandlers = factory.createHandlers( zValidator('query', userServiceListUserRolesQueryParams), zValidator('response', userServiceListUserRolesResponse), async (c: UserServiceListUserRolesContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceListUsers.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceListUsersContext } from '../index.context'; import { userServiceListUsersQueryParams, userServiceListUsersResponse } from '../index.zod'; const factory = createFactory(); export const userServiceListUsersHandlers = factory.createHandlers( zValidator('query', userServiceListUsersQueryParams), zValidator('response', userServiceListUsersResponse), async (c: UserServiceListUsersContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceLogin.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceLoginContext } from '../index.context'; import { userServiceLoginBody, userServiceLoginResponse } from '../index.zod'; const factory = createFactory(); export const userServiceLoginHandlers = factory.createHandlers( zValidator('json', userServiceLoginBody), zValidator('response', userServiceLoginResponse), async (c: UserServiceLoginContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceLogout.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceLogoutContext } from '../index.context'; import { userServiceLogoutResponse } from '../index.zod'; const factory = createFactory(); export const userServiceLogoutHandlers = factory.createHandlers( zValidator('response', userServiceLogoutResponse), async (c: UserServiceLogoutContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceResetPassword.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceResetPasswordContext } from '../index.context'; import { userServiceResetPasswordParams, userServiceResetPasswordBody, userServiceResetPasswordResponse } from '../index.zod'; const factory = createFactory(); export const userServiceResetPasswordHandlers = factory.createHandlers( zValidator('param', userServiceResetPasswordParams), zValidator('json', userServiceResetPasswordBody), zValidator('response', userServiceResetPasswordResponse), async (c: UserServiceResetPasswordContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceUpdateUser.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceUpdateUserContext } from '../index.context'; import { userServiceUpdateUserParams, userServiceUpdateUserBody, userServiceUpdateUserResponse } from '../index.zod'; const factory = createFactory(); export const userServiceUpdateUserHandlers = factory.createHandlers( zValidator('param', userServiceUpdateUserParams), zValidator('json', userServiceUpdateUserBody), zValidator('response', userServiceUpdateUserResponse), async (c: UserServiceUpdateUserContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/handlers/userServiceValidateSession.ts ================================================ import { createFactory } from 'hono/factory'; import { zValidator } from '../index.validator'; import { UserServiceValidateSessionContext } from '../index.context'; import { userServiceValidateSessionResponse } from '../index.zod'; const factory = createFactory(); export const userServiceValidateSessionHandlers = factory.createHandlers( zValidator('response', userServiceValidateSessionResponse), async (c: UserServiceValidateSessionContext) => { }, ); ================================================ FILE: ui-v2/packages/api/server/src/azores/index.context.ts ================================================ /** * Generated by orval v7.3.0 🍺 * Do not edit manually. * Azores Open API * OpenAPI spec version: 2.0.0 */ import type { Context, Env } from 'hono'; // https://stackoverflow.com/questions/49579094/typescript-conditional-types-filter-out-readonly-properties-pick-only-requir/49579497#49579497 type IfEquals = (() => T extends X ? 1 : 2) extends < T, >() => T extends Y ? 1 : 2 ? A : B; type WritableKeys = { [P in keyof T]-?: IfEquals< { [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P >; }[keyof T]; type UnionToIntersection = (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never; type DistributeReadOnlyOverUnions = T extends any ? NonReadonly : never; type Writable = Pick>; type NonReadonly = [T] extends [UnionToIntersection] ? { [P in keyof Writable]: T[P] extends object ? NonReadonly> : T[P]; } : DistributeReadOnlyOverUnions; import { ApiKeyServiceListApiKeysParams, V2CreateApiKeyRequest, ApiKeyServiceUpdateApiKeyBody, GlobalBRServiceListBackupPoliciesParams, V2BackupPolicyBody, GlobalBRServiceUpdateBackupPolicyBody, GlobalBRServiceGetBRSummaryParams, GlobalBRServiceListBRTasksParams, GlobalBRServiceDeleteBRTaskParams, ClusterBRServiceCreateBackupTaskBody, ClusterBRServiceListClusterBackupRecordsParams, ClusterBRServiceCreateRestoreTaskBody, ClusterBRServiceListClusterBRTasksParams, MetricsServiceGetClusterMetricDataParams, DiagnosisServiceGetSlowQueryListParams, DiagnosisServiceDownloadSlowQueryListParams, DiagnosisServiceGetSlowQueryDetailParams, DiagnosisServiceAddSqlLimitBody, DiagnosisServiceRemoveSqlLimitBody, DiagnosisServiceGetSqlLimitListParams, DiagnosisServiceGetSqlPlanListParams, DiagnosisServiceGetSqlPlanBindingListParams, DiagnosisServiceUnbindSqlPlanParams, DiagnosisServiceGetTopSqlListParams, DiagnosisServiceUpdateTopSqlConfigsBody, DiagnosisServiceGetTopSqlDetailParams, CredentialServiceListCredentialsParams, V2Credential, CredentialServiceUpdateCredentialBody, V2GenerateRSAKeyRequest, V2ValidateConnectionRequest, HostServiceListHostsParams, V2CreateHost, HostServiceImportBody, HostServiceHostConfirmBody, V2HostServiceUpdateHostBody, MetricsServiceGetHostMetricDataParams, V2BatchDeleteRequest, HostServiceDownloadListHostsParams, LicenseServiceActivateLicenseBody, LocationServiceListLocationsParams, V2Locations, LocationServiceUpdateLocationsBody, V2LoginRequest, MetricsServiceGetMetricsParams, MetricsServiceGetTopMetricDataParams, MetricsServiceGetOverviewStatusParams, RoleServiceListRolesParams, V2Role, RoleServiceUpdateRoleBody, TagServiceListTagsParams, Tagv2Tag, TagServiceUpdateTagBody, V2BatchCreateTagsRequest, V2BindResourceRequest, V2BindTagRequest, TagServiceListTagsByResourceTypeParams, TagServiceListTagKeysParams, TagServiceListTagsWithBindingsParams, TiupsServiceListTiupsParams, Tiupv2CreateTiups, V2TiupsServiceUpdateTiupsBody, UserServiceListUsersParams, V2User, UserServiceUpdateUserBody, UserServiceResetPasswordBody, V2ChangePasswordRequest } from './index.schemas'; export type ApiKeyServiceListApiKeysContext = Context export type ApiKeyServiceCreateApiKeyContext = Context export type ApiKeyServiceGetApiKeyContext = Context export type ApiKeyServiceDeleteApiKeyContext = Context export type ApiKeyServiceUpdateApiKeyContext = Context export type ApiKeyServiceResetSecretKeyContext = Context export type GlobalBRServiceListBackupPoliciesContext = Context export type GlobalBRServiceCreateBackupPolicyContext = Context export type GlobalBRServicePreCheckBackupPolicyContext = Context export type GlobalBRServiceGetBackupPolicyContext = Context export type GlobalBRServiceDeleteBackupPolicyContext = Context export type GlobalBRServiceUpdateBackupPolicyContext = Context export type GlobalBRServiceGetBRSummaryContext = Context export type GlobalBRServiceListBRTasksContext = Context export type GlobalBRServiceDeleteBRTaskContext = Context export type GlobalBRServiceStartBRTaskContext = Context export type GlobalBRServiceStopBRTaskContext = Context export type ClusterBRServiceCreateBackupTaskContext = Context export type ClusterBRServiceGetClusterBackupPolicyContext = Context export type ClusterBRServiceListClusterBackupRecordsContext = Context export type ClusterBRServiceCreateRestoreTaskContext = Context export type ClusterBRServiceListClusterBRTasksContext = Context export type ClusterBRServiceDetectClusterContext = Context export type MetricsServiceGetClusterMetricDataContext = Context export type MetricsServiceGetClusterMetricInstanceContext = Context export type DiagnosisServiceGetResourceGroupListContext = Context export type ClusterServiceGetProcessListContext = Context export type ClusterServiceDeleteProcessContext = Context export type DiagnosisServiceGetSlowQueryListContext = Context export type DiagnosisServiceGetSlowQueryAvailableAdvancedFiltersContext = Context export type DiagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoContext = Context export type DiagnosisServiceDownloadSlowQueryListContext = Context export type DiagnosisServiceGetSlowQueryAvailableFieldsContext = Context export type DiagnosisServiceGetSlowQueryDetailContext = Context export type DiagnosisServiceAddSqlLimitContext = Context export type DiagnosisServiceCheckSqlLimitSupportContext = Context export type DiagnosisServiceRemoveSqlLimitContext = Context export type DiagnosisServiceGetSqlLimitListContext = Context export type DiagnosisServiceGetSqlPlanListContext = Context export type DiagnosisServiceBindSqlPlanContext = Context export type DiagnosisServiceCheckSqlPlanSupportContext = Context export type DiagnosisServiceGetSqlPlanBindingListContext = Context export type DiagnosisServiceUnbindSqlPlanContext = Context export type DiagnosisServiceGetTopSqlListContext = Context export type DiagnosisServiceGetTopSqlAvailableAdvancedFiltersContext = Context export type DiagnosisServiceGetTopSqlAvailableAdvancedFilterInfoContext = Context export type DiagnosisServiceGetTopSqlConfigsContext = Context export type DiagnosisServiceUpdateTopSqlConfigsContext = Context export type DiagnosisServiceGetTopSqlAvailableFieldsContext = Context export type DiagnosisServiceGetTopSqlDetailContext = Context export type CredentialServiceListCredentialsContext = Context export type CredentialServiceCreateCredentialContext = Context export type CredentialServiceGetCredentialContext = Context export type CredentialServiceDeleteCredentialContext = Context export type CredentialServiceUpdateCredentialContext = Context export type CredentialServiceDownloadRSAKeyContext = Context export type CredentialServiceGenerateRSAKeyContext = Context export type CredentialServiceValidateConnectionContext = Context export type HostServiceListHostsContext = Context export type HostServiceCreateHostsContext = Context export type HostServiceImportContext = Context export type HostServiceImportTaskContext = Context export type HostServiceHostConfirmContext = Context export type HostServiceGetHostContext = Context export type HostServiceDeleteContext = Context export type HostServiceUpdateHostContext = Context export type HostServiceGetDisksContext = Context export type MetricsServiceGetHostMetricDataContext = Context export type HostServiceReportContext = Context export type HostServiceGetTiDBProcessesContext = Context export type HostServiceFixContext = Context export type HostServiceCheckContext = Context export type HostServiceBatchDeleteContext = Context export type HostServiceDownloadListHostsContext = Context export type HostServiceDownloadHostTemplateContext = Context export type LicenseServiceGetLicenseContext = Context export type LicenseServiceGetDeviceCodeContext = Context export type LicenseServiceActivateLicenseContext = Context export type LicenseServiceActivateFreeLicenseContext = Context export type LocationServiceListLocationsContext = Context export type LocationServiceCreateLocationsContext = Context export type LocationServiceGetLocationsContext = Context export type LocationServiceDeleteLocationContext = Context export type LocationServiceUpdateLocationsContext = Context export type UserServiceLoginContext = Context export type UserServiceLogoutContext = Context export type MetricsServiceGetMetricsContext = Context export type MetricsServiceGetTopMetricConfigContext = Context export type MetricsServiceGetTopMetricDataContext = Context export type MetricsServiceGetOverviewStatusContext = Context export type RoleServiceListRolesContext = Context export type RoleServiceCreateRoleContext = Context, }, out: { json: NonReadonly, } }> export type RoleServiceDeleteRoleContext = Context export type RoleServiceUpdateRoleContext = Context export type TagServiceListTagsContext = Context export type TagServiceCreateTagContext = Context export type TagServiceGetTagContext = Context export type TagServiceDeleteTagContext = Context export type TagServiceUpdateTagContext = Context export type TagServiceGetTagWithBindingsContext = Context export type TagServiceBatchCreateTagsContext = Context export type TagServiceBindResourceContext = Context export type TagServiceBindTagContext = Context export type TagServiceListTagsByResourceTypeContext = Context export type TagServiceListTagKeysContext = Context export type TagServiceListTagsWithBindingsContext = Context export type TiupsServiceListTiupsContext = Context export type TiupsServiceCreateTiupsContext = Context export type TiupsServiceGetTiupsContext = Context export type TiupsServiceDeleteTiupsContext = Context export type TiupsServiceUpdateTiupsContext = Context export type TiupsServiceGetTiupsClusterContext = Context export type UserServiceListUsersContext = Context export type UserServiceCreateUserContext = Context, }, out: { json: NonReadonly, } }> export type UserServiceGetUserProfileContext = Context export type UserServiceGetUserContext = Context export type UserServiceDeleteUserContext = Context export type UserServiceUpdateUserContext = Context export type UserServiceResetPasswordContext = Context export type UserServiceChangePasswordContext = Context export type UserServiceValidateSessionContext = Context export type ApiKeyServiceGetTemErrorDetailContext = Context ================================================ FILE: ui-v2/packages/api/server/src/azores/index.schemas.ts ================================================ /** * Generated by orval v7.3.0 🍺 * Do not edit manually. * Azores Open API * OpenAPI spec version: 2.0.0 */ export type UserServiceChangePassword200 = { [key: string]: unknown }; export type UserServiceResetPassword200 = { [key: string]: unknown }; export type UserServiceDeleteUser200 = { [key: string]: unknown }; export type UserServiceListUsersParams = { /** * The number of users to retrieve per page. */ pageSize?: number; /** * Pagination token for retrieving the next page of users. */ pageToken?: string; /** * The number of users to skip for pagination purposes. */ skip?: number; /** * The sorting criteria for the user list. */ orderBy?: string; /** * Filter users by username using a "like" operation. */ nameLike?: string; /** * Filter users by email using a "like" operation. */ emailLike?: string; /** * Filter users by role name. */ roleName?: string; }; export type TiupsServiceDeleteTiups200 = { [key: string]: unknown }; export type TiupsServiceListTiupsParams = { /** * page size */ pageSize?: number; /** * page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; /** * the Tiups key of the Tiups */ searchValue?: string; /** * the Tiups tag_ids of the tagIds */ tagIds?: string[]; /** * the Tiups host_ids of the tagIds */ hostIds?: string[]; }; export type TagServiceListTagsWithBindingsParams = { /** * page size */ pageSize?: number; /** * page token */ pageToken?: string; /** * skip */ skip?: number; /** * order_by */ orderBy?: string; /** * the tag which the tag key in */ tagKeys?: string[]; /** * the tag which the tag value like */ tagValueLike?: string; }; export type TagServiceListTagKeysResourceType = typeof TagServiceListTagKeysResourceType[keyof typeof TagServiceListTagKeysResourceType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const TagServiceListTagKeysResourceType = { TAG_BIND_RESOURCE_TYPE_UNSPECIFIED: 'TAG_BIND_RESOURCE_TYPE_UNSPECIFIED', HOST: 'HOST', TIUP: 'TIUP', CLUSTER: 'CLUSTER', } as const; export type TagServiceListTagKeysParams = { /** * page size */ pageSize?: number; /** * page token */ pageToken?: string; /** * skip */ skip?: number; /** * the keyword which tag key similar to */ keyword?: string; /** * the resource type of the tag has bound with - TAG_BIND_RESOURCE_TYPE_UNSPECIFIED: resource type unspecified - HOST: resource type host - TIUP: resource type tiup - CLUSTER: resource type cluster */ resourceType?: TagServiceListTagKeysResourceType; }; export type TagServiceListTagsByResourceTypeResourceType = typeof TagServiceListTagsByResourceTypeResourceType[keyof typeof TagServiceListTagsByResourceTypeResourceType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const TagServiceListTagsByResourceTypeResourceType = { TAG_BIND_RESOURCE_TYPE_UNSPECIFIED: 'TAG_BIND_RESOURCE_TYPE_UNSPECIFIED', HOST: 'HOST', TIUP: 'TIUP', CLUSTER: 'CLUSTER', } as const; export type TagServiceListTagsByResourceTypeParams = { /** * page size */ pageSize?: number; /** * page token */ pageToken?: string; /** * skip */ skip?: number; /** * the tag key which the tag values belong to */ tagKey?: string; /** * the keyword which tag values similar to */ keyword?: string; /** * the resource type of the tag has bound with - TAG_BIND_RESOURCE_TYPE_UNSPECIFIED: resource type unspecified - HOST: resource type host - TIUP: resource type tiup - CLUSTER: resource type cluster */ resourceType?: TagServiceListTagsByResourceTypeResourceType; }; export type TagServiceDeleteTag200 = { [key: string]: unknown }; export type TagServiceListTagsParams = { /** * page size */ pageSize?: number; /** * page token */ pageToken?: string; /** * skip */ skip?: number; /** * order_by */ orderBy?: string; }; export type RoleServiceDeleteRole200 = { [key: string]: unknown }; export type RoleServiceListRolesParams = { /** * Page size */ pageSize?: number; /** * Page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; /** * role_name_like */ roleNameLike?: string; /** * The name of the role */ roleName?: string; }; export type MetricsServiceGetOverviewStatusParams = { /** * Task start time in Unix timestamp format */ taskStartTime?: string; /** * Task end time in Unix timestamp format */ taskEndTime?: string; }; export type MetricsServiceGetTopMetricDataParams = { /** * Start time for the query */ startTime: string; /** * End time for the query */ endTime: string; /** * Step time for the query */ step?: string; /** * Limit for the number of top results */ limit?: string; }; export type MetricsServiceGetMetricsGroup = typeof MetricsServiceGetMetricsGroup[keyof typeof MetricsServiceGetMetricsGroup]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const MetricsServiceGetMetricsGroup = { unspecified: 'unspecified', overview: 'overview', basic: 'basic', advanced: 'advanced', resource: 'resource', performance: 'performance', process: 'process', } as const; export type MetricsServiceGetMetricsClass = typeof MetricsServiceGetMetricsClass[keyof typeof MetricsServiceGetMetricsClass]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const MetricsServiceGetMetricsClass = { unspecified: 'unspecified', cluster: 'cluster', host: 'host', overview: 'overview', } as const; export type MetricsServiceGetMetricsParams = { /** * Level 1 classification - unspecified: Unspecified - cluster: Cluster metrics - host: Host metrics - overview: Overview metrics */ class?: MetricsServiceGetMetricsClass; /** * Level 2 grouping - unspecified: Unspecified group - overview: Overview group - basic: Basic group - advanced: Advanced group - resource: Resource group - performance: Performance group - process: Process group */ group?: MetricsServiceGetMetricsGroup; /** * Level 3 type */ type?: string; /** * The metric name */ name?: string; }; export type UserServiceLogout200 = { [key: string]: unknown }; export type UserServiceLogin200 = { [key: string]: unknown }; export type LocationServiceDeleteLocation200 = { [key: string]: unknown }; export type LocationServiceListLocationsLocationKey = typeof LocationServiceListLocationsLocationKey[keyof typeof LocationServiceListLocationsLocationKey]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const LocationServiceListLocationsLocationKey = { zone: 'zone', dc: 'dc', rack: 'rack', } as const; export type LocationServiceListLocationsParams = { /** * page size */ pageSize?: number; /** * page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; /** * location key (e.g., "zone", "dc") */ locationKey?: LocationServiceListLocationsLocationKey; /** * the Location value of the Location */ locationValue?: string; /** * the Location parent_Id of the Location */ parentId?: string; }; export type LicenseServiceActivateLicenseBody = { /** The content of the license file The license file to upload to activate the license */ license: Blob; }; export type HostServiceDownloadListHostsParams = { /** * Page size */ pageSize?: number; /** * Page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; /** * The name of the user */ searchValue?: string; /** * location_ids */ locationIds?: string[]; /** * tag_ids */ tagIds?: string[]; }; export type HostServiceBatchDelete200 = { [key: string]: unknown }; export type MetricsServiceGetHostMetricDataParams = { /** * Start time in Unix timestamp format */ startTime: string; /** * End time in Unix timestamp format */ endTime: string; /** * Step time in seconds */ step?: string; /** * Line Label for the metric */ label?: string; /** * Time Range for the query */ range?: string; }; export type HostServiceDelete200 = { [key: string]: unknown }; export type HostServiceImportBody = { /** The Credential_Id of the Import */ credentialId: string; /** Upload a csv form data to host. */ hostData: Blob; }; export type HostServiceListHostsParams = { /** * Page size */ pageSize?: number; /** * Page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; /** * The name of the user */ searchValue?: string; /** * location_ids */ locationIds?: string[]; /** * tag_ids */ tagIds?: string[]; }; export type CredentialServiceDeleteCredential200 = { [key: string]: unknown }; export type CredentialServiceListCredentialsCredentialType = typeof CredentialServiceListCredentialsCredentialType[keyof typeof CredentialServiceListCredentialsCredentialType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const CredentialServiceListCredentialsCredentialType = { CREDENTIAL_TYPE_UNSPECIFIED: 'CREDENTIAL_TYPE_UNSPECIFIED', HOST: 'HOST', TIDB: 'TIDB', } as const; export type CredentialServiceListCredentialsParams = { /** * page size */ pageSize?: number; /** * page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; /** * the credential type of the credential - CREDENTIAL_TYPE_UNSPECIFIED: resource type unspecified - HOST: credential type host - TIDB: credential type tidb */ credentialType?: CredentialServiceListCredentialsCredentialType; /** * the credential id of the credential */ credentialId?: string; }; export type DiagnosisServiceGetTopSqlDetailParams = { /** * Begin time */ beginTime: string; /** * End time */ endTime: string; /** * Plan digest list */ planDigest?: string[]; }; export type DiagnosisServiceGetTopSqlListParams = { /** * Begin time */ beginTime: string; /** * End time */ endTime: string; /** * Database list */ db?: string[]; /** * SQL Text, used for fuzzy query */ text?: string; /** * Order by field */ orderBy?: string; /** * Is descending order */ isDesc?: boolean; /** * Fields to select, e.g., "Query,Digest" */ fields?: string; /** * Page size */ pageSize?: number; /** * Page token */ pageToken?: string; /** * Skip */ skip?: number; /** * Advanced filters, such as "digest = xxx" */ advancedFilter?: string[]; /** * Is group by time */ isGroupByTime?: boolean; }; export type DiagnosisServiceUnbindSqlPlan200 = { [key: string]: unknown }; export type DiagnosisServiceUnbindSqlPlanParams = { /** * SQL digest */ digest: string; }; export type DiagnosisServiceGetSqlPlanBindingListParams = { /** * Begin time */ beginTime: string; /** * End time */ endTime: string; /** * SQL digest */ digest: string; }; export type DiagnosisServiceBindSqlPlan200 = { [key: string]: unknown }; export type DiagnosisServiceGetSqlPlanListParams = { /** * Begin time */ beginTime: string; /** * End time */ endTime: string; /** * SQL digest */ digest?: string; /** * Table name */ schemaName?: string; }; export type DiagnosisServiceGetSqlLimitListParams = { /** * Watch text */ watchText: string; }; export type DiagnosisServiceRemoveSqlLimit200 = { [key: string]: unknown }; export type DiagnosisServiceAddSqlLimit200 = { [key: string]: unknown }; export type DiagnosisServiceGetSlowQueryDetailParams = { /** * Timestamp */ timestamp: number; /** * Connection ID */ connectionId: string; }; export type DiagnosisServiceDownloadSlowQueryListParams = { /** * Begin time in Unix timestamp */ beginTime: string; /** * End time in Unix timestamp */ endTime: string; /** * List of databases */ db?: string[]; /** * Search text */ text?: string; /** * Order by field */ orderBy?: string; /** * Is descending order */ isDesc?: boolean; /** * Fields to select, e.g., "Query,Digest" */ fields?: string; /** * Page size */ pageSize?: number; /** * Page token for pagination */ pageToken?: string; /** * Number of records to skip */ skip?: number; /** * Advanced filters, such as "digest = xxx" */ advancedFilter?: string[]; }; export type DiagnosisServiceGetSlowQueryListParams = { /** * Begin time in Unix timestamp */ beginTime: string; /** * End time in Unix timestamp */ endTime: string; /** * List of databases */ db?: string[]; /** * Search text */ text?: string; /** * Order by field */ orderBy?: string; /** * Is descending order */ isDesc?: boolean; /** * Fields to select, e.g., "Query,Digest" */ fields?: string; /** * Page size */ pageSize?: number; /** * Page token for pagination */ pageToken?: string; /** * Number of records to skip */ skip?: number; /** * Advanced filters, such as "digest = xxx" */ advancedFilter?: string[]; }; export type ClusterServiceDeleteProcess200 = { [key: string]: unknown }; export type MetricsServiceGetClusterMetricDataParams = { /** * Start time in Unix timestamp format */ startTime: string; /** * End time in Unix timestamp format */ endTime: string; /** * Step time in seconds */ step?: string; /** * Line Label for the metric */ label?: string; /** * Time Range for the query */ range?: string; }; export type ClusterBRServiceListClusterBRTasksStatus = typeof ClusterBRServiceListClusterBRTasksStatus[keyof typeof ClusterBRServiceListClusterBRTasksStatus]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const ClusterBRServiceListClusterBRTasksStatus = { running: 'running', finished: 'finished', abnormal: 'abnormal', stopped: 'stopped', } as const; export type ClusterBRServiceListClusterBRTasksType = typeof ClusterBRServiceListClusterBRTasksType[keyof typeof ClusterBRServiceListClusterBRTasksType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const ClusterBRServiceListClusterBRTasksType = { full_backup: 'full_backup', log_backup: 'log_backup', restore_by_file: 'restore_by_file', restore_by_time: 'restore_by_time', all_backup: 'all_backup', all_restore: 'all_restore', } as const; export type ClusterBRServiceListClusterBRTasksParams = { /** * Page size */ pageSize?: number; /** * Page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; /** * The br task ID */ brTaskId?: string; /** * The cluster name */ clusterName?: string; /** * Type of the br task - full_backup: Full backup - log_backup: Log backup - restore_by_file: Restore by file - restore_by_time: Restore by time - all_backup: All backup - all_restore: All restore */ type?: ClusterBRServiceListClusterBRTasksType; /** * Status of the br task - running: Running - finished: Finished - abnormal: Abnormal - stopped: Stopped */ status?: ClusterBRServiceListClusterBRTasksStatus; }; export type ClusterBRServiceListClusterBackupRecordsParams = { /** * Page size */ pageSize?: number; /** * Page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; }; export type GlobalBRServiceStopBRTask200 = { [key: string]: unknown }; export type GlobalBRServiceStartBRTask200 = { [key: string]: unknown }; export type GlobalBRServiceDeleteBRTask200 = { [key: string]: unknown }; export type GlobalBRServiceDeleteBRTaskParams = { /** * delete_backup_file for whether delete the backup files or not */ deleteBackupFile?: boolean; }; export type GlobalBRServiceListBRTasksStatus = typeof GlobalBRServiceListBRTasksStatus[keyof typeof GlobalBRServiceListBRTasksStatus]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const GlobalBRServiceListBRTasksStatus = { all: 'all', running: 'running', finished: 'finished', abnormal: 'abnormal', stopped: 'stopped', } as const; export type GlobalBRServiceListBRTasksType = typeof GlobalBRServiceListBRTasksType[keyof typeof GlobalBRServiceListBRTasksType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const GlobalBRServiceListBRTasksType = { all: 'all', full_backup: 'full_backup', log_backup: 'log_backup', restore_by_file: 'restore_by_file', restore_by_time: 'restore_by_time', all_backup: 'all_backup', all_restore: 'all_restore', } as const; export type GlobalBRServiceListBRTasksParams = { /** * Page size */ pageSize?: number; /** * Page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; /** * The br task ID */ brTaskId?: string; /** * The cluster ID */ clusterId?: string; /** * The cluster name */ clusterName?: string; /** * Type of the br task - all: All - full_backup: Full backup - log_backup: Log backup - restore_by_file: Restore by file - restore_by_time: Restore by time - all_backup: All backup - all_restore: All restore */ type?: GlobalBRServiceListBRTasksType; /** * Status of the br task - all: All - running: Running - finished: Finished - abnormal: Abnormal - stopped: Stopped */ status?: GlobalBRServiceListBRTasksStatus; }; export type GlobalBRServiceGetBRSummaryParams = { /** * Number of top clusters */ top?: number; }; export type GlobalBRServiceDeleteBackupPolicy200 = { [key: string]: unknown }; export type GlobalBRServiceListBackupPoliciesParams = { /** * Page size */ pageSize?: number; /** * Page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; }; export type ApiKeyServiceDeleteApiKey200 = { [key: string]: unknown }; export type ApiKeyServiceListApiKeysParams = { /** * Page size */ pageSize?: number; /** * Page token */ pageToken?: string; /** * Skip */ skip?: number; /** * order_by */ orderBy?: string; /** * The access_key of the apikey */ accessKey?: string; /** * The access_key of the apikey */ creator?: string; /** * The status of the apikey */ status?: string; }; export type V2BackupPolicyBody = V2BackupPolicy; export interface V2ValidateSessionResponse { userId: string; } export interface V2ValidateConnectionResponse { connectionResult?: string; inaccessibleHosts?: string[]; } export interface V2ValidateConnectionRequest { credentialId: string; } export interface V2UserRole { roleId: number; roleName?: string; } /** * UserProfile represents the profile information of the authenticated user. */ export interface V2UserProfile { /** The email address of the user. */ email: string; /** The name of the user. */ name: string; /** The note of the user. */ note?: string; /** The phone of the user. */ phone?: string; /** The unique identifier of the user. */ userId: string; } /** * User represents a user resource containing detailed information about a user. */ export interface V2User { /** The timestamp when the user was created. */ readonly createTime?: string; /** The email address of the user. */ email?: string; /** The full name of the user. */ name: string; /** Additional notes about the user. */ note?: string; /** The user's password (optional). */ password?: string; /** The user's phone number. */ phone?: string; /** The roles assigned to the user. */ roles?: V2UserRole[]; /** The timestamp when the user was last updated. */ readonly updateTime?: string; /** The unique user ID of the user. */ userId: string; /** The type of the user (e.g., admin, regular user). */ userType?: number; /** A description of the user's type. */ userTypeDesc?: string; } /** * - all: All - full_backup: Full backup - log_backup: Log backup - restore_by_file: Restore by file - restore_by_time: Restore by time - all_backup: All backup - all_restore: All restore */ export type V2TypeEnumData = typeof V2TypeEnumData[keyof typeof V2TypeEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2TypeEnumData = { all: 'all', full_backup: 'full_backup', log_backup: 'log_backup', restore_by_file: 'restore_by_file', restore_by_time: 'restore_by_time', all_backup: 'all_backup', all_restore: 'all_restore', } as const; /** * - automatic: automatic - manual: manual */ export type V2TriggerTypeEnumData = typeof V2TriggerTypeEnumData[keyof typeof V2TriggerTypeEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2TriggerTypeEnumData = { automatic: 'automatic', manual: 'manual', } as const; export interface V2TopSqlDetail { avg_affected_rows?: number; avg_backoff_time?: number; avg_commit_backoff_time?: number; avg_commit_time?: number; avg_compile_latency?: number; avg_cop_process_time?: number; avg_cop_wait_time?: number; avg_disk?: number; avg_get_commit_ts_time?: number; avg_latency?: number; avg_local_latch_wait_time?: number; avg_mem?: number; avg_parse_latency?: number; avg_prewrite_regions?: number; avg_prewrite_time?: number; avg_process_time?: number; avg_processed_keys?: number; avg_resolve_lock_time?: number; avg_rocksdb_block_cache_hit_count?: number; avg_rocksdb_block_read_byte?: number; avg_rocksdb_block_read_count?: number; avg_rocksdb_delete_skipped_count?: number; avg_rocksdb_key_skipped_count?: number; avg_ru?: number; avg_tidb_cpu_time?: number; avg_tikv_cpu_time?: number; avg_time_queued_by_rc?: number; avg_total_keys?: number; avg_txn_retry?: number; avg_wait_time?: number; avg_write_keys?: number; avg_write_size?: number; binary_plan?: string; binary_plan_text?: string; digest?: string; digest_text?: string; exec_count?: number; first_seen?: number; index_names?: string; last_seen?: number; max_backoff_time?: number; max_commit_backoff_time?: number; max_commit_time?: number; max_compile_latency?: number; max_cop_process_time?: number; max_cop_wait_time?: number; max_disk?: number; max_get_commit_ts_time?: number; max_latency?: number; max_local_latch_wait_time?: number; max_mem?: number; max_parse_latency?: number; max_prewrite_regions?: number; max_prewrite_time?: number; max_process_time?: number; max_processed_keys?: number; max_resolve_lock_time?: number; max_rocksdb_block_cache_hit_count?: number; max_rocksdb_block_read_byte?: number; max_rocksdb_block_read_count?: number; max_rocksdb_delete_skipped_count?: number; max_rocksdb_key_skipped_count?: number; max_ru?: number; max_time_queued_by_rc?: number; max_total_keys?: number; max_txn_retry?: number; max_wait_time?: number; max_write_keys?: number; max_write_size?: number; min_latency?: number; plan?: string; plan_can_be_bound?: boolean; plan_count?: number; plan_digest?: string; plan_hint?: string; prev_sample_text?: string; query_sample_text?: string; related_schemas?: string; resource_group?: string; sample_user?: string; schema_name?: string; stmt_type?: string; sum_backoff_times?: number; sum_cop_task_num?: number; sum_errors?: number; sum_latency?: number; sum_ru?: number; sum_warnings?: number; summary_begin_time?: number; summary_end_time?: number; table_names?: string; } export interface V2TopSqlList { data?: V2TopSqlDetail[]; nextPageToken?: string; totalSize?: number; } export interface V2TopSqlConfigs { enable?: boolean; historySize?: number; internalQuery?: boolean; maxSize?: number; refreshInterval?: number; } export interface V2TopSqlAvailableFields { fields?: string[]; } export interface V2TopSqlAvailableAdvancedFilters { filters?: string[]; } export interface V2TopSqlAvailableAdvancedFilterInfo { name?: string; type?: string; unit?: string; valueList?: string[]; } export interface V2TopMetricData { data?: V2ExprQueryData[]; status?: string; } export interface V2TopMetricConfig { cacheFlushIntervalInMinutes?: number; } export interface V2TiupsServiceUpdateTiupsBody { tiups?: Tiupv2UpdateTiups; } export interface V2TiupsClusters { clusterId?: string; clusterName?: string; managed?: boolean; metaPath?: string; privateKeyPath?: string; user?: string; version?: string; } export interface V2TiupsClustersResponse { tiupsClusters?: V2TiupsClusters[]; } export interface V2TiupTags { tagId?: string; tagKey?: string; tagValue: string; } export interface V2Tiups { credentialId?: string; description?: string; host?: V2TiupHost; hostId?: string; name?: string; tags?: V2TiupTags[]; tiupHome?: string; tiupId?: string; version?: string; } export type V2TiupHostHostType = typeof V2TiupHostHostType[keyof typeof V2TiupHostHostType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2TiupHostHostType = { VM: 'VM', PM: 'PM', } as const; export interface V2TiupCredential { credentialId?: string; credentialName?: string; credentialType?: string; userName?: string; } export interface V2TiupHost { createdTime?: string; credential?: V2TiupCredential; credentialId?: string; hostId: string; hostName?: string; hostType?: V2TiupHostHostType; ip?: string; locationId?: string; osArchitecture?: string; osName?: string; osRelease?: string; osVersion?: string; sshPort?: number; updatedTime?: string; } export interface V2TiDBProcesses { cmd?: string; pid?: number; ppid?: number; runningTime?: string; startTime?: string; uid?: string; } export interface V2TiDBCredentialObject { clusterId?: string; clusterName?: string; password: string; } export interface V2Tags { tagId?: string; tagKey?: string; tagValue: string; } export interface V2TagWithBindObject { bindObjects?: V2BindObject[]; tagInfo?: Tagv2Tag; } /** * - TAG_BIND_RESOURCE_TYPE_UNSPECIFIED: resource type unspecified - HOST: resource type host - TIUP: resource type tiup - CLUSTER: resource type cluster */ export type V2TagBindResourceType = typeof V2TagBindResourceType[keyof typeof V2TagBindResourceType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2TagBindResourceType = { TAG_BIND_RESOURCE_TYPE_UNSPECIFIED: 'TAG_BIND_RESOURCE_TYPE_UNSPECIFIED', HOST: 'HOST', TIUP: 'TIUP', CLUSTER: 'CLUSTER', } as const; /** * - all: All - running: Running - finished: Finished - abnormal: Abnormal - stopped: Stopped */ export type V2StatusEnumData = typeof V2StatusEnumData[keyof typeof V2StatusEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2StatusEnumData = { all: 'all', running: 'running', finished: 'finished', abnormal: 'abnormal', stopped: 'stopped', } as const; export interface V2StatusCount { count?: number; status?: string; } export interface V2SqlPlanList { data?: V2TopSqlDetail[]; } export interface V2SqlPlanBindingList { data?: V2SqlPlanBindingDetail[]; } export type V2SqlPlanBindingDetailStatus = typeof V2SqlPlanBindingDetailStatus[keyof typeof V2SqlPlanBindingDetailStatus]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2SqlPlanBindingDetailStatus = { enabled: 'enabled', using: 'using', disabled: 'disabled', deleted: 'deleted', invalid: 'invalid', rejected: 'rejected', pending_verify: 'pending verify', } as const; export type V2SqlPlanBindingDetailSource = typeof V2SqlPlanBindingDetailSource[keyof typeof V2SqlPlanBindingDetailSource]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2SqlPlanBindingDetailSource = { manual: 'manual', history: 'history', capture: 'capture', evolve: 'evolve', } as const; export interface V2SqlPlanBindingDetail { digest?: string; planDigest?: string; source?: V2SqlPlanBindingDetailSource; status?: V2SqlPlanBindingDetailStatus; } export type V2SqlLimitAction = typeof V2SqlLimitAction[keyof typeof V2SqlLimitAction]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2SqlLimitAction = { DRYRUN: 'DRYRUN', COOLDOWN: 'COOLDOWN', KILL: 'KILL', } as const; export interface V2SqlLimit { action?: V2SqlLimitAction; endTime?: string; id?: string; resourceGroupName?: string; source?: string; startTime?: string; watch?: string; watchText?: string; } export interface V2SqlLimitList { data?: V2SqlLimit[]; } export interface V2SlowQueryDownloadResponse { fileContent?: string; filename?: string; } export interface V2SlowQueryDetail { backoff_detail?: string; backoff_time?: number; backoff_total?: number; backoff_types?: string; binary_plan?: string; binary_plan_text?: string; commit_backoff_time?: number; commit_time?: number; compile_time?: number; connection_id?: string; cop_proc_addr?: string; cop_proc_avg?: number; cop_proc_max?: number; cop_proc_p90?: number; cop_time?: number; cop_wait_addr?: string; cop_wait_avg?: number; cop_wait_max?: number; cop_wait_p90?: number; db?: string; digest?: string; disk_max?: number; exec_retry_count?: number; exec_retry_time?: number; get_commit_ts_time?: number; has_more_results?: number; host?: string; index_names?: string; instance?: string; is_explicit_txn?: number; is_internal?: number; kv_total?: number; local_latch_wait_time?: number; lock_keys_time?: number; memory_max?: number; optimize_time?: number; parse_time?: number; pd_total?: number; plan?: string; plan_digest?: string; plan_from_binding?: number; plan_from_cache?: number; prepared?: number; preproc_subqueries?: number; preproc_subqueries_time?: number; prev_stmt?: string; prewrite_region?: number; prewrite_time?: number; process_keys?: number; process_time?: number; query?: string; query_time?: number; request_count?: number; request_unit_read?: number; request_unit_write?: number; resolve_lock_time?: number; resource_group?: string; result_rows?: number; rewrite_time?: number; rocksdb_block_cache_hit_count?: number; rocksdb_block_read_byte?: number; rocksdb_block_read_count?: number; rocksdb_delete_skipped_count?: number; rocksdb_key_skipped_count?: number; ru?: number; session_alias?: string; stats?: string; success?: number; tidb_cpu_time?: number; tikv_cpu_time?: number; time_queued_by_rc?: number; timestamp?: number; total_keys?: number; txn_retry?: number; txn_start_ts?: string; user?: string; wait_prewrite_binlog_time?: number; wait_time?: number; wait_ts?: number; warnings?: string; write_keys?: number; write_size?: number; write_sql_response_total?: number; } export interface V2SlowQueryList { data?: V2SlowQueryDetail[]; nextPageToken?: string; totalSize?: number; } export interface V2SlowQueryAvailableFields { fields?: string[]; } export interface V2SlowQueryAvailableAdvancedFilters { filters?: string[]; } export interface V2SlowQueryAvailableAdvancedFilterInfo { name?: string; type?: string; unit?: string; valueList?: string[]; } export interface V2Role { readonly createTime?: string; detail?: string; id?: number; note?: string; roleName?: string; roleType?: number; roleTypeDesc?: string; readonly updateTime?: string; } export interface V2ResourceObject { resourceId?: string; resourceName: string; } export interface V2ResourceGroup { burstable?: string; name?: string; priority?: string; ruPerSec?: string; } export interface V2ResourceGroupList { resourceGroups?: V2ResourceGroup[]; } export interface V2ResetSecretKeyResponse { accessKey: string; secretKey: string; } export type V2ReportResponseTaskState = typeof V2ReportResponseTaskState[keyof typeof V2ReportResponseTaskState]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2ReportResponseTaskState = { init: 'init', running: 'running', success: 'success', fail: 'fail', } as const; export interface V2ReportResponse { reports?: Hostv2Report[]; taskId: string; taskState?: V2ReportResponseTaskState; } export interface V2QueryMetric { device?: string; fstype?: string; instance?: string; job?: string; kind?: string; module?: string; mountpoint?: string; ping?: string; result?: string; sqlType?: string; txnMode?: string; type?: string; } export interface V2QueryResult { metric?: V2QueryMetric; values?: Metricsv2Value[]; } export interface V2ProcessList { activeProcessCount?: string; clusterProcessList?: V2ClusterProcess[]; isSupportKill?: boolean; totalProcessCount?: string; } export interface V2PreCheckBackupPolicyResponse { clusters?: V2Cluster[]; } export interface V2OverviewStatus { alertLevels?: V2StatusCount[]; alerts?: V2StatusCount[]; brTasks?: V2StatusCount[]; clusters?: V2StatusCount[]; hosts?: V2StatusCount[]; otherTasks?: V2StatusCount[]; sysTasks?: V2StatusCount[]; } export interface V2Metrics { metrics?: V2CategoryMetricDetail[]; } export interface V2MetricWithExpressions { description?: string; expressions?: V2ExpressionWithLegend[]; isBuiltin?: boolean; maxTidbVersion?: string; minTidbVersion?: string; name?: string; unit?: string; } export interface V2LoginRequest { password?: string; userId: string; } export type V2LocationsLocationKey = typeof V2LocationsLocationKey[keyof typeof V2LocationsLocationKey]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2LocationsLocationKey = { zone: 'zone', dc: 'dc', rack: 'rack', } as const; export interface V2Locations { locationId?: string; locationKey?: V2LocationsLocationKey; locationValue?: string; parentId?: string; } export interface V2LocationMappings { locationId?: string; locationKey: string; locationValue: string; parentId?: string; } /** * ListUsersResponse defines the response containing a list of users and pagination information. */ export interface V2ListUsersResponse { /** Token for the next page of results. */ nextPageToken?: string; /** The total number of users that match the filter criteria. */ totalSize?: number; /** The list of users retrieved. */ users?: V2User[]; } export interface V2ListTiupsResponse { nextPageToken?: string; tiups?: V2Tiups[]; totalSize?: number; } export interface V2ListTagsWithBindingsResponse { nextPageToken?: string; tags?: V2TagWithBindObject[]; totalSize?: number; } export interface V2ListTagsResponse { nextPageToken?: string; tags?: Tagv2Tag[]; totalSize?: number; } export interface V2ListTagsByResourceTypeResponse { nextPageToken?: string; tags?: Tagv2Tag[]; totalSize?: number; } export interface V2ListTagKeysResponse { nextPageToken?: string; tagKeys?: string[]; totalSize?: number; } export interface V2ListRolesResponse { nextPageToken?: string; roles?: V2Role[]; totalSize?: number; } export interface V2ListLocationsResponse { locations?: V2Locations[]; nextPageToken?: string; totalSize?: number; } export interface V2ListHostsResponse { hosts: V2Host[]; nextPageToken: string; totalSize: number; } export interface V2ListCredentialsResponse { credentials?: V2Credential[]; nextPageToken?: string; totalSize?: number; } export interface V2ListClusterBackupRecordsResponse { backupRecords?: V2ClusterBRTask[]; nextPageToken?: string; totalSize?: number; } export interface V2ListClusterBRTasksResponse { brTasks?: V2ClusterBRTask[]; nextPageToken?: string; totalSize?: number; } export interface V2ListBackupPoliciesResponse { backupPolicies?: V2BackupPolicy[]; nextPageToken?: string; totalSize?: number; } export interface V2ListBRTasksResponse { brTasks?: V2BRTask[]; nextPageToken?: string; totalSize?: number; } export interface V2ListApiKeysResponse { apikeys?: V2ApiKey[]; nextPageToken?: string; totalSize?: number; } /** * - free: free - ultimate: ultimate */ export type V2LicenseTypeEnumData = typeof V2LicenseTypeEnumData[keyof typeof V2LicenseTypeEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2LicenseTypeEnumData = { free: 'free', ultimate: 'ultimate', } as const; /** * - active: active - expired: inactive - expiring: expired - invalid: invalid - revoked: revoked */ export type V2LicenseStatusEnumData = typeof V2LicenseStatusEnumData[keyof typeof V2LicenseStatusEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2LicenseStatusEnumData = { active: 'active', expired: 'expired', expiring: 'expiring', invalid: 'invalid', revoked: 'revoked', } as const; export type V2ImportRequestHeaders = {[key: string]: string}; export interface V2ImportRequest { credentialId?: string; fileName?: string; headers: V2ImportRequestHeaders; /** Upload a csv form data to host. */ hostData: Blob; } export interface V2HostTiDBProcessesResponse { tiDBProcesses?: V2TiDBProcesses[]; } export type V2HostTaskStatus = typeof V2HostTaskStatus[keyof typeof V2HostTaskStatus]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2HostTaskStatus = { init: 'init', existed: 'existed', succeeded: 'succeeded', failed: 'failed', } as const; export interface V2HostTask { credential?: V2Credential; credentialId?: string; hostId: string; hostName?: string; ip: string; locationId?: string; locationMappings?: V2LocationMappings[]; reportId?: string; sshPort: number; status?: V2HostTaskStatus; tags?: string; tagsList?: V2Tags[]; taskId: string; userName?: string; } export interface V2ImportTaskResponse { task: V2HostTask[]; taskId: string; } export interface V2HostServiceUpdateHostBody { host?: Hostv2UpdateHost; } export interface V2HostFixResponse { hostId: string; reportId: string; taskId: string; } export interface V2HostDiskResponse { disk?: V2Disk[]; } export interface V2HostCredentialObject { hostIps?: string[]; password?: string; privateKey?: string; publicKey?: string; } export interface V2HostCreateResponse { taskId?: string; } export interface V2HostCheckResponse { hostId: string; reportId: string; taskId: string; } export type V2HostStatus = typeof V2HostStatus[keyof typeof V2HostStatus]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2HostStatus = { initializing: 'initializing', deleting: 'deleting', deleted: 'deleted', used: 'used', idle: 'idle', } as const; export type V2HostHostType = typeof V2HostHostType[keyof typeof V2HostHostType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2HostHostType = { VM: 'VM', PM: 'PM', } as const; export type V2HostConnectionStatus = typeof V2HostConnectionStatus[keyof typeof V2HostConnectionStatus]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2HostConnectionStatus = { online: 'online', offline: 'offline', } as const; export type V2HostCheckStatus = typeof V2HostCheckStatus[keyof typeof V2HostCheckStatus]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2HostCheckStatus = { checking: 'checking', failed: 'failed', warning: 'warning', succeeded: 'succeeded', } as const; export interface V2Host { checkStatus?: V2HostCheckStatus; clusters?: V2AssociatedClusters[]; comment?: string; connectionStatus?: V2HostConnectionStatus; cpuArch?: string; cpuCache?: number; cpuCores?: number; cpuGovernor?: string; cpuModel?: string; cpuNumaNodes?: number; cpus?: number; cpuSpeed?: number; cpuThreads?: number; cpuVendor?: string; createdTime?: string; credential?: V2Credential; credentialId?: string; diskType?: string; hostId: string; hostName?: string; hostType?: V2HostHostType; ip?: string; locationId?: string; locationMappings?: V2LocationMappings[]; memorySize?: number; memorySpeed?: number; memorySwap?: number; memoryType?: string; memoryUnit?: string; nodeExporterPort?: number; osArchitecture?: string; osName?: string; osRelease?: string; osVendor?: string; osVersion?: string; reportId?: string; sshPort?: number; status?: V2HostStatus; storageAvailable?: number; storageTotalSize?: number; storageUnit?: string; storageUsed?: number; tags?: V2Tags[]; tiupIds?: string[]; updatedTime?: string; } /** * - unspecified: Unspecified group - overview: Overview group - basic: Basic group - advanced: Advanced group - resource: Resource group - performance: Performance group - process: Process group */ export type V2GroupEnumData = typeof V2GroupEnumData[keyof typeof V2GroupEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2GroupEnumData = { unspecified: 'unspecified', overview: 'overview', basic: 'basic', advanced: 'advanced', resource: 'resource', performance: 'performance', process: 'process', } as const; export interface V2GetTagWithBindingsResponse { tag?: V2TagWithBindObject; } export interface V2GenerateRSAKeyResponse { privateKey?: string; publicKey?: string; } export interface V2GenerateRSAKeyRequest { [key: string]: unknown } export interface V2ExpressionWithLegend { labels?: string[]; legend?: string; maxTidbVersion?: string; minTidbVersion?: string; name?: string; promMetric?: string; promql?: string; type?: string; } export interface V2ExprQueryData { expr?: string; legend?: string; result?: V2QueryResult[]; } export interface V2HostMetricData { data?: V2ExprQueryData[]; status?: string; } export interface V2ErrorDetail { locale?: string; message?: string; type?: string; } export interface V2DownloadRSAKeyResponse { data?: string; } export interface V2DownloadListHostResponse { data?: string; } export interface V2DownloadHostTemplateResponse { data?: string; } export type V2DiskDiskType = typeof V2DiskDiskType[keyof typeof V2DiskDiskType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2DiskDiskType = { HDD: 'HDD', SSD: 'SSD', } as const; export interface V2Disk { availableSpace?: number; diskType?: V2DiskDiskType; mountingDir?: string; path?: string; totalSize?: number; usedSpace?: number; } export interface V2DeviceCode { deviceCode?: string; } export interface V2DetectClusterResponse { exist?: boolean; } /** * - week: Week - month: Month */ export type V2CycleEnumData = typeof V2CycleEnumData[keyof typeof V2CycleEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2CycleEnumData = { week: 'week', month: 'month', } as const; /** * - CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED: validate type unspecified - PASSWORD: validate by password - RSAKEY: validate by rsa key */ export type V2CredentialValidateType = typeof V2CredentialValidateType[keyof typeof V2CredentialValidateType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2CredentialValidateType = { CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED: 'CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', PASSWORD: 'PASSWORD', RSAKEY: 'RSAKEY', } as const; /** * - CREDENTIAL_TYPE_UNSPECIFIED: resource type unspecified - HOST: credential type host - TIDB: credential type tidb */ export type V2CredentialType = typeof V2CredentialType[keyof typeof V2CredentialType]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2CredentialType = { CREDENTIAL_TYPE_UNSPECIFIED: 'CREDENTIAL_TYPE_UNSPECIFIED', HOST: 'HOST', TIDB: 'TIDB', } as const; export interface V2Credential { credentialId?: string; credentialName?: string; credentialType: V2CredentialType; description?: string; hostCredential?: V2HostCredentialObject; tidbCredential?: V2TiDBCredentialObject; userName: string; validateType: V2CredentialValidateType; } export interface V2CreateHost { comment?: string; credentialId?: string; ips?: string[]; locationId?: string; sshPort?: number; tagIds?: string[]; } export interface V2CreateApiKeyRequest { description: string; } export interface V2ConfirmResponse { taskId: string; } export interface V2ClusterWithoutBRPolicy { clusterId?: string; clusterName?: string; lastBackupTime?: string; sizeByte?: string; } export interface V2ClusterWithBRSize { clusterId?: string; clusterName?: string; totalSize?: string; totalSizeByte?: string; } export interface V2ClusterWithBRAlert { alertCount?: string; clusterId?: string; clusterName?: string; } export type V2ClusterProcessCommand = typeof V2ClusterProcessCommand[keyof typeof V2ClusterProcessCommand]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2ClusterProcessCommand = { Sleep: 'Sleep', Quit: 'Quit', Init_DB: 'Init DB', Query: 'Query', Field_List: 'Field List', Create_DB: 'Create DB', Drop_DB: 'Drop DB', Refresh: 'Refresh', Shutdown: 'Shutdown', Statistics: 'Statistics', Processlist: 'Processlist', Connect: 'Connect', Kill: 'Kill', Debug: 'Debug', Ping: 'Ping', Time: 'Time', Delayed_Insert: 'Delayed Insert', Change_User: 'Change User', Binlog_Dump: 'Binlog Dump', Table_Dump: 'Table Dump', Connect_out: 'Connect out', Register_Slave: 'Register Slave', Prepare: 'Prepare', Execute: 'Execute', Long_Data: 'Long Data', Close_stmt: 'Close stmt', Reset_stmt: 'Reset stmt', Set_option: 'Set option', Fetch: 'Fetch', Daemon: 'Daemon', Reset_connect: 'Reset connect', } as const; export interface V2ClusterProcess { command?: V2ClusterProcessCommand; db?: string; digest?: string; disk?: string; host?: string; id?: string; info?: string; instance?: string; mem?: string; resourceGroup?: string; rowsAffected?: string; sessionAlias?: string; state?: string; tidbCpu?: string; tikvCpu?: string; time?: string; txnStart?: string; user?: string; } export interface V2ClusterMetricInstance { instanceList?: string[]; type?: string; } export interface V2ClusterMetricData { data?: V2ExprQueryData[]; status?: string; } export interface V2ClusterBackupPolicy { accessKeyId?: string; clusters?: V2BasicClusterInfo[]; concurrency?: number; cycle: V2BackupCycleEnumData; destination: string; frequency: string; lastBackupTime?: string; lastLogBackupTime?: string; logBackup: boolean; logBackupDelay?: string; logFile?: string; name: string; policyId?: string; rateLimit?: number; retention: number; secretAccessKey?: string; size?: string; sizeByte?: string; time: string; } /** * - full_backup: Full backup - log_backup: Log backup - restore_by_file: Restore by file - restore_by_time: Restore by time - all_backup: All backup - all_restore: All restore */ export type V2ClusterBRTypeEnumData = typeof V2ClusterBRTypeEnumData[keyof typeof V2ClusterBRTypeEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2ClusterBRTypeEnumData = { full_backup: 'full_backup', log_backup: 'log_backup', restore_by_file: 'restore_by_file', restore_by_time: 'restore_by_time', all_backup: 'all_backup', all_restore: 'all_restore', } as const; /** * - automatic: automatic - manual: manual */ export type V2ClusterBRTriggerTypeEnumData = typeof V2ClusterBRTriggerTypeEnumData[keyof typeof V2ClusterBRTriggerTypeEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2ClusterBRTriggerTypeEnumData = { automatic: 'automatic', manual: 'manual', } as const; /** * - running: Running - finished: Finished - abnormal: Abnormal - stopped: Stopped */ export type V2ClusterBRStatusEnumData = typeof V2ClusterBRStatusEnumData[keyof typeof V2ClusterBRStatusEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2ClusterBRStatusEnumData = { running: 'running', finished: 'finished', abnormal: 'abnormal', stopped: 'stopped', } as const; export interface V2ClusterBRTask { accessKeyId?: string; clusterId?: string; clusterName?: string; concurrency?: number; destination?: string; endTime?: string; errorMessage?: string; expireTime?: string; log?: string; logFile?: string; name?: string; policyId?: string; policyName?: string; rateLimit?: number; restoredTs?: string; secretAccessKey?: string; size?: string; sizeByte?: string; startTime?: string; status?: V2ClusterBRStatusEnumData; taskId?: string; triggerType?: V2ClusterBRTriggerTypeEnumData; type?: V2ClusterBRTypeEnumData; } export interface V2Cluster { id?: string; name?: string; } /** * - unspecified: Unspecified - cluster: Cluster metrics - host: Host metrics - overview: Overview metrics */ export type V2ClassEnumData = typeof V2ClassEnumData[keyof typeof V2ClassEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2ClassEnumData = { unspecified: 'unspecified', cluster: 'cluster', host: 'host', overview: 'overview', } as const; export interface V2CheckSupportResponse { isSupport?: boolean; } export interface V2ChangePasswordRequest { newPassword: string; oldPassword?: string; userId: string; } export interface V2CategoryMetricDetail { class?: string; description?: string; displayName?: string; group?: string; metric?: V2MetricWithExpressions; name?: string; order?: number; type?: string; } export interface V2BindTagResponse { tag?: V2TagWithBindObject; } export interface V2BindResourceResponse { tags?: Tagv2Tag[]; } export interface V2BindResourceRequest { resourceId: string; resourceType: V2TagBindResourceType; tagIds?: string[]; } export interface V2BindObject { resources: V2ResourceObject[]; resourceType: V2TagBindResourceType; } export interface V2BindTagRequest { bindObjects?: V2BindObject[]; tagId: string; } export interface V2BatchDeleteRequest { hostId: string[]; } export interface V2BatchCreateTagsResponse { tags?: Tagv2Tag[]; } export interface V2BatchCreateTagsRequest { tags: Tagv2Tag[]; } export interface V2BasicClusterInfo { id?: string; name?: string; } export interface V2BackupPolicy { accessKeyId?: string; clusterIds?: string[]; clusters?: V2Cluster[]; concurrency?: number; cycle: V2CycleEnumData; destination: string; frequency: string; logBackup: boolean; logFile?: string; name: string; policyId?: string; rateLimit?: number; retention: number; secretAccessKey?: string; time: string; } /** * - week: Week - month: Month */ export type V2BackupCycleEnumData = typeof V2BackupCycleEnumData[keyof typeof V2BackupCycleEnumData]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2BackupCycleEnumData = { week: 'week', month: 'month', } as const; export interface V2BRTask { accessKeyId?: string; clusterId?: string; clusterName?: string; concurrency?: number; destination?: string; endTime?: string; errorMessage?: string; expireTime?: string; log?: string; logFile?: string; name?: string; policyId?: string; policyName?: string; rateLimit?: number; restoredTs?: string; secretAccessKey?: string; size?: string; sizeByte?: string; startTime?: string; status?: V2StatusEnumData; taskId?: string; triggerType?: V2TriggerTypeEnumData; type?: V2TypeEnumData; } export interface V2BRSummary { topClustersWithBrAlert?: V2ClusterWithBRAlert[]; topClustersWithBrSize?: V2ClusterWithBRSize[]; topClustersWithoutBrPolicy?: V2ClusterWithoutBRPolicy[]; } export interface V2AssociatedClusters { clusterId?: string; clusterName?: string; } export type V2ApiKeyStatus = typeof V2ApiKeyStatus[keyof typeof V2ApiKeyStatus]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const V2ApiKeyStatus = { disable: 'disable', enable: 'enable', } as const; export interface V2ApiKey { accessKey: string; readonly createTime?: string; creator?: string; description?: string; secretKey?: string; status?: V2ApiKeyStatus; readonly updateTime?: string; } export type V2ActivateLicenseRequestHeaders = {[key: string]: string}; export interface V2ActivateLicenseRequest { /** The license file to upload to activate the license */ content: Blob; fileName: string; headers: V2ActivateLicenseRequestHeaders; } export interface Tiupv2UpdateTiups { description?: string; name?: string; tagIds?: string[]; } export interface Tiupv2CreateTiups { description?: string; hostId?: string; name?: string; tagIds?: string[]; tiupHome?: string; } export interface Tagv2Tag { tagId?: string; tagKey?: string; tagValue: string; } export type RpcStatusError = { code?: number; details?: V2ErrorDetail[]; message?: string; status?: string; }; export interface RpcStatus { error?: RpcStatusError; } /** * `Any` contains an arbitrary serialized protocol buffer message along with a URL that describes the type of the serialized message. Protobuf library provides support to pack/unpack Any values in the form of utility functions or additional generated methods of the Any type. Example 1: Pack and unpack a message in C++. Foo foo = ...; Any any; any.PackFrom(foo); ... if (any.UnpackTo(&foo)) { ... } Example 2: Pack and unpack a message in Java. Foo foo = ...; Any any = Any.pack(foo); ... if (any.is(Foo.class)) { foo = any.unpack(Foo.class); } // or ... if (any.isSameTypeAs(Foo.getDefaultInstance())) { foo = any.unpack(Foo.getDefaultInstance()); } Example 3: Pack and unpack a message in Python. foo = Foo(...) any = Any() any.Pack(foo) ... if any.Is(Foo.DESCRIPTOR): any.Unpack(foo) ... Example 4: Pack and unpack a message in Go foo := &pb.Foo{...} any, err := anypb.New(foo) if err != nil { ... } ... foo := &pb.Foo{} if err := any.UnmarshalTo(foo); err != nil { ... } The pack methods provided by protobuf library will by default use 'type.googleapis.com/full.type.name' as the type URL and the unpack methods only use the fully qualified type name after the last '/' in the type URL, for example "foo.bar.com/x/y.z" will yield type name "y.z". JSON ==== The JSON representation of an `Any` value uses the regular representation of the deserialized, embedded message, with an additional field `@type` which contains the type URL. Example: package google.profile; message Person { string first_name = 1; string last_name = 2; } { "@type": "type.googleapis.com/google.profile.Person", "firstName": , "lastName": } If the embedded message type is well-known and has a custom JSON representation, that representation will be embedded adding a field `value` which holds the custom JSON in addition to the `@type` field. Example (for message [google.protobuf.Duration][]): { "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } */ export interface ProtobufAny { /** A URL/resource name that uniquely identifies the type of the serialized protocol buffer message. This string must contain at least one "/" character. The last segment of the URL's path must represent the fully qualified name of the type (as in `path/google.protobuf.Duration`). The name should be in a canonical form (e.g., leading "." is not accepted). In practice, teams usually precompile into the binary all types that they expect it to use in the context of Any. However, for URLs which use the scheme `http`, `https`, or no scheme, one can optionally set up a type server that maps type URLs to message definitions as follows: * If no scheme is provided, `https` is assumed. * An HTTP GET on the URL must yield a [google.protobuf.Type][] value in binary format, or produce an error. * Applications are allowed to cache lookup results based on the URL, or have them precompiled into a binary to avoid any lookup. Therefore, binary compatibility needs to be preserved on changes to types. (Use versioned type names to manage breaking changes.) Note: this functionality is not currently available in the official protobuf release, and it is not used for type URLs beginning with type.googleapis.com. As of May 2023, there are no widely used type server implementations and no plans to implement one. Schemes other than `http`, `https` (or the empty scheme) might be used with implementation specific semantics. */ '@type'?: string; [key: string]: unknown; } export interface Metricsv2Value { timestamp?: number; value?: string; } export interface Licensev2License { activateAt?: string; alerts?: string; allow?: string[]; customerCode?: string; deny?: string[]; deviceCode?: string; expirationAt?: string; hosts?: string; licenseId?: string; licenseType?: V2LicenseTypeEnumData; signature?: string; status?: V2LicenseStatusEnumData; vcpu?: string; version?: string; } export interface Hostv2UpdateHost { comment?: string; credentialId?: string; hostId?: string; locationId?: string; sshPort?: number; tagIds?: string[]; } export type Hostv2ReportCheckResult = typeof Hostv2ReportCheckResult[keyof typeof Hostv2ReportCheckResult]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const Hostv2ReportCheckResult = { passed: 'passed', failed: 'failed', warned: 'warned', } as const; export interface Hostv2Report { checkBody?: string; checkDesc?: string; checkId?: string; checkName?: string; checkOut?: string; checkResult?: Hostv2ReportCheckResult; fixable?: boolean; hostId?: string; optional?: boolean; reportId: string; } export interface UserServiceUpdateUserBody { /** The email address of the user. */ email?: string; /** Additional notes about the user. */ note?: string; /** The user's phone number. */ phone?: string; /** The roles assigned to the user. */ roles?: V2UserRole[]; /** The type of the user (e.g., admin, regular user). */ userType?: number; } export interface UserServiceResetPasswordBody { newPassword: string; } export interface TagServiceUpdateTagBody { tagKey?: string; tagValue: string; } export interface RoleServiceUpdateRoleBody { detail?: string; note?: string; roleName: string; roleType?: number; } export interface LocationServiceUpdateLocationsBody { location?: V2Locations; } export interface HostServiceHostConfirmBody { [key: string]: unknown } export interface GlobalBRServiceUpdateBackupPolicyBody { accessKeyId?: string; clusterIds?: string[]; clusters?: V2Cluster[]; concurrency?: number; cycle: V2CycleEnumData; destination: string; frequency: string; logBackup: boolean; logFile?: string; name: string; rateLimit?: number; retention: number; secretAccessKey?: string; time: string; } export interface DiagnosisServiceUpdateTopSqlConfigsBody { enable: boolean; historySize?: number; internalQuery?: boolean; maxSize?: number; refreshInterval?: number; } export interface DiagnosisServiceRemoveSqlLimitBody { id: string; watchText: string; } export type DiagnosisServiceAddSqlLimitBodyAction = typeof DiagnosisServiceAddSqlLimitBodyAction[keyof typeof DiagnosisServiceAddSqlLimitBodyAction]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const DiagnosisServiceAddSqlLimitBodyAction = { DRYRUN: 'DRYRUN', COOLDOWN: 'COOLDOWN', KILL: 'KILL', } as const; export interface DiagnosisServiceAddSqlLimitBody { action: DiagnosisServiceAddSqlLimitBodyAction; resourceGroup: string; watchText: string; } export interface CredentialServiceUpdateCredentialBody { credentialName?: string; credentialType: V2CredentialType; description?: string; forceUpdate?: boolean; hostCredential?: V2HostCredentialObject; tidbCredential?: V2TiDBCredentialObject; userName: string; validateType: V2CredentialValidateType; } export interface ClusterBRServiceCreateRestoreTaskBody { accessKeyId?: string; backupTaskId?: string; concurrency?: number; destination?: string; logFile?: string; rateLimit?: number; restoreTime?: string; secretAccessKey?: string; targetClusterId: string; type?: V2ClusterBRTypeEnumData; } export interface ClusterBRServiceCreateBackupTaskBody { accessKeyId?: string; concurrency?: number; destination: string; logFile?: string; name?: string; rateLimit?: number; retention?: number; secretAccessKey?: string; } export type ApiKeyServiceUpdateApiKeyBodyStatus = typeof ApiKeyServiceUpdateApiKeyBodyStatus[keyof typeof ApiKeyServiceUpdateApiKeyBodyStatus]; // eslint-disable-next-line @typescript-eslint/no-redeclare export const ApiKeyServiceUpdateApiKeyBodyStatus = { disable: 'disable', enable: 'enable', } as const; export interface ApiKeyServiceUpdateApiKeyBody { creator?: string; description?: string; secretKey?: string; status?: ApiKeyServiceUpdateApiKeyBodyStatus; } ================================================ FILE: ui-v2/packages/api/server/src/azores/index.ts ================================================ /** * Generated by orval v7.3.0 🍺 * Do not edit manually. * Azores Open API * OpenAPI spec version: 2.0.0 */ import { Hono } from 'hono' import { cors } from 'hono/cors' import { apiKeyServiceListApiKeysHandlers } from './handlers/apiKeyServiceListApiKeys'; import { apiKeyServiceCreateApiKeyHandlers } from './handlers/apiKeyServiceCreateApiKey'; import { apiKeyServiceGetApiKeyHandlers } from './handlers/apiKeyServiceGetApiKey'; import { apiKeyServiceDeleteApiKeyHandlers } from './handlers/apiKeyServiceDeleteApiKey'; import { apiKeyServiceUpdateApiKeyHandlers } from './handlers/apiKeyServiceUpdateApiKey'; import { apiKeyServiceResetSecretKeyHandlers } from './handlers/apiKeyServiceResetSecretKey'; import { globalBRServiceListBackupPoliciesHandlers } from './handlers/globalBRServiceListBackupPolicies'; import { globalBRServiceCreateBackupPolicyHandlers } from './handlers/globalBRServiceCreateBackupPolicy'; import { globalBRServicePreCheckBackupPolicyHandlers } from './handlers/globalBRServicePreCheckBackupPolicy'; import { globalBRServiceGetBackupPolicyHandlers } from './handlers/globalBRServiceGetBackupPolicy'; import { globalBRServiceDeleteBackupPolicyHandlers } from './handlers/globalBRServiceDeleteBackupPolicy'; import { globalBRServiceUpdateBackupPolicyHandlers } from './handlers/globalBRServiceUpdateBackupPolicy'; import { globalBRServiceGetBRSummaryHandlers } from './handlers/globalBRServiceGetBRSummary'; import { globalBRServiceListBRTasksHandlers } from './handlers/globalBRServiceListBRTasks'; import { globalBRServiceDeleteBRTaskHandlers } from './handlers/globalBRServiceDeleteBRTask'; import { globalBRServiceStartBRTaskHandlers } from './handlers/globalBRServiceStartBRTask'; import { globalBRServiceStopBRTaskHandlers } from './handlers/globalBRServiceStopBRTask'; import { clusterBRServiceCreateBackupTaskHandlers } from './handlers/clusterBRServiceCreateBackupTask'; import { clusterBRServiceGetClusterBackupPolicyHandlers } from './handlers/clusterBRServiceGetClusterBackupPolicy'; import { clusterBRServiceListClusterBackupRecordsHandlers } from './handlers/clusterBRServiceListClusterBackupRecords'; import { clusterBRServiceCreateRestoreTaskHandlers } from './handlers/clusterBRServiceCreateRestoreTask'; import { clusterBRServiceListClusterBRTasksHandlers } from './handlers/clusterBRServiceListClusterBRTasks'; import { clusterBRServiceDetectClusterHandlers } from './handlers/clusterBRServiceDetectCluster'; import { metricsServiceGetClusterMetricDataHandlers } from './handlers/metricsServiceGetClusterMetricData'; import { metricsServiceGetClusterMetricInstanceHandlers } from './handlers/metricsServiceGetClusterMetricInstance'; import { diagnosisServiceGetResourceGroupListHandlers } from './handlers/diagnosisServiceGetResourceGroupList'; import { clusterServiceGetProcessListHandlers } from './handlers/clusterServiceGetProcessList'; import { clusterServiceDeleteProcessHandlers } from './handlers/clusterServiceDeleteProcess'; import { diagnosisServiceGetSlowQueryListHandlers } from './handlers/diagnosisServiceGetSlowQueryList'; import { diagnosisServiceGetSlowQueryAvailableAdvancedFiltersHandlers } from './handlers/diagnosisServiceGetSlowQueryAvailableAdvancedFilters'; import { diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoHandlers } from './handlers/diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfo'; import { diagnosisServiceDownloadSlowQueryListHandlers } from './handlers/diagnosisServiceDownloadSlowQueryList'; import { diagnosisServiceGetSlowQueryAvailableFieldsHandlers } from './handlers/diagnosisServiceGetSlowQueryAvailableFields'; import { diagnosisServiceGetSlowQueryDetailHandlers } from './handlers/diagnosisServiceGetSlowQueryDetail'; import { diagnosisServiceAddSqlLimitHandlers } from './handlers/diagnosisServiceAddSqlLimit'; import { diagnosisServiceCheckSqlLimitSupportHandlers } from './handlers/diagnosisServiceCheckSqlLimitSupport'; import { diagnosisServiceRemoveSqlLimitHandlers } from './handlers/diagnosisServiceRemoveSqlLimit'; import { diagnosisServiceGetSqlLimitListHandlers } from './handlers/diagnosisServiceGetSqlLimitList'; import { diagnosisServiceGetSqlPlanListHandlers } from './handlers/diagnosisServiceGetSqlPlanList'; import { diagnosisServiceBindSqlPlanHandlers } from './handlers/diagnosisServiceBindSqlPlan'; import { diagnosisServiceCheckSqlPlanSupportHandlers } from './handlers/diagnosisServiceCheckSqlPlanSupport'; import { diagnosisServiceGetSqlPlanBindingListHandlers } from './handlers/diagnosisServiceGetSqlPlanBindingList'; import { diagnosisServiceUnbindSqlPlanHandlers } from './handlers/diagnosisServiceUnbindSqlPlan'; import { diagnosisServiceGetTopSqlListHandlers } from './handlers/diagnosisServiceGetTopSqlList'; import { diagnosisServiceGetTopSqlAvailableAdvancedFiltersHandlers } from './handlers/diagnosisServiceGetTopSqlAvailableAdvancedFilters'; import { diagnosisServiceGetTopSqlAvailableAdvancedFilterInfoHandlers } from './handlers/diagnosisServiceGetTopSqlAvailableAdvancedFilterInfo'; import { diagnosisServiceGetTopSqlConfigsHandlers } from './handlers/diagnosisServiceGetTopSqlConfigs'; import { diagnosisServiceUpdateTopSqlConfigsHandlers } from './handlers/diagnosisServiceUpdateTopSqlConfigs'; import { diagnosisServiceGetTopSqlAvailableFieldsHandlers } from './handlers/diagnosisServiceGetTopSqlAvailableFields'; import { diagnosisServiceGetTopSqlDetailHandlers } from './handlers/diagnosisServiceGetTopSqlDetail'; import { credentialServiceListCredentialsHandlers } from './handlers/credentialServiceListCredentials'; import { credentialServiceCreateCredentialHandlers } from './handlers/credentialServiceCreateCredential'; import { credentialServiceGetCredentialHandlers } from './handlers/credentialServiceGetCredential'; import { credentialServiceDeleteCredentialHandlers } from './handlers/credentialServiceDeleteCredential'; import { credentialServiceUpdateCredentialHandlers } from './handlers/credentialServiceUpdateCredential'; import { credentialServiceDownloadRSAKeyHandlers } from './handlers/credentialServiceDownloadRSAKey'; import { credentialServiceGenerateRSAKeyHandlers } from './handlers/credentialServiceGenerateRSAKey'; import { credentialServiceValidateConnectionHandlers } from './handlers/credentialServiceValidateConnection'; import { hostServiceListHostsHandlers } from './handlers/hostServiceListHosts'; import { hostServiceCreateHostsHandlers } from './handlers/hostServiceCreateHosts'; import { hostServiceImportHandlers } from './handlers/hostServiceImport'; import { hostServiceImportTaskHandlers } from './handlers/hostServiceImportTask'; import { hostServiceHostConfirmHandlers } from './handlers/hostServiceHostConfirm'; import { hostServiceGetHostHandlers } from './handlers/hostServiceGetHost'; import { hostServiceDeleteHandlers } from './handlers/hostServiceDelete'; import { hostServiceUpdateHostHandlers } from './handlers/hostServiceUpdateHost'; import { hostServiceGetDisksHandlers } from './handlers/hostServiceGetDisks'; import { metricsServiceGetHostMetricDataHandlers } from './handlers/metricsServiceGetHostMetricData'; import { hostServiceReportHandlers } from './handlers/hostServiceReport'; import { hostServiceGetTiDBProcessesHandlers } from './handlers/hostServiceGetTiDBProcesses'; import { hostServiceFixHandlers } from './handlers/hostServiceFix'; import { hostServiceCheckHandlers } from './handlers/hostServiceCheck'; import { hostServiceBatchDeleteHandlers } from './handlers/hostServiceBatchDelete'; import { hostServiceDownloadListHostsHandlers } from './handlers/hostServiceDownloadListHosts'; import { hostServiceDownloadHostTemplateHandlers } from './handlers/hostServiceDownloadHostTemplate'; import { licenseServiceGetLicenseHandlers } from './handlers/licenseServiceGetLicense'; import { licenseServiceGetDeviceCodeHandlers } from './handlers/licenseServiceGetDeviceCode'; import { licenseServiceActivateLicenseHandlers } from './handlers/licenseServiceActivateLicense'; import { licenseServiceActivateFreeLicenseHandlers } from './handlers/licenseServiceActivateFreeLicense'; import { locationServiceListLocationsHandlers } from './handlers/locationServiceListLocations'; import { locationServiceCreateLocationsHandlers } from './handlers/locationServiceCreateLocations'; import { locationServiceGetLocationsHandlers } from './handlers/locationServiceGetLocations'; import { locationServiceDeleteLocationHandlers } from './handlers/locationServiceDeleteLocation'; import { locationServiceUpdateLocationsHandlers } from './handlers/locationServiceUpdateLocations'; import { userServiceLoginHandlers } from './handlers/userServiceLogin'; import { userServiceLogoutHandlers } from './handlers/userServiceLogout'; import { metricsServiceGetMetricsHandlers } from './handlers/metricsServiceGetMetrics'; import { metricsServiceGetTopMetricConfigHandlers } from './handlers/metricsServiceGetTopMetricConfig'; import { metricsServiceGetTopMetricDataHandlers } from './handlers/metricsServiceGetTopMetricData'; import { metricsServiceGetOverviewStatusHandlers } from './handlers/metricsServiceGetOverviewStatus'; import { roleServiceListRolesHandlers } from './handlers/roleServiceListRoles'; import { roleServiceCreateRoleHandlers } from './handlers/roleServiceCreateRole'; import { roleServiceDeleteRoleHandlers } from './handlers/roleServiceDeleteRole'; import { roleServiceUpdateRoleHandlers } from './handlers/roleServiceUpdateRole'; import { tagServiceListTagsHandlers } from './handlers/tagServiceListTags'; import { tagServiceCreateTagHandlers } from './handlers/tagServiceCreateTag'; import { tagServiceGetTagHandlers } from './handlers/tagServiceGetTag'; import { tagServiceDeleteTagHandlers } from './handlers/tagServiceDeleteTag'; import { tagServiceUpdateTagHandlers } from './handlers/tagServiceUpdateTag'; import { tagServiceGetTagWithBindingsHandlers } from './handlers/tagServiceGetTagWithBindings'; import { tagServiceBatchCreateTagsHandlers } from './handlers/tagServiceBatchCreateTags'; import { tagServiceBindResourceHandlers } from './handlers/tagServiceBindResource'; import { tagServiceBindTagHandlers } from './handlers/tagServiceBindTag'; import { tagServiceListTagsByResourceTypeHandlers } from './handlers/tagServiceListTagsByResourceType'; import { tagServiceListTagKeysHandlers } from './handlers/tagServiceListTagKeys'; import { tagServiceListTagsWithBindingsHandlers } from './handlers/tagServiceListTagsWithBindings'; import { tiupsServiceListTiupsHandlers } from './handlers/tiupsServiceListTiups'; import { tiupsServiceCreateTiupsHandlers } from './handlers/tiupsServiceCreateTiups'; import { tiupsServiceGetTiupsHandlers } from './handlers/tiupsServiceGetTiups'; import { tiupsServiceDeleteTiupsHandlers } from './handlers/tiupsServiceDeleteTiups'; import { tiupsServiceUpdateTiupsHandlers } from './handlers/tiupsServiceUpdateTiups'; import { tiupsServiceGetTiupsClusterHandlers } from './handlers/tiupsServiceGetTiupsCluster'; import { userServiceListUsersHandlers } from './handlers/userServiceListUsers'; import { userServiceCreateUserHandlers } from './handlers/userServiceCreateUser'; import { userServiceGetUserProfileHandlers } from './handlers/userServiceGetUserProfile'; import { userServiceGetUserHandlers } from './handlers/userServiceGetUser'; import { userServiceDeleteUserHandlers } from './handlers/userServiceDeleteUser'; import { userServiceUpdateUserHandlers } from './handlers/userServiceUpdateUser'; import { userServiceResetPasswordHandlers } from './handlers/userServiceResetPassword'; import { userServiceChangePasswordHandlers } from './handlers/userServiceChangePassword'; import { userServiceValidateSessionHandlers } from './handlers/userServiceValidateSession'; import { apiKeyServiceGetTemErrorDetailHandlers } from './handlers/apiKeyServiceGetTemErrorDetail'; const app = new Hono() app.use('/api/v2/*', cors()) /** * @summary ListApiKeys retrieves a list of API keys. */ app.get('/api/v2/apiKeys',...apiKeyServiceListApiKeysHandlers) /** * @summary CreateApiKey creates a new API key. */ app.post('/api/v2/apiKeys',...apiKeyServiceCreateApiKeyHandlers) /** * @summary GetApiKeyRequest get an API key by its access key. */ app.get('/api/v2/apiKeys/:accessKey',...apiKeyServiceGetApiKeyHandlers) /** * @summary DeleteApiKey deletes an API key by its access key. */ app.delete('/api/v2/apiKeys/:accessKey',...apiKeyServiceDeleteApiKeyHandlers) /** * @summary UpdateApiKey updates an API key by its access key. */ app.patch('/api/v2/apiKeys/:accessKey',...apiKeyServiceUpdateApiKeyHandlers) /** * @summary ResetSecretKey resets the secret key for an existing API key. */ app.patch('/api/v2/apiKeys/:accessKey:resetSecretKey',...apiKeyServiceResetSecretKeyHandlers) /** * @summary ListBackupPolicies lists Backup policies */ app.get('/api/v2/backup/policies',...globalBRServiceListBackupPoliciesHandlers) /** * @summary CreateBackupPolicy creates a Backup policy */ app.post('/api/v2/backup/policies',...globalBRServiceCreateBackupPolicyHandlers) /** * @summary PreCheckBackupPolicy pre-checks a Backup policy */ app.post('/api/v2/backup/policies/precheck',...globalBRServicePreCheckBackupPolicyHandlers) /** * @summary GetBackupPolicy gets a Backup policy */ app.get('/api/v2/backup/policies/:policyId',...globalBRServiceGetBackupPolicyHandlers) /** * @summary DeleteBackupPolicy deletes a Backup policy */ app.delete('/api/v2/backup/policies/:policyId',...globalBRServiceDeleteBackupPolicyHandlers) /** * @summary UpdateBackupPolicy updates a Backup policy */ app.put('/api/v2/backup/policies/:policyId',...globalBRServiceUpdateBackupPolicyHandlers) /** * @summary GetBRSummary retrieves the summary of BR */ app.get('/api/v2/backup/summary',...globalBRServiceGetBRSummaryHandlers) /** * @summary ListBRTasks retrieves the tasks of BR */ app.get('/api/v2/backup/tasks',...globalBRServiceListBRTasksHandlers) /** * @summary DeleteBRTask deletes a BR task */ app.delete('/api/v2/backup/tasks/:taskId',...globalBRServiceDeleteBRTaskHandlers) /** * @summary StartBRTask starts a BR task */ app.post('/api/v2/backup/tasks/:taskId/start',...globalBRServiceStartBRTaskHandlers) /** * @summary StopBRTask stops a BR task */ app.post('/api/v2/backup/tasks/:taskId/stop',...globalBRServiceStopBRTaskHandlers) /** * @summary CreateBackupTask backups a cluster */ app.post('/api/v2/clusters/:clusterId/backup',...clusterBRServiceCreateBackupTaskHandlers) /** * @summary GetClusterBackupPolicy gets the backup info of a specific cluster */ app.get('/api/v2/clusters/:clusterId/backup/policy',...clusterBRServiceGetClusterBackupPolicyHandlers) /** * @summary ListBackupRecords lists the valid full backup records of a specific cluster */ app.get('/api/v2/clusters/:clusterId/backup/records',...clusterBRServiceListClusterBackupRecordsHandlers) /** * @summary CreateRestoreTask restores a cluster */ app.post('/api/v2/clusters/:clusterId/backup/restore',...clusterBRServiceCreateRestoreTaskHandlers) /** * @summary ListClusterBRTasks lists the backup tasks of a specific cluster */ app.get('/api/v2/clusters/:clusterId/backup/tasks',...clusterBRServiceListClusterBRTasksHandlers) /** * @summary DetectCluster detects the if the cluster exist */ app.post('/api/v2/clusters/:clusterId/backup:detect',...clusterBRServiceDetectClusterHandlers) /** * @summary Get cluster metric data */ app.get('/api/v2/clusters/:clusterId/metrics/:name/data',...metricsServiceGetClusterMetricDataHandlers) /** * @summary Get metric instances */ app.get('/api/v2/clusters/:clusterId/metrics/:name/instance',...metricsServiceGetClusterMetricInstanceHandlers) /** * @summary Get resource group list */ app.get('/api/v2/clusters/:clusterId/resourcegroups',...diagnosisServiceGetResourceGroupListHandlers) /** * @summary GetProcessList retrieves the list of running processes in a cluster */ app.get('/api/v2/clusters/:clusterId/sessions',...clusterServiceGetProcessListHandlers) /** * @summary DeleteProcess terminates a specific process in the cluster */ app.delete('/api/v2/clusters/:clusterId/sessions/:sessionId',...clusterServiceDeleteProcessHandlers) /** * @summary GetSlowQueryList retrieves the list of slow queries */ app.get('/api/v2/clusters/:clusterId/slowqueries',...diagnosisServiceGetSlowQueryListHandlers) /** * @summary GetSlowQueryAvailableAdvancedFilters retrieves the list of available advanced filters */ app.get('/api/v2/clusters/:clusterId/slowqueries/advancedFilters',...diagnosisServiceGetSlowQueryAvailableAdvancedFiltersHandlers) /** * @summary GetSlowQueryAvailableAdvancedFilterInfo retrieves the list of available advanced filter info */ app.get('/api/v2/clusters/:clusterId/slowqueries/advancedFilters/:filterName',...diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoHandlers) /** * @summary DownloadSlowQueryList downloads the list of slow queries */ app.get('/api/v2/clusters/:clusterId/slowqueries/download',...diagnosisServiceDownloadSlowQueryListHandlers) /** * @summary GetSlowQueryAvailableFields retrieves the list of available fields for slow queries */ app.get('/api/v2/clusters/:clusterId/slowqueries/fields',...diagnosisServiceGetSlowQueryAvailableFieldsHandlers) /** * @summary GetSlowQueryDetail retrieves the details of a specific slow query */ app.get('/api/v2/clusters/:clusterId/slowqueries/:digest',...diagnosisServiceGetSlowQueryDetailHandlers) /** * @summary Create SQL limit */ app.post('/api/v2/clusters/:clusterId/sqllimits:addSqlLimit',...diagnosisServiceAddSqlLimitHandlers) /** * @summary Check if SQL limit is supported */ app.get('/api/v2/clusters/:clusterId/sqllimits:checkSupport',...diagnosisServiceCheckSqlLimitSupportHandlers) /** * @summary Remove SQL limit */ app.post('/api/v2/clusters/:clusterId/sqllimits:removeSqlLimit',...diagnosisServiceRemoveSqlLimitHandlers) /** * @summary Query SQL limit */ app.get('/api/v2/clusters/:clusterId/sqllimits:showSqlLimit',...diagnosisServiceGetSqlLimitListHandlers) /** * @summary GetSqlPlanList retrieves the list of plans */ app.get('/api/v2/clusters/:clusterId/sqlplans',...diagnosisServiceGetSqlPlanListHandlers) /** * @summary BindSqlPlan binds a plan to a specific sql */ app.post('/api/v2/clusters/:clusterId/sqlplans/:planDigest:bindSqlPlan',...diagnosisServiceBindSqlPlanHandlers) /** * @summary CheckSupport returns whether sql plan binding is supported */ app.get('/api/v2/clusters/:clusterId/sqlplans:checkSupport',...diagnosisServiceCheckSqlPlanSupportHandlers) /** * @summary GetSQLBindInfo */ app.get('/api/v2/clusters/:clusterId/sqlplans:showSqlPlanBinding',...diagnosisServiceGetSqlPlanBindingListHandlers) /** * @summary UnbindSqlPlan unbinds a plan from a specific sql */ app.post('/api/v2/clusters/:clusterId/sqlplans:unbindSqlPlan',...diagnosisServiceUnbindSqlPlanHandlers) /** * @summary GetTopSqlList retrieves the list of top sql */ app.get('/api/v2/clusters/:clusterId/topsqls',...diagnosisServiceGetTopSqlListHandlers) /** * @summary GetTopSqlAvailableAdvancedFilters retrieves the list of available advanced filters */ app.get('/api/v2/clusters/:clusterId/topsqls/advancedFilters',...diagnosisServiceGetTopSqlAvailableAdvancedFiltersHandlers) /** * @summary GetTopSqlAvailableAdvancedFilterInfo retrieves the list of available advanced filter info */ app.get('/api/v2/clusters/:clusterId/topsqls/advancedFilters/:filterName',...diagnosisServiceGetTopSqlAvailableAdvancedFilterInfoHandlers) /** * @summary GetTopSqlConfigs retrieves the list of top sql configs */ app.get('/api/v2/clusters/:clusterId/topsqls/configs',...diagnosisServiceGetTopSqlConfigsHandlers) /** * @summary UpdateTopSqlConfigs updates the list of top sql configs */ app.patch('/api/v2/clusters/:clusterId/topsqls/configs',...diagnosisServiceUpdateTopSqlConfigsHandlers) /** * @summary GetTopSqlAvailableFields retrieves the list of available fields for top sqls */ app.get('/api/v2/clusters/:clusterId/topsqls/fields',...diagnosisServiceGetTopSqlAvailableFieldsHandlers) /** * @summary GetTopSqlDetail retrieves the details of a specific top sql */ app.get('/api/v2/clusters/:clusterId/topsqls/:digest',...diagnosisServiceGetTopSqlDetailHandlers) /** * @summary List credentials */ app.get('/api/v2/credentials',...credentialServiceListCredentialsHandlers) /** * @summary Create credential */ app.post('/api/v2/credentials',...credentialServiceCreateCredentialHandlers) /** * @summary Get credential */ app.get('/api/v2/credentials/:credentialId',...credentialServiceGetCredentialHandlers) /** * @summary Delete credential by credential id */ app.delete('/api/v2/credentials/:credentialId',...credentialServiceDeleteCredentialHandlers) /** * @summary Update credential by credential id */ app.patch('/api/v2/credentials/:credentialId',...credentialServiceUpdateCredentialHandlers) /** * @summary Download credential public key and private key */ app.get('/api/v2/credentials/:credentialId:downloadRsaKey',...credentialServiceDownloadRSAKeyHandlers) /** * @summary Generate credential public key and private key */ app.post('/api/v2/credentials:generateRsaKey',...credentialServiceGenerateRSAKeyHandlers) /** * @summary Validate credential is accessible */ app.post('/api/v2/credentials:validateConnection',...credentialServiceValidateConnectionHandlers) /** * @summary ListHosts */ app.get('/api/v2/hosts',...hostServiceListHostsHandlers) /** * @summary CreateHosts */ app.post('/api/v2/hosts',...hostServiceCreateHostsHandlers) /** * Upload a csv form data to host. * @summary import host */ app.post('/api/v2/hosts/import/tasks',...hostServiceImportHandlers) /** * @summary Import one host */ app.get('/api/v2/hosts/import/tasks/:taskId',...hostServiceImportTaskHandlers) /** * @summary HostConfirm one host */ app.post('/api/v2/hosts/import/tasks/:taskId:confirm',...hostServiceHostConfirmHandlers) /** * @summary Get */ app.get('/api/v2/hosts/:hostId',...hostServiceGetHostHandlers) /** * @summary delete one host by host_id */ app.delete('/api/v2/hosts/:hostId',...hostServiceDeleteHandlers) /** * @summary update one host by host_id */ app.patch('/api/v2/hosts/:hostId',...hostServiceUpdateHostHandlers) /** * @summary GetDisks */ app.get('/api/v2/hosts/:hostId/disks',...hostServiceGetDisksHandlers) /** * @summary Get host metric data */ app.get('/api/v2/hosts/:hostId/metrics/:name/data',...metricsServiceGetHostMetricDataHandlers) /** * @summary Report */ app.get('/api/v2/hosts/:hostId/report/:reportId',...hostServiceReportHandlers) /** * @summary GetInstances */ app.get('/api/v2/hosts/:hostId/tidbProcesses',...hostServiceGetTiDBProcessesHandlers) /** * @summary Fix */ app.post('/api/v2/hosts/:hostId:fix',...hostServiceFixHandlers) /** * @summary Check */ app.post('/api/v2/hosts/:hostId:systemCheck',...hostServiceCheckHandlers) /** * @summary delete one host by host_id */ app.post('/api/v2/hosts:batchDelete',...hostServiceBatchDeleteHandlers) /** * @summary HostConfirm one host */ app.get('/api/v2/hosts:download',...hostServiceDownloadListHostsHandlers) /** * @summary DownloadHostTemplate one host */ app.get('/api/v2/hosts:downloadHostTemplate',...hostServiceDownloadHostTemplateHandlers) /** * @summary GetLicense returns the license details */ app.get('/api/v2/license',...licenseServiceGetLicenseHandlers) /** * @summary GetDeviceCode returns the device code to help activate the license */ app.get('/api/v2/license/devicecode',...licenseServiceGetDeviceCodeHandlers) /** * Upload a license using form data to activate. * @summary Activate a license */ app.post('/api/v2/license:activate',...licenseServiceActivateLicenseHandlers) /** * @summary ActivateFreeLicense activate the embedded free license */ app.post('/api/v2/license:trial',...licenseServiceActivateFreeLicenseHandlers) /** * @summary list location */ app.get('/api/v2/locations',...locationServiceListLocationsHandlers) /** * @summary create CreateLocationRequest */ app.post('/api/v2/locations',...locationServiceCreateLocationsHandlers) /** * @summary get Location */ app.get('/api/v2/locations/:locationId',...locationServiceGetLocationsHandlers) /** * @summary delete Location by Location id */ app.delete('/api/v2/locations/:locationId',...locationServiceDeleteLocationHandlers) /** * @summary update Location basic info by Location id */ app.patch('/api/v2/locations/:locationId',...locationServiceUpdateLocationsHandlers) /** * @summary Login allows a user to log in and start a session. */ app.post('/api/v2/login',...userServiceLoginHandlers) /** * @summary Logout allows a user to log out and end their session. */ app.post('/api/v2/logout',...userServiceLogoutHandlers) /** * @summary Get metrics info */ app.get('/api/v2/metrics',...metricsServiceGetMetricsHandlers) /** * @summary Get top metric config */ app.get('/api/v2/overview/metrics/config',...metricsServiceGetTopMetricConfigHandlers) /** * @summary Get top metric data */ app.get('/api/v2/overview/metrics/:name/data',...metricsServiceGetTopMetricDataHandlers) /** * @summary Get overview status */ app.get('/api/v2/overview/status',...metricsServiceGetOverviewStatusHandlers) /** * @summary ListRoles retrieves a list of roles. */ app.get('/api/v2/roles',...roleServiceListRolesHandlers) /** * @summary CreateRole creates a new role. */ app.post('/api/v2/roles',...roleServiceCreateRoleHandlers) /** * @summary DeleteRole deletes a role by role ID. */ app.delete('/api/v2/roles/:roleId',...roleServiceDeleteRoleHandlers) /** * @summary UpdateRole updates a role by role ID. */ app.patch('/api/v2/roles/:roleId',...roleServiceUpdateRoleHandlers) /** * @summary List tags */ app.get('/api/v2/tags',...tagServiceListTagsHandlers) /** * @summary Create tag */ app.post('/api/v2/tags',...tagServiceCreateTagHandlers) /** * @summary Get tag */ app.get('/api/v2/tags/:tagId',...tagServiceGetTagHandlers) /** * @summary Delete tag by tag id */ app.delete('/api/v2/tags/:tagId',...tagServiceDeleteTagHandlers) /** * @summary Update tag basic info by tag id */ app.patch('/api/v2/tags/:tagId',...tagServiceUpdateTagHandlers) /** * @summary Get tag with bindings */ app.get('/api/v2/tags/:tagId:getWithBindings',...tagServiceGetTagWithBindingsHandlers) /** * @summary Batch create tags */ app.post('/api/v2/tags:batchCreate',...tagServiceBatchCreateTagsHandlers) /** * @summary Modify bind object by resource id */ app.post('/api/v2/tags:bindResource',...tagServiceBindResourceHandlers) /** * @summary Modify bind object by tag id */ app.post('/api/v2/tags:bindTag',...tagServiceBindTagHandlers) /** * @summary List tags by resource type */ app.get('/api/v2/tags:listByResourceType',...tagServiceListTagsByResourceTypeHandlers) /** * @summary List tag keys */ app.get('/api/v2/tags:listKeys',...tagServiceListTagKeysHandlers) /** * @summary List tags with bindings */ app.get('/api/v2/tags:listWithBindings',...tagServiceListTagsWithBindingsHandlers) /** * @summary list Tiups */ app.get('/api/v2/tiups',...tiupsServiceListTiupsHandlers) /** * @summary create Tiups */ app.post('/api/v2/tiups',...tiupsServiceCreateTiupsHandlers) /** * @summary get Tiups */ app.get('/api/v2/tiups/:tiupId',...tiupsServiceGetTiupsHandlers) /** * @summary delete Tiups by Tiups id */ app.delete('/api/v2/tiups/:tiupId',...tiupsServiceDeleteTiupsHandlers) /** * @summary update Tiups basic info by Tiups id */ app.patch('/api/v2/tiups/:tiupId',...tiupsServiceUpdateTiupsHandlers) /** * @summary Get TiupsCluster */ app.get('/api/v2/tiups/:tiupId/clusters',...tiupsServiceGetTiupsClusterHandlers) /** * @summary ListUsers retrieves a list of users. */ app.get('/api/v2/users',...userServiceListUsersHandlers) /** * @summary CreateUser creates a new user. */ app.post('/api/v2/users',...userServiceCreateUserHandlers) /** * @summary GetUserProfile retrieves the profile information of the authenticated user. */ app.get('/api/v2/users/profile',...userServiceGetUserProfileHandlers) /** * @summary GetUser retrieves a user by user ID. */ app.get('/api/v2/users/:userId',...userServiceGetUserHandlers) /** * @summary DeleteUser deletes a user by user ID. */ app.delete('/api/v2/users/:userId',...userServiceDeleteUserHandlers) /** * @summary UpdateUser updates a user's information by user ID. */ app.patch('/api/v2/users/:userId',...userServiceUpdateUserHandlers) /** * @summary ResetPassword allows an admin user to reset the password of another user. */ app.patch('/api/v2/users/:userId:resetPassword',...userServiceResetPasswordHandlers) /** * @summary ChangePassword allows the authenticated user to change their password. */ app.patch('/api/v2/users:changePassword',...userServiceChangePasswordHandlers) /** * @summary ValidateSession verifies the validity of the current session. */ app.get('/api/v2/users:validateSession',...userServiceValidateSessionHandlers) /** * @summary GetTemErrorDetail */ app.get('/documentation/errorDetail',...apiKeyServiceGetTemErrorDetailHandlers) export default app ================================================ FILE: ui-v2/packages/api/server/src/azores/index.validator.ts ================================================ /** * Generated by orval v7.3.0 🍺 * Do not edit manually. * Azores Open API * OpenAPI spec version: 2.0.0 */ // based on https://github.com/honojs/middleware/blob/main/packages/zod-validator/src/index.ts import type { z, ZodSchema, ZodError } from 'zod'; import { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets, } from 'hono'; type HasUndefined = undefined extends T ? true : false; type Hook = ( result: | { success: true; data: T } | { success: false; error: ZodError; data: T }, c: Context, ) => | Response | Promise | void | Promise | TypedResponse; import { zValidator as zValidatorBase } from '@hono/zod-validator'; type ValidationTargetsWithResponse = ValidationTargets & { response: any }; export const zValidator = < T extends ZodSchema, Target extends keyof ValidationTargetsWithResponse, E extends Env, P extends string, In = z.input, Out = z.output, I extends Input = { in: HasUndefined extends true ? { [K in Target]?: K extends 'json' ? In : HasUndefined< keyof ValidationTargetsWithResponse[K] > extends true ? { [K2 in keyof In]?: ValidationTargetsWithResponse[K][K2] } : { [K2 in keyof In]: ValidationTargetsWithResponse[K][K2] }; } : { [K in Target]: K extends 'json' ? In : HasUndefined< keyof ValidationTargetsWithResponse[K] > extends true ? { [K2 in keyof In]?: ValidationTargetsWithResponse[K][K2] } : { [K2 in keyof In]: ValidationTargetsWithResponse[K][K2] }; }; out: { [K in Target]: Out }; }, V extends I = I, >( target: Target, schema: T, hook?: Hook, E, P>, ): MiddlewareHandler => async (c, next) => { if (target !== 'response') { const value = await zValidatorBase< T, keyof ValidationTargets, E, P, In, Out, I, V >( target, schema, hook, )(c, next); if (value instanceof Response) { return value; } } else { await next(); if ( c.res.status !== 200 || !c.res.headers.get('Content-Type')?.includes('application/json') ) { return; } let value: unknown; try { value = await c.res.json(); } catch { const message = 'Malformed JSON in response'; c.res = new Response(message, { status: 400 }); return; } const result = await schema.safeParseAsync(value); if (hook) { const hookResult = hook({ data: value, ...result }, c); if (hookResult) { if (hookResult instanceof Response || hookResult instanceof Promise) { const hookResponse = await hookResult; if (hookResponse instanceof Response) { c.res = new Response(hookResponse.body, hookResponse); } } if ( 'response' in hookResult && hookResult.response instanceof Response ) { c.res = new Response(hookResult.response.body, hookResult.response); } } } if (!result.success) { c.res = new Response(JSON.stringify(result), { status: 400, headers: { 'Content-Type': 'application/json', }, }); } else { c.res = new Response(JSON.stringify(result.data), c.res); } } }; ================================================ FILE: ui-v2/packages/api/server/src/azores/index.zod.ts ================================================ /** * Generated by orval v7.3.0 🍺 * Do not edit manually. * Azores Open API * OpenAPI spec version: 2.0.0 */ import { z as zod } from 'zod'; export const apiKeyServiceListApiKeysQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "accessKey": zod.string().optional(), "creator": zod.string().optional(), "status": zod.string().optional() }) export const apiKeyServiceListApiKeysResponse = zod.object({ "apikeys": zod.array(zod.object({ "accessKey": zod.string(), "secretKey": zod.string().optional(), "creator": zod.string().optional(), "status": zod.enum(['disable', 'enable']).optional(), "description": zod.string().optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const apiKeyServiceCreateApiKeyBody = zod.object({ "description": zod.string() }) export const apiKeyServiceCreateApiKeyResponse = zod.object({ "accessKey": zod.string(), "secretKey": zod.string().optional(), "creator": zod.string().optional(), "status": zod.enum(['disable', 'enable']).optional(), "description": zod.string().optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() }) export const apiKeyServiceGetApiKeyParams = zod.object({ "accessKey": zod.string() }) export const apiKeyServiceGetApiKeyResponse = zod.object({ "accessKey": zod.string(), "secretKey": zod.string().optional(), "creator": zod.string().optional(), "status": zod.enum(['disable', 'enable']).optional(), "description": zod.string().optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() }) export const apiKeyServiceDeleteApiKeyParams = zod.object({ "accessKey": zod.string() }) export const apiKeyServiceDeleteApiKeyResponse = zod.object({ }) export const apiKeyServiceUpdateApiKeyParams = zod.object({ "accessKey": zod.string() }) export const apiKeyServiceUpdateApiKeyBody = zod.object({ "secretKey": zod.string().optional(), "creator": zod.string().optional(), "status": zod.enum(['disable', 'enable']).optional(), "description": zod.string().optional() }) export const apiKeyServiceUpdateApiKeyResponse = zod.object({ "accessKey": zod.string(), "secretKey": zod.string().optional(), "creator": zod.string().optional(), "status": zod.enum(['disable', 'enable']).optional(), "description": zod.string().optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() }) export const apiKeyServiceResetSecretKeyParams = zod.object({ "accessKey": zod.string() }) export const apiKeyServiceResetSecretKeyResponse = zod.object({ "accessKey": zod.string(), "secretKey": zod.string() }) export const globalBRServiceListBackupPoliciesQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional() }) export const globalBRServiceListBackupPoliciesResponse = zod.object({ "backupPolicies": zod.array(zod.object({ "policyId": zod.string().optional(), "name": zod.string(), "logBackup": zod.boolean(), "destination": zod.string(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "cycle": zod.enum(['week', 'month']).optional(), "frequency": zod.string(), "time": zod.string(), "retention": zod.number(), "clusters": zod.array(zod.object({ "id": zod.string().optional(), "name": zod.string().optional() })).optional(), "clusterIds": zod.array(zod.string()).optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const globalBRServiceCreateBackupPolicyBody = zod.object({ "policyId": zod.string().optional(), "name": zod.string(), "logBackup": zod.boolean(), "destination": zod.string(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "cycle": zod.enum(['week', 'month']).optional(), "frequency": zod.string(), "time": zod.string(), "retention": zod.number(), "clusters": zod.array(zod.object({ "id": zod.string().optional(), "name": zod.string().optional() })).optional(), "clusterIds": zod.array(zod.string()).optional() }) export const globalBRServiceCreateBackupPolicyResponse = zod.object({ "policyId": zod.string().optional(), "name": zod.string(), "logBackup": zod.boolean(), "destination": zod.string(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "cycle": zod.enum(['week', 'month']).optional(), "frequency": zod.string(), "time": zod.string(), "retention": zod.number(), "clusters": zod.array(zod.object({ "id": zod.string().optional(), "name": zod.string().optional() })).optional(), "clusterIds": zod.array(zod.string()).optional() }) export const globalBRServicePreCheckBackupPolicyBody = zod.object({ "policyId": zod.string().optional(), "name": zod.string(), "logBackup": zod.boolean(), "destination": zod.string(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "cycle": zod.enum(['week', 'month']).optional(), "frequency": zod.string(), "time": zod.string(), "retention": zod.number(), "clusters": zod.array(zod.object({ "id": zod.string().optional(), "name": zod.string().optional() })).optional(), "clusterIds": zod.array(zod.string()).optional() }) export const globalBRServicePreCheckBackupPolicyResponse = zod.object({ "clusters": zod.array(zod.object({ "id": zod.string().optional(), "name": zod.string().optional() })).optional() }) export const globalBRServiceGetBackupPolicyParams = zod.object({ "policyId": zod.string() }) export const globalBRServiceGetBackupPolicyResponse = zod.object({ "policyId": zod.string().optional(), "name": zod.string(), "logBackup": zod.boolean(), "destination": zod.string(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "cycle": zod.enum(['week', 'month']).optional(), "frequency": zod.string(), "time": zod.string(), "retention": zod.number(), "clusters": zod.array(zod.object({ "id": zod.string().optional(), "name": zod.string().optional() })).optional(), "clusterIds": zod.array(zod.string()).optional() }) export const globalBRServiceDeleteBackupPolicyParams = zod.object({ "policyId": zod.string() }) export const globalBRServiceDeleteBackupPolicyResponse = zod.object({ }) export const globalBRServiceUpdateBackupPolicyParams = zod.object({ "policyId": zod.string() }) export const globalBRServiceUpdateBackupPolicyBody = zod.object({ "name": zod.string(), "logBackup": zod.boolean(), "destination": zod.string(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "cycle": zod.enum(['week', 'month']).optional(), "frequency": zod.string(), "time": zod.string(), "retention": zod.number(), "clusters": zod.array(zod.object({ "id": zod.string().optional(), "name": zod.string().optional() })).optional(), "clusterIds": zod.array(zod.string()).optional() }) export const globalBRServiceUpdateBackupPolicyResponse = zod.object({ "policyId": zod.string().optional(), "name": zod.string(), "logBackup": zod.boolean(), "destination": zod.string(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "cycle": zod.enum(['week', 'month']).optional(), "frequency": zod.string(), "time": zod.string(), "retention": zod.number(), "clusters": zod.array(zod.object({ "id": zod.string().optional(), "name": zod.string().optional() })).optional(), "clusterIds": zod.array(zod.string()).optional() }) export const globalBRServiceGetBRSummaryQueryParams = zod.object({ "top": zod.number().optional() }) export const globalBRServiceGetBRSummaryResponse = zod.object({ "topClustersWithBrSize": zod.array(zod.object({ "clusterId": zod.string().optional(), "clusterName": zod.string().optional(), "totalSizeByte": zod.string().optional(), "totalSize": zod.string().optional() })).optional(), "topClustersWithBrAlert": zod.array(zod.object({ "clusterId": zod.string().optional(), "clusterName": zod.string().optional(), "alertCount": zod.string().optional() })).optional(), "topClustersWithoutBrPolicy": zod.array(zod.object({ "clusterId": zod.string().optional(), "clusterName": zod.string().optional(), "lastBackupTime": zod.string().optional(), "sizeByte": zod.string().optional() })).optional() }) export const globalBRServiceListBRTasksQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "brTaskId": zod.string().optional(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional(), "type": zod.enum(['all', 'full_backup', 'log_backup', 'restore_by_file', 'restore_by_time', 'all_backup', 'all_restore']).optional(), "status": zod.enum(['all', 'running', 'finished', 'abnormal', 'stopped']).optional() }) export const globalBRServiceListBRTasksResponse = zod.object({ "brTasks": zod.array(zod.object({ "taskId": zod.string().optional(), "type": zod.enum(['all', 'full_backup', 'log_backup', 'restore_by_file', 'restore_by_time', 'all_backup', 'all_restore']).optional(), "triggerType": zod.enum(['automatic', 'manual']).optional(), "name": zod.string().optional(), "status": zod.enum(['all', 'running', 'finished', 'abnormal', 'stopped']).optional(), "restoredTs": zod.string().optional(), "startTime": zod.string().optional(), "endTime": zod.string().optional(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional(), "destination": zod.string().optional(), "size": zod.string().optional(), "sizeByte": zod.string().optional(), "errorMessage": zod.string().optional(), "policyId": zod.string().optional(), "policyName": zod.string().optional(), "log": zod.string().optional(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "expireTime": zod.string().optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const globalBRServiceDeleteBRTaskParams = zod.object({ "taskId": zod.string() }) export const globalBRServiceDeleteBRTaskQueryParams = zod.object({ "deleteBackupFile": zod.boolean().optional() }) export const globalBRServiceDeleteBRTaskResponse = zod.object({ }) export const globalBRServiceStartBRTaskParams = zod.object({ "taskId": zod.string() }) export const globalBRServiceStartBRTaskResponse = zod.object({ }) export const globalBRServiceStopBRTaskParams = zod.object({ "taskId": zod.string() }) export const globalBRServiceStopBRTaskResponse = zod.object({ }) export const clusterBRServiceCreateBackupTaskParams = zod.object({ "clusterId": zod.string() }) export const clusterBRServiceCreateBackupTaskBody = zod.object({ "name": zod.string().optional(), "destination": zod.string(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "retention": zod.number().optional() }) export const clusterBRServiceCreateBackupTaskResponse = zod.object({ "taskId": zod.string().optional(), "type": zod.enum(['full_backup', 'log_backup', 'restore_by_file', 'restore_by_time', 'all_backup', 'all_restore']).optional(), "triggerType": zod.enum(['automatic', 'manual']).optional(), "name": zod.string().optional(), "status": zod.enum(['running', 'finished', 'abnormal', 'stopped']).optional(), "restoredTs": zod.string().optional(), "startTime": zod.string().optional(), "endTime": zod.string().optional(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional(), "destination": zod.string().optional(), "size": zod.string().optional(), "sizeByte": zod.string().optional(), "errorMessage": zod.string().optional(), "policyId": zod.string().optional(), "policyName": zod.string().optional(), "log": zod.string().optional(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "expireTime": zod.string().optional() }) export const clusterBRServiceGetClusterBackupPolicyParams = zod.object({ "clusterId": zod.string() }) export const clusterBRServiceGetClusterBackupPolicyResponse = zod.object({ "policyId": zod.string().optional(), "name": zod.string(), "logBackup": zod.boolean(), "destination": zod.string(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "cycle": zod.enum(['week', 'month']).optional(), "frequency": zod.string(), "time": zod.string(), "retention": zod.number(), "clusters": zod.array(zod.object({ "id": zod.string().optional(), "name": zod.string().optional() })).optional(), "lastBackupTime": zod.string().optional(), "size": zod.string().optional(), "sizeByte": zod.string().optional(), "lastLogBackupTime": zod.string().optional(), "logBackupDelay": zod.string().optional() }) export const clusterBRServiceListClusterBackupRecordsParams = zod.object({ "clusterId": zod.string() }) export const clusterBRServiceListClusterBackupRecordsQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional() }) export const clusterBRServiceListClusterBackupRecordsResponse = zod.object({ "backupRecords": zod.array(zod.object({ "taskId": zod.string().optional(), "type": zod.enum(['full_backup', 'log_backup', 'restore_by_file', 'restore_by_time', 'all_backup', 'all_restore']).optional(), "triggerType": zod.enum(['automatic', 'manual']).optional(), "name": zod.string().optional(), "status": zod.enum(['running', 'finished', 'abnormal', 'stopped']).optional(), "restoredTs": zod.string().optional(), "startTime": zod.string().optional(), "endTime": zod.string().optional(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional(), "destination": zod.string().optional(), "size": zod.string().optional(), "sizeByte": zod.string().optional(), "errorMessage": zod.string().optional(), "policyId": zod.string().optional(), "policyName": zod.string().optional(), "log": zod.string().optional(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "expireTime": zod.string().optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const clusterBRServiceCreateRestoreTaskParams = zod.object({ "clusterId": zod.string() }) export const clusterBRServiceCreateRestoreTaskBody = zod.object({ "targetClusterId": zod.string(), "type": zod.enum(['full_backup', 'log_backup', 'restore_by_file', 'restore_by_time', 'all_backup', 'all_restore']).optional(), "backupTaskId": zod.string().optional(), "restoreTime": zod.string().optional(), "destination": zod.string().optional(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional() }) export const clusterBRServiceCreateRestoreTaskResponse = zod.object({ "taskId": zod.string().optional(), "type": zod.enum(['full_backup', 'log_backup', 'restore_by_file', 'restore_by_time', 'all_backup', 'all_restore']).optional(), "triggerType": zod.enum(['automatic', 'manual']).optional(), "name": zod.string().optional(), "status": zod.enum(['running', 'finished', 'abnormal', 'stopped']).optional(), "restoredTs": zod.string().optional(), "startTime": zod.string().optional(), "endTime": zod.string().optional(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional(), "destination": zod.string().optional(), "size": zod.string().optional(), "sizeByte": zod.string().optional(), "errorMessage": zod.string().optional(), "policyId": zod.string().optional(), "policyName": zod.string().optional(), "log": zod.string().optional(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "expireTime": zod.string().optional() }) export const clusterBRServiceListClusterBRTasksParams = zod.object({ "clusterId": zod.string() }) export const clusterBRServiceListClusterBRTasksQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "brTaskId": zod.string().optional(), "clusterName": zod.string().optional(), "type": zod.enum(['full_backup', 'log_backup', 'restore_by_file', 'restore_by_time', 'all_backup', 'all_restore']).optional(), "status": zod.enum(['running', 'finished', 'abnormal', 'stopped']).optional() }) export const clusterBRServiceListClusterBRTasksResponse = zod.object({ "brTasks": zod.array(zod.object({ "taskId": zod.string().optional(), "type": zod.enum(['full_backup', 'log_backup', 'restore_by_file', 'restore_by_time', 'all_backup', 'all_restore']).optional(), "triggerType": zod.enum(['automatic', 'manual']).optional(), "name": zod.string().optional(), "status": zod.enum(['running', 'finished', 'abnormal', 'stopped']).optional(), "restoredTs": zod.string().optional(), "startTime": zod.string().optional(), "endTime": zod.string().optional(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional(), "destination": zod.string().optional(), "size": zod.string().optional(), "sizeByte": zod.string().optional(), "errorMessage": zod.string().optional(), "policyId": zod.string().optional(), "policyName": zod.string().optional(), "log": zod.string().optional(), "accessKeyId": zod.string().optional(), "secretAccessKey": zod.string().optional(), "rateLimit": zod.number().optional(), "concurrency": zod.number().optional(), "logFile": zod.string().optional(), "expireTime": zod.string().optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const clusterBRServiceDetectClusterParams = zod.object({ "clusterId": zod.string() }) export const clusterBRServiceDetectClusterResponse = zod.object({ "exist": zod.boolean().optional() }) export const metricsServiceGetClusterMetricDataParams = zod.object({ "clusterId": zod.string(), "name": zod.string() }) export const metricsServiceGetClusterMetricDataQueryParams = zod.object({ "startTime": zod.string(), "endTime": zod.string(), "step": zod.string().optional(), "label": zod.string().optional(), "range": zod.string().optional() }) export const metricsServiceGetClusterMetricDataResponse = zod.object({ "status": zod.string().optional(), "data": zod.array(zod.object({ "expr": zod.string().optional(), "legend": zod.string().optional(), "result": zod.array(zod.object({ "metric": zod.object({ "instance": zod.string().optional(), "sqlType": zod.string().optional(), "type": zod.string().optional(), "result": zod.string().optional(), "txnMode": zod.string().optional(), "job": zod.string().optional(), "device": zod.string().optional(), "fstype": zod.string().optional(), "mountpoint": zod.string().optional(), "module": zod.string().optional(), "kind": zod.string().optional(), "ping": zod.string().optional() }).optional(), "values": zod.array(zod.object({ "timestamp": zod.number().optional(), "value": zod.string().optional() })).optional() })).optional() })).optional() }) export const metricsServiceGetClusterMetricInstanceParams = zod.object({ "clusterId": zod.string(), "name": zod.string() }) export const metricsServiceGetClusterMetricInstanceResponse = zod.object({ "type": zod.string().optional(), "instanceList": zod.array(zod.string()).optional() }) export const diagnosisServiceGetResourceGroupListParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetResourceGroupListResponse = zod.object({ "resourceGroups": zod.array(zod.object({ "name": zod.string().optional(), "ruPerSec": zod.string().optional(), "priority": zod.string().optional(), "burstable": zod.string().optional() })).optional() }) export const clusterServiceGetProcessListParams = zod.object({ "clusterId": zod.string() }) export const clusterServiceGetProcessListResponse = zod.object({ "clusterProcessList": zod.array(zod.object({ "instance": zod.string().optional(), "id": zod.string().optional(), "user": zod.string().optional(), "host": zod.string().optional(), "db": zod.string().optional(), "command": zod.enum(['Sleep', 'Quit', 'Init DB', 'Query', 'Field List', 'Create DB', 'Drop DB', 'Refresh', 'Shutdown', 'Statistics', 'Processlist', 'Connect', 'Kill', 'Debug', 'Ping', 'Time', 'Delayed Insert', 'Change User', 'Binlog Dump', 'Table Dump', 'Connect out', 'Register Slave', 'Prepare', 'Execute', 'Long Data', 'Close stmt', 'Reset stmt', 'Set option', 'Fetch', 'Daemon', 'Reset connect']).optional(), "time": zod.string().optional(), "state": zod.string().optional(), "info": zod.string().optional(), "digest": zod.string().optional(), "mem": zod.string().optional(), "disk": zod.string().optional(), "txnStart": zod.string().optional(), "resourceGroup": zod.string().optional(), "sessionAlias": zod.string().optional(), "rowsAffected": zod.string().optional(), "tidbCpu": zod.string().optional(), "tikvCpu": zod.string().optional() })).optional(), "isSupportKill": zod.boolean().optional(), "totalProcessCount": zod.string().optional(), "activeProcessCount": zod.string().optional() }) export const clusterServiceDeleteProcessParams = zod.object({ "clusterId": zod.string(), "sessionId": zod.string() }) export const clusterServiceDeleteProcessResponse = zod.object({ }) export const diagnosisServiceGetSlowQueryListParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetSlowQueryListQueryParams = zod.object({ "beginTime": zod.string(), "endTime": zod.string(), "db": zod.array(zod.string()).optional(), "text": zod.string().optional(), "orderBy": zod.string().optional(), "isDesc": zod.boolean().optional(), "fields": zod.string().optional(), "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "advancedFilter": zod.array(zod.string()).optional() }) export const diagnosisServiceGetSlowQueryListResponse = zod.object({ "data": zod.array(zod.object({ "digest": zod.string().optional(), "query": zod.string().optional(), "instance": zod.string().optional(), "db": zod.string().optional(), "connection_id": zod.string().optional(), "success": zod.number().optional(), "timestamp": zod.number().optional(), "query_time": zod.number().optional(), "parse_time": zod.number().optional(), "compile_time": zod.number().optional(), "rewrite_time": zod.number().optional(), "preproc_subqueries_time": zod.number().optional(), "optimize_time": zod.number().optional(), "wait_ts": zod.number().optional(), "cop_time": zod.number().optional(), "lock_keys_time": zod.number().optional(), "write_sql_response_total": zod.number().optional(), "exec_retry_time": zod.number().optional(), "memory_max": zod.number().optional(), "disk_max": zod.number().optional(), "txn_start_ts": zod.string().optional(), "prev_stmt": zod.string().optional(), "plan": zod.string().optional(), "binary_plan": zod.string().optional(), "warnings": zod.string().optional(), "is_internal": zod.number().optional(), "index_names": zod.string().optional(), "stats": zod.string().optional(), "backoff_types": zod.string().optional(), "prepared": zod.number().optional(), "plan_from_cache": zod.number().optional(), "plan_from_binding": zod.number().optional(), "user": zod.string().optional(), "host": zod.string().optional(), "ia_remote_read_segment_size": zod.number().optional(), "ia_remote_read_segment_wait_time": zod.number().optional(), "process_time": zod.number().optional(), "wait_time": zod.number().optional(), "backoff_time": zod.number().optional(), "get_commit_ts_time": zod.number().optional(), "local_latch_wait_time": zod.number().optional(), "resolve_lock_time": zod.number().optional(), "prewrite_time": zod.number().optional(), "wait_prewrite_binlog_time": zod.number().optional(), "commit_time": zod.number().optional(), "commit_backoff_time": zod.number().optional(), "cop_proc_avg": zod.number().optional(), "cop_proc_p90": zod.number().optional(), "cop_proc_max": zod.number().optional(), "cop_wait_avg": zod.number().optional(), "cop_wait_p90": zod.number().optional(), "cop_wait_max": zod.number().optional(), "write_keys": zod.number().optional(), "write_size": zod.number().optional(), "prewrite_region": zod.number().optional(), "txn_retry": zod.number().optional(), "request_count": zod.number().optional(), "process_keys": zod.number().optional(), "total_keys": zod.number().optional(), "cop_proc_addr": zod.string().optional(), "cop_wait_addr": zod.string().optional(), "rocksdb_delete_skipped_count": zod.number().optional(), "rocksdb_key_skipped_count": zod.number().optional(), "rocksdb_block_cache_hit_count": zod.number().optional(), "rocksdb_block_read_count": zod.number().optional(), "rocksdb_block_read_byte": zod.number().optional(), "binary_plan_text": zod.string().optional(), "session_alias": zod.string().optional(), "exec_retry_count": zod.number().optional(), "preproc_subqueries": zod.number().optional(), "kv_total": zod.number().optional(), "pd_total": zod.number().optional(), "backoff_total": zod.number().optional(), "time_queued_by_rc": zod.number().optional(), "tidb_cpu_time": zod.number().optional(), "tikv_cpu_time": zod.number().optional(), "backoff_detail": zod.string().optional(), "is_explicit_txn": zod.number().optional(), "plan_digest": zod.string().optional(), "has_more_results": zod.number().optional(), "resource_group": zod.string().optional(), "request_unit_read": zod.number().optional(), "request_unit_write": zod.number().optional(), "result_rows": zod.number().optional(), "ru": zod.number().optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const diagnosisServiceGetSlowQueryAvailableAdvancedFiltersParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetSlowQueryAvailableAdvancedFiltersResponse = zod.object({ "filters": zod.array(zod.string()).optional() }) export const diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoParams = zod.object({ "clusterId": zod.string(), "filterName": zod.string() }) export const diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfoResponse = zod.object({ "name": zod.string().optional(), "unit": zod.string().optional(), "valueList": zod.array(zod.string()).optional(), "type": zod.string().optional() }) export const diagnosisServiceDownloadSlowQueryListParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceDownloadSlowQueryListQueryParams = zod.object({ "beginTime": zod.string(), "endTime": zod.string(), "db": zod.array(zod.string()).optional(), "text": zod.string().optional(), "orderBy": zod.string().optional(), "isDesc": zod.boolean().optional(), "fields": zod.string().optional(), "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "advancedFilter": zod.array(zod.string()).optional() }) export const diagnosisServiceDownloadSlowQueryListResponse = zod.object({ "filename": zod.string().optional(), "fileContent": zod.string().optional() }) export const diagnosisServiceGetSlowQueryAvailableFieldsParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetSlowQueryAvailableFieldsResponse = zod.object({ "fields": zod.array(zod.string()).optional() }) export const diagnosisServiceGetSlowQueryDetailParams = zod.object({ "clusterId": zod.string(), "digest": zod.string() }) export const diagnosisServiceGetSlowQueryDetailQueryParams = zod.object({ "timestamp": zod.number(), "connectionId": zod.string() }) export const diagnosisServiceGetSlowQueryDetailResponse = zod.object({ "digest": zod.string().optional(), "query": zod.string().optional(), "instance": zod.string().optional(), "db": zod.string().optional(), "connection_id": zod.string().optional(), "success": zod.number().optional(), "timestamp": zod.number().optional(), "query_time": zod.number().optional(), "parse_time": zod.number().optional(), "compile_time": zod.number().optional(), "rewrite_time": zod.number().optional(), "preproc_subqueries_time": zod.number().optional(), "optimize_time": zod.number().optional(), "wait_ts": zod.number().optional(), "cop_time": zod.number().optional(), "lock_keys_time": zod.number().optional(), "write_sql_response_total": zod.number().optional(), "exec_retry_time": zod.number().optional(), "memory_max": zod.number().optional(), "disk_max": zod.number().optional(), "txn_start_ts": zod.string().optional(), "prev_stmt": zod.string().optional(), "plan": zod.string().optional(), "binary_plan": zod.string().optional(), "warnings": zod.string().optional(), "is_internal": zod.number().optional(), "index_names": zod.string().optional(), "stats": zod.string().optional(), "backoff_types": zod.string().optional(), "prepared": zod.number().optional(), "plan_from_cache": zod.number().optional(), "plan_from_binding": zod.number().optional(), "user": zod.string().optional(), "host": zod.string().optional(), "ia_remote_read_segment_size": zod.number().optional(), "ia_remote_read_segment_wait_time": zod.number().optional(), "process_time": zod.number().optional(), "wait_time": zod.number().optional(), "backoff_time": zod.number().optional(), "get_commit_ts_time": zod.number().optional(), "local_latch_wait_time": zod.number().optional(), "resolve_lock_time": zod.number().optional(), "prewrite_time": zod.number().optional(), "wait_prewrite_binlog_time": zod.number().optional(), "commit_time": zod.number().optional(), "commit_backoff_time": zod.number().optional(), "cop_proc_avg": zod.number().optional(), "cop_proc_p90": zod.number().optional(), "cop_proc_max": zod.number().optional(), "cop_wait_avg": zod.number().optional(), "cop_wait_p90": zod.number().optional(), "cop_wait_max": zod.number().optional(), "write_keys": zod.number().optional(), "write_size": zod.number().optional(), "prewrite_region": zod.number().optional(), "txn_retry": zod.number().optional(), "request_count": zod.number().optional(), "process_keys": zod.number().optional(), "total_keys": zod.number().optional(), "cop_proc_addr": zod.string().optional(), "cop_wait_addr": zod.string().optional(), "rocksdb_delete_skipped_count": zod.number().optional(), "rocksdb_key_skipped_count": zod.number().optional(), "rocksdb_block_cache_hit_count": zod.number().optional(), "rocksdb_block_read_count": zod.number().optional(), "rocksdb_block_read_byte": zod.number().optional(), "binary_plan_text": zod.string().optional(), "session_alias": zod.string().optional(), "exec_retry_count": zod.number().optional(), "preproc_subqueries": zod.number().optional(), "kv_total": zod.number().optional(), "pd_total": zod.number().optional(), "backoff_total": zod.number().optional(), "time_queued_by_rc": zod.number().optional(), "tidb_cpu_time": zod.number().optional(), "tikv_cpu_time": zod.number().optional(), "backoff_detail": zod.string().optional(), "is_explicit_txn": zod.number().optional(), "plan_digest": zod.string().optional(), "has_more_results": zod.number().optional(), "resource_group": zod.string().optional(), "request_unit_read": zod.number().optional(), "request_unit_write": zod.number().optional(), "result_rows": zod.number().optional(), "ru": zod.number().optional() }) export const diagnosisServiceAddSqlLimitParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceAddSqlLimitBody = zod.object({ "resourceGroup": zod.string(), "action": zod.enum(['DRYRUN', 'COOLDOWN', 'KILL']), "watchText": zod.string() }) export const diagnosisServiceAddSqlLimitResponse = zod.object({ }) export const diagnosisServiceCheckSqlLimitSupportParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceCheckSqlLimitSupportResponse = zod.object({ "isSupport": zod.boolean().optional() }) export const diagnosisServiceRemoveSqlLimitParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceRemoveSqlLimitBody = zod.object({ "watchText": zod.string(), "id": zod.string() }) export const diagnosisServiceRemoveSqlLimitResponse = zod.object({ }) export const diagnosisServiceGetSqlLimitListParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetSqlLimitListQueryParams = zod.object({ "watchText": zod.string() }) export const diagnosisServiceGetSqlLimitListResponse = zod.object({ "data": zod.array(zod.object({ "id": zod.string().optional(), "resourceGroupName": zod.string().optional(), "startTime": zod.string().optional(), "endTime": zod.string().optional(), "watch": zod.string().optional(), "watchText": zod.string().optional(), "source": zod.string().optional(), "action": zod.enum(['DRYRUN', 'COOLDOWN', 'KILL']).optional() })).optional() }) export const diagnosisServiceGetSqlPlanListParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetSqlPlanListQueryParams = zod.object({ "beginTime": zod.string(), "endTime": zod.string(), "digest": zod.string().optional(), "schemaName": zod.string().optional() }) export const diagnosisServiceGetSqlPlanListResponse = zod.object({ "data": zod.array(zod.object({ "summary_begin_time": zod.number().optional(), "summary_end_time": zod.number().optional(), "digest_text": zod.string().optional(), "digest": zod.string().optional(), "exec_count": zod.number().optional(), "stmt_type": zod.string().optional(), "sum_errors": zod.number().optional(), "sum_warnings": zod.number().optional(), "sum_latency": zod.number().optional(), "max_latency": zod.number().optional(), "min_latency": zod.number().optional(), "avg_latency": zod.number().optional(), "avg_parse_latency": zod.number().optional(), "max_parse_latency": zod.number().optional(), "avg_compile_latency": zod.number().optional(), "max_compile_latency": zod.number().optional(), "sum_cop_task_num": zod.number().optional(), "avg_cop_process_time": zod.number().optional(), "max_cop_process_time": zod.number().optional(), "avg_cop_wait_time": zod.number().optional(), "max_cop_wait_time": zod.number().optional(), "avg_process_time": zod.number().optional(), "max_process_time": zod.number().optional(), "avg_wait_time": zod.number().optional(), "max_wait_time": zod.number().optional(), "avg_backoff_time": zod.number().optional(), "max_backoff_time": zod.number().optional(), "avg_total_keys": zod.number().optional(), "max_total_keys": zod.number().optional(), "avg_processed_keys": zod.number().optional(), "max_processed_keys": zod.number().optional(), "avg_prewrite_time": zod.number().optional(), "max_prewrite_time": zod.number().optional(), "avg_commit_time": zod.number().optional(), "max_commit_time": zod.number().optional(), "avg_get_commit_ts_time": zod.number().optional(), "max_get_commit_ts_time": zod.number().optional(), "avg_commit_backoff_time": zod.number().optional(), "max_commit_backoff_time": zod.number().optional(), "avg_resolve_lock_time": zod.number().optional(), "max_resolve_lock_time": zod.number().optional(), "avg_local_latch_wait_time": zod.number().optional(), "max_local_latch_wait_time": zod.number().optional(), "avg_write_keys": zod.number().optional(), "max_write_keys": zod.number().optional(), "avg_write_size": zod.number().optional(), "max_write_size": zod.number().optional(), "avg_prewrite_regions": zod.number().optional(), "max_prewrite_regions": zod.number().optional(), "avg_txn_retry": zod.number().optional(), "max_txn_retry": zod.number().optional(), "sum_backoff_times": zod.number().optional(), "avg_mem": zod.number().optional(), "max_mem": zod.number().optional(), "avg_disk": zod.number().optional(), "max_disk": zod.number().optional(), "avg_affected_rows": zod.number().optional(), "first_seen": zod.number().optional(), "last_seen": zod.number().optional(), "sample_user": zod.string().optional(), "query_sample_text": zod.string().optional(), "prev_sample_text": zod.string().optional(), "schema_name": zod.string().optional(), "table_names": zod.string().optional(), "index_names": zod.string().optional(), "plan_count": zod.number().optional(), "plan": zod.string().optional(), "binary_plan": zod.string().optional(), "plan_digest": zod.string().optional(), "plan_hint": zod.string().optional(), "max_rocksdb_delete_skipped_count": zod.number().optional(), "avg_rocksdb_delete_skipped_count": zod.number().optional(), "max_rocksdb_key_skipped_count": zod.number().optional(), "avg_rocksdb_key_skipped_count": zod.number().optional(), "max_rocksdb_block_cache_hit_count": zod.number().optional(), "avg_rocksdb_block_cache_hit_count": zod.number().optional(), "max_rocksdb_block_read_count": zod.number().optional(), "avg_rocksdb_block_read_count": zod.number().optional(), "max_rocksdb_block_read_byte": zod.number().optional(), "avg_rocksdb_block_read_byte": zod.number().optional(), "related_schemas": zod.string().optional(), "plan_can_be_bound": zod.boolean().optional(), "binary_plan_text": zod.string().optional(), "resource_group": zod.string().optional(), "avg_ru": zod.number().optional(), "max_ru": zod.number().optional(), "sum_ru": zod.number().optional(), "avg_time_queued_by_rc": zod.number().optional(), "max_time_queued_by_rc": zod.number().optional(), "avg_tidb_cpu_time": zod.number().optional(), "avg_tikv_cpu_time": zod.number().optional() })).optional() }) export const diagnosisServiceBindSqlPlanParams = zod.object({ "clusterId": zod.string(), "planDigest": zod.string() }) export const diagnosisServiceBindSqlPlanResponse = zod.object({ }) export const diagnosisServiceCheckSqlPlanSupportParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceCheckSqlPlanSupportResponse = zod.object({ "isSupport": zod.boolean().optional() }) export const diagnosisServiceGetSqlPlanBindingListParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetSqlPlanBindingListQueryParams = zod.object({ "beginTime": zod.string(), "endTime": zod.string(), "digest": zod.string() }) export const diagnosisServiceGetSqlPlanBindingListResponse = zod.object({ "data": zod.array(zod.object({ "status": zod.enum(['enabled', 'using', 'disabled', 'deleted', 'invalid', 'rejected', 'pending verify']).optional(), "source": zod.enum(['manual', 'history', 'capture', 'evolve']).optional(), "digest": zod.string().optional(), "planDigest": zod.string().optional() })).optional() }) export const diagnosisServiceUnbindSqlPlanParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceUnbindSqlPlanQueryParams = zod.object({ "digest": zod.string() }) export const diagnosisServiceUnbindSqlPlanResponse = zod.object({ }) export const diagnosisServiceGetTopSqlListParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetTopSqlListQueryParams = zod.object({ "beginTime": zod.string(), "endTime": zod.string(), "db": zod.array(zod.string()).optional(), "text": zod.string().optional(), "orderBy": zod.string().optional(), "isDesc": zod.boolean().optional(), "fields": zod.string().optional(), "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "advancedFilter": zod.array(zod.string()).optional(), "isGroupByTime": zod.boolean().optional() }) export const diagnosisServiceGetTopSqlListResponse = zod.object({ "data": zod.array(zod.object({ "summary_begin_time": zod.number().optional(), "summary_end_time": zod.number().optional(), "digest_text": zod.string().optional(), "digest": zod.string().optional(), "exec_count": zod.number().optional(), "stmt_type": zod.string().optional(), "sum_errors": zod.number().optional(), "sum_warnings": zod.number().optional(), "sum_latency": zod.number().optional(), "max_latency": zod.number().optional(), "min_latency": zod.number().optional(), "avg_latency": zod.number().optional(), "avg_parse_latency": zod.number().optional(), "max_parse_latency": zod.number().optional(), "avg_compile_latency": zod.number().optional(), "max_compile_latency": zod.number().optional(), "sum_cop_task_num": zod.number().optional(), "avg_cop_process_time": zod.number().optional(), "max_cop_process_time": zod.number().optional(), "avg_cop_wait_time": zod.number().optional(), "max_cop_wait_time": zod.number().optional(), "avg_process_time": zod.number().optional(), "max_process_time": zod.number().optional(), "avg_wait_time": zod.number().optional(), "max_wait_time": zod.number().optional(), "avg_backoff_time": zod.number().optional(), "max_backoff_time": zod.number().optional(), "avg_total_keys": zod.number().optional(), "max_total_keys": zod.number().optional(), "avg_processed_keys": zod.number().optional(), "max_processed_keys": zod.number().optional(), "avg_prewrite_time": zod.number().optional(), "max_prewrite_time": zod.number().optional(), "avg_commit_time": zod.number().optional(), "max_commit_time": zod.number().optional(), "avg_get_commit_ts_time": zod.number().optional(), "max_get_commit_ts_time": zod.number().optional(), "avg_commit_backoff_time": zod.number().optional(), "max_commit_backoff_time": zod.number().optional(), "avg_resolve_lock_time": zod.number().optional(), "max_resolve_lock_time": zod.number().optional(), "avg_local_latch_wait_time": zod.number().optional(), "max_local_latch_wait_time": zod.number().optional(), "avg_write_keys": zod.number().optional(), "max_write_keys": zod.number().optional(), "avg_write_size": zod.number().optional(), "max_write_size": zod.number().optional(), "avg_prewrite_regions": zod.number().optional(), "max_prewrite_regions": zod.number().optional(), "avg_txn_retry": zod.number().optional(), "max_txn_retry": zod.number().optional(), "sum_backoff_times": zod.number().optional(), "avg_mem": zod.number().optional(), "max_mem": zod.number().optional(), "avg_disk": zod.number().optional(), "max_disk": zod.number().optional(), "avg_affected_rows": zod.number().optional(), "first_seen": zod.number().optional(), "last_seen": zod.number().optional(), "sample_user": zod.string().optional(), "query_sample_text": zod.string().optional(), "prev_sample_text": zod.string().optional(), "schema_name": zod.string().optional(), "table_names": zod.string().optional(), "index_names": zod.string().optional(), "plan_count": zod.number().optional(), "plan": zod.string().optional(), "binary_plan": zod.string().optional(), "plan_digest": zod.string().optional(), "plan_hint": zod.string().optional(), "max_rocksdb_delete_skipped_count": zod.number().optional(), "avg_rocksdb_delete_skipped_count": zod.number().optional(), "max_rocksdb_key_skipped_count": zod.number().optional(), "avg_rocksdb_key_skipped_count": zod.number().optional(), "max_rocksdb_block_cache_hit_count": zod.number().optional(), "avg_rocksdb_block_cache_hit_count": zod.number().optional(), "max_rocksdb_block_read_count": zod.number().optional(), "avg_rocksdb_block_read_count": zod.number().optional(), "max_rocksdb_block_read_byte": zod.number().optional(), "avg_rocksdb_block_read_byte": zod.number().optional(), "related_schemas": zod.string().optional(), "plan_can_be_bound": zod.boolean().optional(), "binary_plan_text": zod.string().optional(), "resource_group": zod.string().optional(), "avg_ru": zod.number().optional(), "max_ru": zod.number().optional(), "sum_ru": zod.number().optional(), "avg_time_queued_by_rc": zod.number().optional(), "max_time_queued_by_rc": zod.number().optional(), "avg_tidb_cpu_time": zod.number().optional(), "avg_tikv_cpu_time": zod.number().optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const diagnosisServiceGetTopSqlAvailableAdvancedFiltersParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetTopSqlAvailableAdvancedFiltersResponse = zod.object({ "filters": zod.array(zod.string()).optional() }) export const diagnosisServiceGetTopSqlAvailableAdvancedFilterInfoParams = zod.object({ "clusterId": zod.string(), "filterName": zod.string() }) export const diagnosisServiceGetTopSqlAvailableAdvancedFilterInfoResponse = zod.object({ "name": zod.string().optional(), "unit": zod.string().optional(), "valueList": zod.array(zod.string()).optional(), "type": zod.string().optional() }) export const diagnosisServiceGetTopSqlConfigsParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetTopSqlConfigsResponse = zod.object({ "enable": zod.boolean().optional(), "refreshInterval": zod.number().optional(), "historySize": zod.number().optional(), "maxSize": zod.number().optional(), "internalQuery": zod.boolean().optional() }) export const diagnosisServiceUpdateTopSqlConfigsParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceUpdateTopSqlConfigsBody = zod.object({ "enable": zod.boolean(), "refreshInterval": zod.number().optional(), "historySize": zod.number().optional(), "maxSize": zod.number().optional(), "internalQuery": zod.boolean().optional() }) export const diagnosisServiceUpdateTopSqlConfigsResponse = zod.object({ "enable": zod.boolean().optional(), "refreshInterval": zod.number().optional(), "historySize": zod.number().optional(), "maxSize": zod.number().optional(), "internalQuery": zod.boolean().optional() }) export const diagnosisServiceGetTopSqlAvailableFieldsParams = zod.object({ "clusterId": zod.string() }) export const diagnosisServiceGetTopSqlAvailableFieldsResponse = zod.object({ "fields": zod.array(zod.string()).optional() }) export const diagnosisServiceGetTopSqlDetailParams = zod.object({ "clusterId": zod.string(), "digest": zod.string() }) export const diagnosisServiceGetTopSqlDetailQueryParams = zod.object({ "beginTime": zod.string(), "endTime": zod.string(), "planDigest": zod.array(zod.string()).optional() }) export const diagnosisServiceGetTopSqlDetailResponse = zod.object({ "summary_begin_time": zod.number().optional(), "summary_end_time": zod.number().optional(), "digest_text": zod.string().optional(), "digest": zod.string().optional(), "exec_count": zod.number().optional(), "stmt_type": zod.string().optional(), "sum_errors": zod.number().optional(), "sum_warnings": zod.number().optional(), "sum_latency": zod.number().optional(), "max_latency": zod.number().optional(), "min_latency": zod.number().optional(), "avg_latency": zod.number().optional(), "avg_parse_latency": zod.number().optional(), "max_parse_latency": zod.number().optional(), "avg_compile_latency": zod.number().optional(), "max_compile_latency": zod.number().optional(), "sum_cop_task_num": zod.number().optional(), "avg_cop_process_time": zod.number().optional(), "max_cop_process_time": zod.number().optional(), "avg_cop_wait_time": zod.number().optional(), "max_cop_wait_time": zod.number().optional(), "avg_process_time": zod.number().optional(), "max_process_time": zod.number().optional(), "avg_wait_time": zod.number().optional(), "max_wait_time": zod.number().optional(), "avg_backoff_time": zod.number().optional(), "max_backoff_time": zod.number().optional(), "avg_total_keys": zod.number().optional(), "max_total_keys": zod.number().optional(), "avg_processed_keys": zod.number().optional(), "max_processed_keys": zod.number().optional(), "avg_prewrite_time": zod.number().optional(), "max_prewrite_time": zod.number().optional(), "avg_commit_time": zod.number().optional(), "max_commit_time": zod.number().optional(), "avg_get_commit_ts_time": zod.number().optional(), "max_get_commit_ts_time": zod.number().optional(), "avg_commit_backoff_time": zod.number().optional(), "max_commit_backoff_time": zod.number().optional(), "avg_resolve_lock_time": zod.number().optional(), "max_resolve_lock_time": zod.number().optional(), "avg_local_latch_wait_time": zod.number().optional(), "max_local_latch_wait_time": zod.number().optional(), "avg_write_keys": zod.number().optional(), "max_write_keys": zod.number().optional(), "avg_write_size": zod.number().optional(), "max_write_size": zod.number().optional(), "avg_prewrite_regions": zod.number().optional(), "max_prewrite_regions": zod.number().optional(), "avg_txn_retry": zod.number().optional(), "max_txn_retry": zod.number().optional(), "sum_backoff_times": zod.number().optional(), "avg_mem": zod.number().optional(), "max_mem": zod.number().optional(), "avg_disk": zod.number().optional(), "max_disk": zod.number().optional(), "avg_affected_rows": zod.number().optional(), "first_seen": zod.number().optional(), "last_seen": zod.number().optional(), "sample_user": zod.string().optional(), "query_sample_text": zod.string().optional(), "prev_sample_text": zod.string().optional(), "schema_name": zod.string().optional(), "table_names": zod.string().optional(), "index_names": zod.string().optional(), "plan_count": zod.number().optional(), "plan": zod.string().optional(), "binary_plan": zod.string().optional(), "plan_digest": zod.string().optional(), "plan_hint": zod.string().optional(), "max_rocksdb_delete_skipped_count": zod.number().optional(), "avg_rocksdb_delete_skipped_count": zod.number().optional(), "max_rocksdb_key_skipped_count": zod.number().optional(), "avg_rocksdb_key_skipped_count": zod.number().optional(), "max_rocksdb_block_cache_hit_count": zod.number().optional(), "avg_rocksdb_block_cache_hit_count": zod.number().optional(), "max_rocksdb_block_read_count": zod.number().optional(), "avg_rocksdb_block_read_count": zod.number().optional(), "max_rocksdb_block_read_byte": zod.number().optional(), "avg_rocksdb_block_read_byte": zod.number().optional(), "related_schemas": zod.string().optional(), "plan_can_be_bound": zod.boolean().optional(), "binary_plan_text": zod.string().optional(), "resource_group": zod.string().optional(), "avg_ru": zod.number().optional(), "max_ru": zod.number().optional(), "sum_ru": zod.number().optional(), "avg_time_queued_by_rc": zod.number().optional(), "max_time_queued_by_rc": zod.number().optional(), "avg_tidb_cpu_time": zod.number().optional(), "avg_tikv_cpu_time": zod.number().optional() }) export const credentialServiceListCredentialsQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "credentialId": zod.string().optional() }) export const credentialServiceListCredentialsResponse = zod.object({ "credentials": zod.array(zod.object({ "credentialId": zod.string().optional(), "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const credentialServiceCreateCredentialBody = zod.object({ "credentialId": zod.string().optional(), "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional() }) export const credentialServiceCreateCredentialResponse = zod.object({ "credentialId": zod.string().optional(), "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional() }) export const credentialServiceGetCredentialParams = zod.object({ "credentialId": zod.string() }) export const credentialServiceGetCredentialResponse = zod.object({ "credentialId": zod.string().optional(), "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional() }) export const credentialServiceDeleteCredentialParams = zod.object({ "credentialId": zod.string() }) export const credentialServiceDeleteCredentialResponse = zod.object({ }) export const credentialServiceUpdateCredentialParams = zod.object({ "credentialId": zod.string() }) export const credentialServiceUpdateCredentialBody = zod.object({ "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional(), "forceUpdate": zod.boolean().optional() }) export const credentialServiceUpdateCredentialResponse = zod.object({ "credentialId": zod.string().optional(), "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional() }) export const credentialServiceDownloadRSAKeyParams = zod.object({ "credentialId": zod.string() }) export const credentialServiceDownloadRSAKeyResponse = zod.object({ "data": zod.string().optional() }) export const credentialServiceGenerateRSAKeyBody = zod.object({ }) export const credentialServiceGenerateRSAKeyResponse = zod.object({ "publicKey": zod.string().optional(), "privateKey": zod.string().optional() }) export const credentialServiceValidateConnectionBody = zod.object({ "credentialId": zod.string() }) export const credentialServiceValidateConnectionResponse = zod.object({ "connectionResult": zod.string().optional(), "inaccessibleHosts": zod.array(zod.string()).optional() }) export const hostServiceListHostsQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "searchValue": zod.string().optional(), "locationIds": zod.array(zod.string()).optional(), "tagIds": zod.array(zod.string()).optional() }) export const hostServiceListHostsResponse = zod.object({ "hosts": zod.array(zod.object({ "hostId": zod.string(), "ip": zod.string().optional(), "hostName": zod.string().optional(), "sshPort": zod.number().optional(), "status": zod.enum(['initializing', 'deleting', 'deleted', 'used', 'idle']).optional(), "connectionStatus": zod.enum(['online', 'offline']).optional(), "checkStatus": zod.enum(['checking', 'failed', 'warning', 'succeeded']).optional(), "credentialId": zod.string().optional(), "reportId": zod.string().optional(), "osName": zod.string().optional(), "osVendor": zod.string().optional(), "osVersion": zod.string().optional(), "osRelease": zod.string().optional(), "osArchitecture": zod.string().optional(), "cpuVendor": zod.string().optional(), "cpuModel": zod.string().optional(), "cpuSpeed": zod.number().optional(), "cpuCache": zod.number().optional(), "cpus": zod.number().optional(), "cpuThreads": zod.number().optional(), "cpuGovernor": zod.string().optional(), "cpuArch": zod.string().optional(), "memoryType": zod.string().optional(), "memorySpeed": zod.number().optional(), "memorySize": zod.number().optional(), "memorySwap": zod.number().optional(), "cpuNumaNodes": zod.number().optional(), "storageTotalSize": zod.number().optional(), "storageAvailable": zod.number().optional(), "storageUsed": zod.number().optional(), "diskType": zod.string().optional(), "clusters": zod.array(zod.object({ "clusterId": zod.string().optional(), "clusterName": zod.string().optional() })).optional(), "nodeExporterPort": zod.number().optional(), "tiupIds": zod.array(zod.string()).optional(), "hostType": zod.enum(['VM', 'PM']).optional(), "comment": zod.string().optional(), "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "createdTime": zod.string().datetime().optional(), "updatedTime": zod.string().datetime().optional(), "credential": zod.object({ "credentialId": zod.string().optional(), "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional() }).optional(), "locationMappings": zod.array(zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.string(), "locationValue": zod.string() })).optional(), "memoryUnit": zod.string().optional(), "storageUnit": zod.string().optional(), "locationId": zod.string().optional(), "cpuCores": zod.number().optional() })), "nextPageToken": zod.string(), "totalSize": zod.number() }) export const hostServiceCreateHostsBody = zod.object({ "ips": zod.array(zod.string()).optional(), "sshPort": zod.number().optional(), "credentialId": zod.string().optional(), "locationId": zod.string().optional(), "tagIds": zod.array(zod.string()).optional(), "comment": zod.string().optional() }) export const hostServiceCreateHostsResponse = zod.object({ "taskId": zod.string().optional() }) export const hostServiceImportResponse = zod.object({ "task": zod.array(zod.object({ "taskId": zod.string(), "hostId": zod.string(), "reportId": zod.string().optional(), "ip": zod.string(), "userName": zod.string().optional(), "sshPort": zod.number(), "status": zod.enum(['init', 'existed', 'succeeded', 'failed']).optional(), "tags": zod.string().optional(), "locationId": zod.string().optional(), "credentialId": zod.string().optional(), "hostName": zod.string().optional(), "tagsList": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "locationMappings": zod.array(zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.string(), "locationValue": zod.string() })).optional(), "credential": zod.object({ "credentialId": zod.string().optional(), "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional() }).optional() })), "taskId": zod.string() }) export const hostServiceImportTaskParams = zod.object({ "taskId": zod.string() }) export const hostServiceImportTaskResponse = zod.object({ "task": zod.array(zod.object({ "taskId": zod.string(), "hostId": zod.string(), "reportId": zod.string().optional(), "ip": zod.string(), "userName": zod.string().optional(), "sshPort": zod.number(), "status": zod.enum(['init', 'existed', 'succeeded', 'failed']).optional(), "tags": zod.string().optional(), "locationId": zod.string().optional(), "credentialId": zod.string().optional(), "hostName": zod.string().optional(), "tagsList": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "locationMappings": zod.array(zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.string(), "locationValue": zod.string() })).optional(), "credential": zod.object({ "credentialId": zod.string().optional(), "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional() }).optional() })), "taskId": zod.string() }) export const hostServiceHostConfirmParams = zod.object({ "taskId": zod.string() }) export const hostServiceHostConfirmBody = zod.object({ }) export const hostServiceHostConfirmResponse = zod.object({ "taskId": zod.string() }) export const hostServiceGetHostParams = zod.object({ "hostId": zod.string() }) export const hostServiceGetHostResponse = zod.object({ "hostId": zod.string(), "ip": zod.string().optional(), "hostName": zod.string().optional(), "sshPort": zod.number().optional(), "status": zod.enum(['initializing', 'deleting', 'deleted', 'used', 'idle']).optional(), "connectionStatus": zod.enum(['online', 'offline']).optional(), "checkStatus": zod.enum(['checking', 'failed', 'warning', 'succeeded']).optional(), "credentialId": zod.string().optional(), "reportId": zod.string().optional(), "osName": zod.string().optional(), "osVendor": zod.string().optional(), "osVersion": zod.string().optional(), "osRelease": zod.string().optional(), "osArchitecture": zod.string().optional(), "cpuVendor": zod.string().optional(), "cpuModel": zod.string().optional(), "cpuSpeed": zod.number().optional(), "cpuCache": zod.number().optional(), "cpus": zod.number().optional(), "cpuThreads": zod.number().optional(), "cpuGovernor": zod.string().optional(), "cpuArch": zod.string().optional(), "memoryType": zod.string().optional(), "memorySpeed": zod.number().optional(), "memorySize": zod.number().optional(), "memorySwap": zod.number().optional(), "cpuNumaNodes": zod.number().optional(), "storageTotalSize": zod.number().optional(), "storageAvailable": zod.number().optional(), "storageUsed": zod.number().optional(), "diskType": zod.string().optional(), "clusters": zod.array(zod.object({ "clusterId": zod.string().optional(), "clusterName": zod.string().optional() })).optional(), "nodeExporterPort": zod.number().optional(), "tiupIds": zod.array(zod.string()).optional(), "hostType": zod.enum(['VM', 'PM']).optional(), "comment": zod.string().optional(), "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "createdTime": zod.string().datetime().optional(), "updatedTime": zod.string().datetime().optional(), "credential": zod.object({ "credentialId": zod.string().optional(), "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional() }).optional(), "locationMappings": zod.array(zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.string(), "locationValue": zod.string() })).optional(), "memoryUnit": zod.string().optional(), "storageUnit": zod.string().optional(), "locationId": zod.string().optional(), "cpuCores": zod.number().optional() }) export const hostServiceDeleteParams = zod.object({ "hostId": zod.string() }) export const hostServiceDeleteResponse = zod.object({ }) export const hostServiceUpdateHostParams = zod.object({ "hostId": zod.string() }) export const hostServiceUpdateHostBody = zod.object({ "host": zod.object({ "hostId": zod.string().optional(), "sshPort": zod.number().optional(), "credentialId": zod.string().optional(), "locationId": zod.string().optional(), "tagIds": zod.array(zod.string()).optional(), "comment": zod.string().optional() }).optional() }) export const hostServiceUpdateHostResponse = zod.object({ "hostId": zod.string(), "ip": zod.string().optional(), "hostName": zod.string().optional(), "sshPort": zod.number().optional(), "status": zod.enum(['initializing', 'deleting', 'deleted', 'used', 'idle']).optional(), "connectionStatus": zod.enum(['online', 'offline']).optional(), "checkStatus": zod.enum(['checking', 'failed', 'warning', 'succeeded']).optional(), "credentialId": zod.string().optional(), "reportId": zod.string().optional(), "osName": zod.string().optional(), "osVendor": zod.string().optional(), "osVersion": zod.string().optional(), "osRelease": zod.string().optional(), "osArchitecture": zod.string().optional(), "cpuVendor": zod.string().optional(), "cpuModel": zod.string().optional(), "cpuSpeed": zod.number().optional(), "cpuCache": zod.number().optional(), "cpus": zod.number().optional(), "cpuThreads": zod.number().optional(), "cpuGovernor": zod.string().optional(), "cpuArch": zod.string().optional(), "memoryType": zod.string().optional(), "memorySpeed": zod.number().optional(), "memorySize": zod.number().optional(), "memorySwap": zod.number().optional(), "cpuNumaNodes": zod.number().optional(), "storageTotalSize": zod.number().optional(), "storageAvailable": zod.number().optional(), "storageUsed": zod.number().optional(), "diskType": zod.string().optional(), "clusters": zod.array(zod.object({ "clusterId": zod.string().optional(), "clusterName": zod.string().optional() })).optional(), "nodeExporterPort": zod.number().optional(), "tiupIds": zod.array(zod.string()).optional(), "hostType": zod.enum(['VM', 'PM']).optional(), "comment": zod.string().optional(), "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "createdTime": zod.string().datetime().optional(), "updatedTime": zod.string().datetime().optional(), "credential": zod.object({ "credentialId": zod.string().optional(), "userName": zod.string(), "credentialType": zod.enum(['CREDENTIAL_TYPE_UNSPECIFIED', 'HOST', 'TIDB']).optional(), "validateType": zod.enum(['CREDENTIAL_VALIDATE_TYPE_UNSPECIFIED', 'PASSWORD', 'RSAKEY']).optional(), "credentialName": zod.string().optional(), "description": zod.string().optional(), "hostCredential": zod.object({ "password": zod.string().optional(), "publicKey": zod.string().optional(), "privateKey": zod.string().optional(), "hostIps": zod.array(zod.string()).optional() }).optional(), "tidbCredential": zod.object({ "password": zod.string(), "clusterId": zod.string().optional(), "clusterName": zod.string().optional() }).optional() }).optional(), "locationMappings": zod.array(zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.string(), "locationValue": zod.string() })).optional(), "memoryUnit": zod.string().optional(), "storageUnit": zod.string().optional(), "locationId": zod.string().optional(), "cpuCores": zod.number().optional() }) export const hostServiceGetDisksParams = zod.object({ "hostId": zod.string() }) export const hostServiceGetDisksResponse = zod.object({ "disk": zod.array(zod.object({ "path": zod.string().optional(), "totalSize": zod.number().optional(), "usedSpace": zod.number().optional(), "availableSpace": zod.number().optional(), "mountingDir": zod.string().optional(), "diskType": zod.enum(['HDD', 'SSD']).optional() })).optional() }) export const metricsServiceGetHostMetricDataParams = zod.object({ "hostId": zod.string(), "name": zod.string() }) export const metricsServiceGetHostMetricDataQueryParams = zod.object({ "startTime": zod.string(), "endTime": zod.string(), "step": zod.string().optional(), "label": zod.string().optional(), "range": zod.string().optional() }) export const metricsServiceGetHostMetricDataResponse = zod.object({ "status": zod.string().optional(), "data": zod.array(zod.object({ "expr": zod.string().optional(), "legend": zod.string().optional(), "result": zod.array(zod.object({ "metric": zod.object({ "instance": zod.string().optional(), "sqlType": zod.string().optional(), "type": zod.string().optional(), "result": zod.string().optional(), "txnMode": zod.string().optional(), "job": zod.string().optional(), "device": zod.string().optional(), "fstype": zod.string().optional(), "mountpoint": zod.string().optional(), "module": zod.string().optional(), "kind": zod.string().optional(), "ping": zod.string().optional() }).optional(), "values": zod.array(zod.object({ "timestamp": zod.number().optional(), "value": zod.string().optional() })).optional() })).optional() })).optional() }) export const hostServiceReportParams = zod.object({ "hostId": zod.string(), "reportId": zod.string() }) export const hostServiceReportResponse = zod.object({ "taskId": zod.string(), "taskState": zod.enum(['init', 'running', 'success', 'fail']).optional(), "reports": zod.array(zod.object({ "reportId": zod.string(), "hostId": zod.string().optional(), "checkId": zod.string().optional(), "checkName": zod.string().optional(), "checkOut": zod.string().optional(), "checkDesc": zod.string().optional(), "checkResult": zod.enum(['passed', 'failed', 'warned']).optional(), "optional": zod.boolean().enum(['true', 'false']).optional(), "fixable": zod.boolean().enum(['true', 'false']).optional(), "checkBody": zod.string().optional() })).optional() }) export const hostServiceGetTiDBProcessesParams = zod.object({ "hostId": zod.string() }) export const hostServiceGetTiDBProcessesResponse = zod.object({ "tiDBProcesses": zod.array(zod.object({ "uid": zod.string().optional(), "pid": zod.number().optional(), "ppid": zod.number().optional(), "startTime": zod.string().optional(), "runningTime": zod.string().optional(), "cmd": zod.string().optional() })).optional() }) export const hostServiceFixParams = zod.object({ "hostId": zod.string() }) export const hostServiceFixResponse = zod.object({ "hostId": zod.string(), "taskId": zod.string(), "reportId": zod.string() }) export const hostServiceCheckParams = zod.object({ "hostId": zod.string() }) export const hostServiceCheckResponse = zod.object({ "hostId": zod.string(), "taskId": zod.string(), "reportId": zod.string() }) export const hostServiceBatchDeleteBody = zod.object({ "hostId": zod.array(zod.string()) }) export const hostServiceBatchDeleteResponse = zod.object({ }) export const hostServiceDownloadListHostsQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "searchValue": zod.string().optional(), "locationIds": zod.array(zod.string()).optional(), "tagIds": zod.array(zod.string()).optional() }) export const hostServiceDownloadListHostsResponse = zod.object({ "data": zod.string().optional() }) export const hostServiceDownloadHostTemplateResponse = zod.object({ "data": zod.string().optional() }) export const licenseServiceGetLicenseResponse = zod.object({ "licenseId": zod.string().optional(), "version": zod.string().optional(), "licenseType": zod.enum(['free', 'ultimate']).optional(), "allow": zod.array(zod.string()).optional(), "deny": zod.array(zod.string()).optional(), "activateAt": zod.string().datetime().optional(), "expirationAt": zod.string().datetime().optional(), "signature": zod.string().optional(), "hosts": zod.string().optional(), "vcpu": zod.string().optional(), "alerts": zod.string().optional(), "customerCode": zod.string().optional(), "deviceCode": zod.string().optional(), "status": zod.enum(['active', 'expired', 'expiring', 'invalid', 'revoked']).optional() }) export const licenseServiceGetDeviceCodeResponse = zod.object({ "deviceCode": zod.string().optional() }) export const licenseServiceActivateLicenseResponse = zod.object({ "licenseId": zod.string().optional(), "version": zod.string().optional(), "licenseType": zod.enum(['free', 'ultimate']).optional(), "allow": zod.array(zod.string()).optional(), "deny": zod.array(zod.string()).optional(), "activateAt": zod.string().datetime().optional(), "expirationAt": zod.string().datetime().optional(), "signature": zod.string().optional(), "hosts": zod.string().optional(), "vcpu": zod.string().optional(), "alerts": zod.string().optional(), "customerCode": zod.string().optional(), "deviceCode": zod.string().optional(), "status": zod.enum(['active', 'expired', 'expiring', 'invalid', 'revoked']).optional() }) export const licenseServiceActivateFreeLicenseResponse = zod.object({ "licenseId": zod.string().optional(), "version": zod.string().optional(), "licenseType": zod.enum(['free', 'ultimate']).optional(), "allow": zod.array(zod.string()).optional(), "deny": zod.array(zod.string()).optional(), "activateAt": zod.string().datetime().optional(), "expirationAt": zod.string().datetime().optional(), "signature": zod.string().optional(), "hosts": zod.string().optional(), "vcpu": zod.string().optional(), "alerts": zod.string().optional(), "customerCode": zod.string().optional(), "deviceCode": zod.string().optional(), "status": zod.enum(['active', 'expired', 'expiring', 'invalid', 'revoked']).optional() }) export const locationServiceListLocationsQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "locationKey": zod.enum(['zone', 'dc', 'rack']).optional(), "locationValue": zod.string().optional(), "parentId": zod.string().optional() }) export const locationServiceListLocationsResponse = zod.object({ "locations": zod.array(zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.enum(['zone', 'dc', 'rack']).optional(), "locationValue": zod.string().optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const locationServiceCreateLocationsBody = zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.enum(['zone', 'dc', 'rack']).optional(), "locationValue": zod.string().optional() }) export const locationServiceCreateLocationsResponse = zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.enum(['zone', 'dc', 'rack']).optional(), "locationValue": zod.string().optional() }) export const locationServiceGetLocationsParams = zod.object({ "locationId": zod.string() }) export const locationServiceGetLocationsResponse = zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.enum(['zone', 'dc', 'rack']).optional(), "locationValue": zod.string().optional() }) export const locationServiceDeleteLocationParams = zod.object({ "locationId": zod.string() }) export const locationServiceDeleteLocationResponse = zod.object({ }) export const locationServiceUpdateLocationsParams = zod.object({ "locationId": zod.string() }) export const locationServiceUpdateLocationsBody = zod.object({ "location": zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.enum(['zone', 'dc', 'rack']).optional(), "locationValue": zod.string().optional() }).optional() }) export const locationServiceUpdateLocationsResponse = zod.object({ "locationId": zod.string().optional(), "parentId": zod.string().optional(), "locationKey": zod.enum(['zone', 'dc', 'rack']).optional(), "locationValue": zod.string().optional() }) export const userServiceLoginBody = zod.object({ "userId": zod.string(), "password": zod.string().optional() }) export const userServiceLoginResponse = zod.object({ }) export const userServiceLogoutResponse = zod.object({ }) export const metricsServiceGetMetricsQueryParams = zod.object({ "class": zod.enum(['unspecified', 'cluster', 'host', 'overview']).optional(), "group": zod.enum(['unspecified', 'overview', 'basic', 'advanced', 'resource', 'performance', 'process']).optional(), "type": zod.string().optional(), "name": zod.string().optional() }) export const metricsServiceGetMetricsResponse = zod.object({ "metrics": zod.array(zod.object({ "class": zod.string().optional(), "group": zod.string().optional(), "type": zod.string().optional(), "order": zod.number().optional(), "displayName": zod.string().optional(), "name": zod.string().optional(), "description": zod.string().optional(), "metric": zod.object({ "name": zod.string().optional(), "unit": zod.string().optional(), "description": zod.string().optional(), "minTidbVersion": zod.string().optional(), "maxTidbVersion": zod.string().optional(), "isBuiltin": zod.boolean().optional(), "expressions": zod.array(zod.object({ "name": zod.string().optional(), "promql": zod.string().optional(), "promMetric": zod.string().optional(), "labels": zod.array(zod.string()).optional(), "type": zod.string().optional(), "legend": zod.string().optional(), "minTidbVersion": zod.string().optional(), "maxTidbVersion": zod.string().optional() })).optional() }).optional() })).optional() }) export const metricsServiceGetTopMetricConfigResponse = zod.object({ "cacheFlushIntervalInMinutes": zod.number().optional() }) export const metricsServiceGetTopMetricDataParams = zod.object({ "name": zod.string() }) export const metricsServiceGetTopMetricDataQueryParams = zod.object({ "startTime": zod.string(), "endTime": zod.string(), "step": zod.string().optional(), "limit": zod.string().optional() }) export const metricsServiceGetTopMetricDataResponse = zod.object({ "status": zod.string().optional(), "data": zod.array(zod.object({ "expr": zod.string().optional(), "legend": zod.string().optional(), "result": zod.array(zod.object({ "metric": zod.object({ "instance": zod.string().optional(), "sqlType": zod.string().optional(), "type": zod.string().optional(), "result": zod.string().optional(), "txnMode": zod.string().optional(), "job": zod.string().optional(), "device": zod.string().optional(), "fstype": zod.string().optional(), "mountpoint": zod.string().optional(), "module": zod.string().optional(), "kind": zod.string().optional(), "ping": zod.string().optional() }).optional(), "values": zod.array(zod.object({ "timestamp": zod.number().optional(), "value": zod.string().optional() })).optional() })).optional() })).optional() }) export const metricsServiceGetOverviewStatusQueryParams = zod.object({ "taskStartTime": zod.string().optional(), "taskEndTime": zod.string().optional() }) export const metricsServiceGetOverviewStatusResponse = zod.object({ "clusters": zod.array(zod.object({ "status": zod.string().optional(), "count": zod.number().optional() })).optional(), "hosts": zod.array(zod.object({ "status": zod.string().optional(), "count": zod.number().optional() })).optional(), "alerts": zod.array(zod.object({ "status": zod.string().optional(), "count": zod.number().optional() })).optional(), "alertLevels": zod.array(zod.object({ "status": zod.string().optional(), "count": zod.number().optional() })).optional(), "brTasks": zod.array(zod.object({ "status": zod.string().optional(), "count": zod.number().optional() })).optional(), "sysTasks": zod.array(zod.object({ "status": zod.string().optional(), "count": zod.number().optional() })).optional(), "otherTasks": zod.array(zod.object({ "status": zod.string().optional(), "count": zod.number().optional() })).optional() }) export const roleServiceListRolesQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "roleNameLike": zod.string().optional(), "roleName": zod.string().optional() }) export const roleServiceListRolesResponse = zod.object({ "roles": zod.array(zod.object({ "id": zod.number().optional(), "roleName": zod.string().optional(), "roleType": zod.number().optional(), "roleTypeDesc": zod.string().optional(), "detail": zod.string().optional(), "note": zod.string().optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const roleServiceCreateRoleBody = zod.object({ "id": zod.number().optional(), "roleName": zod.string().optional(), "roleType": zod.number().optional(), "roleTypeDesc": zod.string().optional(), "detail": zod.string().optional(), "note": zod.string().optional() }) export const roleServiceCreateRoleResponse = zod.object({ "id": zod.number().optional(), "roleName": zod.string().optional(), "roleType": zod.number().optional(), "roleTypeDesc": zod.string().optional(), "detail": zod.string().optional(), "note": zod.string().optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() }) export const roleServiceDeleteRoleParams = zod.object({ "roleId": zod.number() }) export const roleServiceDeleteRoleResponse = zod.object({ }) export const roleServiceUpdateRoleParams = zod.object({ "roleId": zod.number() }) export const roleServiceUpdateRoleBody = zod.object({ "roleName": zod.string(), "roleType": zod.number().optional(), "detail": zod.string().optional(), "note": zod.string().optional() }) export const roleServiceUpdateRoleResponse = zod.object({ "id": zod.number().optional(), "roleName": zod.string().optional(), "roleType": zod.number().optional(), "roleTypeDesc": zod.string().optional(), "detail": zod.string().optional(), "note": zod.string().optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() }) export const tagServiceListTagsQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional() }) export const tagServiceListTagsResponse = zod.object({ "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const tagServiceCreateTagBody = zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() }) export const tagServiceCreateTagResponse = zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() }) export const tagServiceGetTagParams = zod.object({ "tagId": zod.string() }) export const tagServiceGetTagResponse = zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() }) export const tagServiceDeleteTagParams = zod.object({ "tagId": zod.string() }) export const tagServiceDeleteTagResponse = zod.object({ }) export const tagServiceUpdateTagParams = zod.object({ "tagId": zod.string() }) export const tagServiceUpdateTagBody = zod.object({ "tagKey": zod.string().optional(), "tagValue": zod.string() }) export const tagServiceUpdateTagResponse = zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() }) export const tagServiceGetTagWithBindingsParams = zod.object({ "tagId": zod.string() }) export const tagServiceGetTagWithBindingsResponse = zod.object({ "tag": zod.object({ "tagInfo": zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() }).optional(), "bindObjects": zod.array(zod.object({ "resourceType": zod.enum(['TAG_BIND_RESOURCE_TYPE_UNSPECIFIED', 'HOST', 'TIUP', 'CLUSTER']).optional(), "resources": zod.array(zod.object({ "resourceId": zod.string().optional(), "resourceName": zod.string() })) })).optional() }).optional() }) export const tagServiceBatchCreateTagsBody = zod.object({ "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })) }) export const tagServiceBatchCreateTagsResponse = zod.object({ "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional() }) export const tagServiceBindResourceBody = zod.object({ "resourceType": zod.enum(['TAG_BIND_RESOURCE_TYPE_UNSPECIFIED', 'HOST', 'TIUP', 'CLUSTER']).optional(), "resourceId": zod.string(), "tagIds": zod.array(zod.string()).optional() }) export const tagServiceBindResourceResponse = zod.object({ "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional() }) export const tagServiceBindTagBody = zod.object({ "tagId": zod.string(), "bindObjects": zod.array(zod.object({ "resourceType": zod.enum(['TAG_BIND_RESOURCE_TYPE_UNSPECIFIED', 'HOST', 'TIUP', 'CLUSTER']).optional(), "resources": zod.array(zod.object({ "resourceId": zod.string().optional(), "resourceName": zod.string() })) })).optional() }) export const tagServiceBindTagResponse = zod.object({ "tag": zod.object({ "tagInfo": zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() }).optional(), "bindObjects": zod.array(zod.object({ "resourceType": zod.enum(['TAG_BIND_RESOURCE_TYPE_UNSPECIFIED', 'HOST', 'TIUP', 'CLUSTER']).optional(), "resources": zod.array(zod.object({ "resourceId": zod.string().optional(), "resourceName": zod.string() })) })).optional() }).optional() }) export const tagServiceListTagsByResourceTypeQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "tagKey": zod.string().optional(), "keyword": zod.string().optional(), "resourceType": zod.enum(['TAG_BIND_RESOURCE_TYPE_UNSPECIFIED', 'HOST', 'TIUP', 'CLUSTER']).optional() }) export const tagServiceListTagsByResourceTypeResponse = zod.object({ "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const tagServiceListTagKeysQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "keyword": zod.string().optional(), "resourceType": zod.enum(['TAG_BIND_RESOURCE_TYPE_UNSPECIFIED', 'HOST', 'TIUP', 'CLUSTER']).optional() }) export const tagServiceListTagKeysResponse = zod.object({ "tagKeys": zod.array(zod.string()).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const tagServiceListTagsWithBindingsQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "tagKeys": zod.array(zod.string()).optional(), "tagValueLike": zod.string().optional() }) export const tagServiceListTagsWithBindingsResponse = zod.object({ "tags": zod.array(zod.object({ "tagInfo": zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() }).optional(), "bindObjects": zod.array(zod.object({ "resourceType": zod.enum(['TAG_BIND_RESOURCE_TYPE_UNSPECIFIED', 'HOST', 'TIUP', 'CLUSTER']).optional(), "resources": zod.array(zod.object({ "resourceId": zod.string().optional(), "resourceName": zod.string() })) })).optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const tiupsServiceListTiupsQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "searchValue": zod.string().optional(), "tagIds": zod.array(zod.string()).optional(), "hostIds": zod.array(zod.string()).optional() }) export const tiupsServiceListTiupsResponse = zod.object({ "tiups": zod.array(zod.object({ "tiupId": zod.string().optional(), "name": zod.string().optional(), "tiupHome": zod.string().optional(), "version": zod.string().optional(), "credentialId": zod.string().optional(), "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "description": zod.string().optional(), "hostId": zod.string().optional(), "host": zod.object({ "hostId": zod.string(), "ip": zod.string().optional(), "hostName": zod.string().optional(), "sshPort": zod.number().optional(), "credentialId": zod.string().optional(), "osName": zod.string().optional(), "osVersion": zod.string().optional(), "osRelease": zod.string().optional(), "osArchitecture": zod.string().optional(), "hostType": zod.enum(['VM', 'PM']).optional(), "locationId": zod.string().optional(), "createdTime": zod.string().datetime().optional(), "updatedTime": zod.string().datetime().optional(), "credential": zod.object({ "credentialId": zod.string().optional(), "credentialName": zod.string().optional(), "credentialType": zod.string().optional(), "userName": zod.string().optional() }).optional() }).optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const tiupsServiceCreateTiupsBody = zod.object({ "hostId": zod.string().optional(), "tiupHome": zod.string().optional(), "description": zod.string().optional(), "name": zod.string().optional(), "tagIds": zod.array(zod.string()).optional() }) export const tiupsServiceCreateTiupsResponse = zod.object({ "tiupId": zod.string().optional(), "name": zod.string().optional(), "tiupHome": zod.string().optional(), "version": zod.string().optional(), "credentialId": zod.string().optional(), "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "description": zod.string().optional(), "hostId": zod.string().optional(), "host": zod.object({ "hostId": zod.string(), "ip": zod.string().optional(), "hostName": zod.string().optional(), "sshPort": zod.number().optional(), "credentialId": zod.string().optional(), "osName": zod.string().optional(), "osVersion": zod.string().optional(), "osRelease": zod.string().optional(), "osArchitecture": zod.string().optional(), "hostType": zod.enum(['VM', 'PM']).optional(), "locationId": zod.string().optional(), "createdTime": zod.string().datetime().optional(), "updatedTime": zod.string().datetime().optional(), "credential": zod.object({ "credentialId": zod.string().optional(), "credentialName": zod.string().optional(), "credentialType": zod.string().optional(), "userName": zod.string().optional() }).optional() }).optional() }) export const tiupsServiceGetTiupsParams = zod.object({ "tiupId": zod.string() }) export const tiupsServiceGetTiupsResponse = zod.object({ "tiupId": zod.string().optional(), "name": zod.string().optional(), "tiupHome": zod.string().optional(), "version": zod.string().optional(), "credentialId": zod.string().optional(), "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "description": zod.string().optional(), "hostId": zod.string().optional(), "host": zod.object({ "hostId": zod.string(), "ip": zod.string().optional(), "hostName": zod.string().optional(), "sshPort": zod.number().optional(), "credentialId": zod.string().optional(), "osName": zod.string().optional(), "osVersion": zod.string().optional(), "osRelease": zod.string().optional(), "osArchitecture": zod.string().optional(), "hostType": zod.enum(['VM', 'PM']).optional(), "locationId": zod.string().optional(), "createdTime": zod.string().datetime().optional(), "updatedTime": zod.string().datetime().optional(), "credential": zod.object({ "credentialId": zod.string().optional(), "credentialName": zod.string().optional(), "credentialType": zod.string().optional(), "userName": zod.string().optional() }).optional() }).optional() }) export const tiupsServiceDeleteTiupsParams = zod.object({ "tiupId": zod.string() }) export const tiupsServiceDeleteTiupsResponse = zod.object({ }) export const tiupsServiceUpdateTiupsParams = zod.object({ "tiupId": zod.string() }) export const tiupsServiceUpdateTiupsBody = zod.object({ "tiups": zod.object({ "tagIds": zod.array(zod.string()).optional(), "description": zod.string().optional(), "name": zod.string().optional() }).optional() }) export const tiupsServiceUpdateTiupsResponse = zod.object({ "tiupId": zod.string().optional(), "name": zod.string().optional(), "tiupHome": zod.string().optional(), "version": zod.string().optional(), "credentialId": zod.string().optional(), "tags": zod.array(zod.object({ "tagId": zod.string().optional(), "tagKey": zod.string().optional(), "tagValue": zod.string() })).optional(), "description": zod.string().optional(), "hostId": zod.string().optional(), "host": zod.object({ "hostId": zod.string(), "ip": zod.string().optional(), "hostName": zod.string().optional(), "sshPort": zod.number().optional(), "credentialId": zod.string().optional(), "osName": zod.string().optional(), "osVersion": zod.string().optional(), "osRelease": zod.string().optional(), "osArchitecture": zod.string().optional(), "hostType": zod.enum(['VM', 'PM']).optional(), "locationId": zod.string().optional(), "createdTime": zod.string().datetime().optional(), "updatedTime": zod.string().datetime().optional(), "credential": zod.object({ "credentialId": zod.string().optional(), "credentialName": zod.string().optional(), "credentialType": zod.string().optional(), "userName": zod.string().optional() }).optional() }).optional() }) export const tiupsServiceGetTiupsClusterParams = zod.object({ "tiupId": zod.string() }) export const tiupsServiceGetTiupsClusterResponse = zod.object({ "tiupsClusters": zod.array(zod.object({ "clusterId": zod.string().optional(), "clusterName": zod.string().optional(), "user": zod.string().optional(), "version": zod.string().optional(), "metaPath": zod.string().optional(), "privateKeyPath": zod.string().optional(), "managed": zod.boolean().optional() })).optional() }) export const userServiceListUsersQueryParams = zod.object({ "pageSize": zod.number().optional(), "pageToken": zod.string().optional(), "skip": zod.number().optional(), "orderBy": zod.string().optional(), "nameLike": zod.string().optional(), "emailLike": zod.string().optional(), "roleName": zod.string().optional() }) export const userServiceListUsersResponse = zod.object({ "users": zod.array(zod.object({ "userId": zod.string(), "name": zod.string(), "email": zod.string().optional(), "note": zod.string().optional(), "password": zod.string().optional(), "userType": zod.number().optional(), "userTypeDesc": zod.string().optional(), "phone": zod.string().optional(), "roles": zod.array(zod.object({ "roleName": zod.string().optional(), "roleId": zod.number() })).optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() })).optional(), "nextPageToken": zod.string().optional(), "totalSize": zod.number().optional() }) export const userServiceCreateUserBody = zod.object({ "userId": zod.string(), "name": zod.string(), "email": zod.string().optional(), "note": zod.string().optional(), "password": zod.string().optional(), "userType": zod.number().optional(), "userTypeDesc": zod.string().optional(), "phone": zod.string().optional(), "roles": zod.array(zod.object({ "roleName": zod.string().optional(), "roleId": zod.number() })).optional() }) export const userServiceCreateUserResponse = zod.object({ "userId": zod.string(), "name": zod.string(), "email": zod.string().optional(), "note": zod.string().optional(), "password": zod.string().optional(), "userType": zod.number().optional(), "userTypeDesc": zod.string().optional(), "phone": zod.string().optional(), "roles": zod.array(zod.object({ "roleName": zod.string().optional(), "roleId": zod.number() })).optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() }) export const userServiceGetUserProfileResponse = zod.object({ "userId": zod.string(), "name": zod.string(), "email": zod.string(), "note": zod.string().optional(), "phone": zod.string().optional() }) export const userServiceGetUserParams = zod.object({ "userId": zod.string() }) export const userServiceGetUserResponse = zod.object({ "userId": zod.string(), "name": zod.string(), "email": zod.string().optional(), "note": zod.string().optional(), "password": zod.string().optional(), "userType": zod.number().optional(), "userTypeDesc": zod.string().optional(), "phone": zod.string().optional(), "roles": zod.array(zod.object({ "roleName": zod.string().optional(), "roleId": zod.number() })).optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() }) export const userServiceDeleteUserParams = zod.object({ "userId": zod.string() }) export const userServiceDeleteUserResponse = zod.object({ }) export const userServiceUpdateUserParams = zod.object({ "userId": zod.string() }) export const userServiceUpdateUserBody = zod.object({ "email": zod.string().optional(), "note": zod.string().optional(), "userType": zod.number().optional(), "phone": zod.string().optional(), "roles": zod.array(zod.object({ "roleName": zod.string().optional(), "roleId": zod.number() })).optional() }) export const userServiceUpdateUserResponse = zod.object({ "userId": zod.string(), "name": zod.string(), "email": zod.string().optional(), "note": zod.string().optional(), "password": zod.string().optional(), "userType": zod.number().optional(), "userTypeDesc": zod.string().optional(), "phone": zod.string().optional(), "roles": zod.array(zod.object({ "roleName": zod.string().optional(), "roleId": zod.number() })).optional(), "createTime": zod.string().datetime().optional(), "updateTime": zod.string().datetime().optional() }) export const userServiceResetPasswordParams = zod.object({ "userId": zod.string() }) export const userServiceResetPasswordBody = zod.object({ "newPassword": zod.string() }) export const userServiceResetPasswordResponse = zod.object({ }) export const userServiceChangePasswordBody = zod.object({ "userId": zod.string(), "oldPassword": zod.string().optional(), "newPassword": zod.string() }) export const userServiceChangePasswordResponse = zod.object({ }) export const userServiceValidateSessionResponse = zod.object({ "userId": zod.string() }) export const apiKeyServiceGetTemErrorDetailResponse = zod.object({ "type": zod.string().optional(), "locale": zod.string().optional(), "message": zod.string().optional() }) ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/metrics-config-cluster-overview.json ================================================ { "metrics": [ { "class": "cluster", "group": "overview", "type": "", "order": 1, "displayName": "Duration", "name": "duration", "description": "Overall query execution duration across the cluster", "metric": { "name": "duration", "unit": "s", "description": "Shows average and percentile query durations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_query_duration", "promql": "sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!=\"internal\",@LABEL}[1m])) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!=\"internal\",@LABEL}[1m]))", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tidb_query_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{sql_type!=\"internal\",@LABEL}[1m])) by (le))", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_query_duration_by_sql_type", "promql": "sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!=\"internal\",@LABEL}[1m])) by (sql_type) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!=\"internal\",@LABEL}[1m])) by (sql_type)", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance", "sql_type" ], "type": "tidb", "legend": "avg-{sqlType}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 2, "displayName": "QPS", "name": "qps", "description": "Total Queries Per Second across all TiDB instances", "metric": { "name": "qps", "unit": "short", "description": "Shows total QPS, QPS by type, and failed query rates", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "qps_by_type", "promql": "sum(rate(tidb_executor_statement_total{@LABEL}[1m])) by (type)", "promMetric": "tidb_executor_statement_total", "labels": [ "instance", "type" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "qps", "promql": "sum(rate(tidb_executor_statement_total{@LABEL}[1m]))", "promMetric": "tidb_executor_statement", "labels": [ "instance" ], "type": "tidb", "legend": "Total", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "qps_error", "promql": "sum(rate(tidb_server_execute_error_total{@LABEL}[1m])) ", "promMetric": "tidb_server_execute_error_total", "labels": [ "instance" ], "type": "tidb", "legend": "Failed", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 3, "displayName": "Transaction OPS", "name": "transaction_ops_by_type_and_txn_mode", "description": "Transaction operations per second categorized by type and transaction mode", "metric": { "name": "transaction_ops_by_type_and_txn_mode", "unit": "short", "description": "Shows transaction rates categorized by type and transaction mode", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tps_by_type_and_txn_mode", "promql": "sum(rate(tidb_session_transaction_duration_seconds_count{@LABEL}[1m])) by (type, txn_mode)", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "type", "txn_mode" ], "type": "tidb", "legend": "{type}-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 4, "displayName": "Transaction Duration", "name": "transaction_duration", "description": "Time taken to execute transactions, indicating transaction processing efficiency", "metric": { "name": "transaction_duration", "unit": "s", "description": "Shows transaction duration percentiles by transaction mode", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p99_transaction_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_session_transaction_duration_seconds_bucket{@LABEL}[1m])) by (le, txn_mode))", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "txn_mode" ], "type": "tidb", "legend": "99-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p95_transaction_duration", "promql": "histogram_quantile(0.95, sum(rate(tidb_session_transaction_duration_seconds_bucket{@LABEL}[1m])) by (le, txn_mode))", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "txn_mode" ], "type": "tidb", "legend": "95-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p80_transaction_duration", "promql": "histogram_quantile(0.80, sum(rate(tidb_session_transaction_duration_seconds_bucket{@LABEL}[1m])) by (le, txn_mode))", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "txn_mode" ], "type": "tidb", "legend": "80-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 5, "displayName": "TiKV Increase of Storage Usage", "name": "tikv_increase_of_storage_usage", "description": "Rate of storage space consumption increase in TiKV nodes", "metric": { "name": "tikv_increase_of_storage_usage", "unit": "Bps", "description": "Shows the rate of storage usage increase in TiKV instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_increase_of_storage_usage", "promql": "rate(tikv_store_size_bytes{type=\"used\",@LABEL}[5m])", "promMetric": "tikv_store_size_bytes", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 6, "displayName": "TiFlash Increase of Storage Usage", "name": "tiflash_increase_of_storage_usage", "description": "Rate of storage space consumption increase in TiFlash nodes", "metric": { "name": "tiflash_increase_of_storage_usage", "unit": "Bps", "description": "Shows the rate of storage usage increase in TiFlash instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_increase_of_storage_usage", "promql": "rate(tiflash_system_current_metric_StoreSizeUsed{@LABEL}[5m])", "promMetric": "tiflash_system_current_metric_StoreSizeUsed", "labels": [ "instance" ], "type": "tiflash", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } } ] } ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/metrics-config-cluster.json ================================================ { "metrics": [ { "class": "cluster", "group": "advanced", "type": "optimizer_behavior", "order": 1, "displayName": "Parse Duration", "name": "parse_duration", "description": "Time taken by TiDB to parse SQL statements into abstract syntax trees (AST)", "metric": { "name": "parse_duration", "unit": "s", "description": "Shows average and percentile parse durations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p95_tidb_session_parse_duration", "promql": "histogram_quantile(0.95, sum(rate(tidb_session_parse_duration_seconds_bucket{sql_type=\"general\",@LABEL}[1m])) by (le))", "promMetric": "tidb_session_parse_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "95", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_session_parse_duration", "promql": "sum(rate(tidb_session_parse_duration_seconds_sum{sql_type=\"general\",@LABEL}[1m]))", "promMetric": "tidb_session_parse_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tidb_session_parse_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_session_parse_duration_seconds_bucket{sql_type=\"general\",@LABEL}[1m])) by (le))", "promMetric": "tidb_session_parse_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "sql_tuning", "order": 1, "displayName": "Slow query", "name": "slow_query", "description": "Statistics about slow query execution, including processing, coprocessor, and wait durations", "metric": { "name": "slow_query", "unit": "s", "description": "90th percentile of slow query durations, broken down by processing phases and SQL types", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p90_slow_query_prcess_duration", "promql": "histogram_quantile(0.90, sum(rate(tidb_server_slow_query_process_duration_seconds_bucket{@LABEL}[1m])) by (le,sql_type))", "promMetric": "tidb_server_slow_query_process_duration_seconds", "labels": [ "instance", "sql_type" ], "type": "tidb", "legend": "all_proc_{sqlType}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p90_slow_query_cop_duration", "promql": "histogram_quantile(0.90, sum(rate(tidb_server_slow_query_cop_duration_seconds_bucket{@LABEL}[1m])) by (le,sql_type))", "promMetric": "tidb_server_slow_query_cop_duration_seconds", "labels": [ "instance", "sql_type" ], "type": "tidb", "legend": "all_cop_proc_{sqlType}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p90_slow_query_wait_duration", "promql": "histogram_quantile(0.90, sum(rate(tidb_server_slow_query_wait_duration_seconds_bucket{@LABEL}[1m])) by (le,sql_type))", "promMetric": "tidb_server_slow_query_wait_duration_seconds", "labels": [ "instance", "sql_type" ], "type": "tidb", "legend": "all_cop_wait_{sqlType}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "io_env", "order": 1, "displayName": "Disk Latency", "name": "disk_latency", "description": "Average time taken for read and write I/O operations to complete", "metric": { "name": "disk_latency", "unit": "s", "description": "Average time taken for read and write I/O operations to complete", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_io_write_latency", "promql": "(rate(node_disk_write_time_seconds_total{@LABEL}[5m])/ rate(node_disk_writes_completed_total{@LABEL}[5m]))", "promMetric": "node_disk_write_time_seconds_total", "labels": [ "instance" ], "type": "host", "legend": "Write Latency: [{instance}-{device}]", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "host_io_read_latency", "promql": "(rate(node_disk_read_time_seconds_total{@LABEL}[5m])/ rate(node_disk_reads_completed_total{@LABEL}[5m]))", "promMetric": "node_disk_read_time_seconds_total", "labels": [ "instance" ], "type": "host", "legend": "Read Latency: [{instance}-{device}]", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "load_analysis", "order": 1, "displayName": " QPS", "name": "qps", "description": "Queries Per Second (QPS) measures the number of SQL queries processed by TiDB per second, indicating the overall system workload", "metric": { "name": "qps", "unit": "short", "description": "Shows total QPS, QPS by type, and failed query rates", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "qps_by_type", "promql": "sum(rate(tidb_executor_statement_total{@LABEL}[1m])) by (type)", "promMetric": "tidb_executor_statement_total", "labels": [ "instance", "type" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "qps", "promql": "sum(rate(tidb_executor_statement_total{@LABEL}[1m]))", "promMetric": "tidb_executor_statement", "labels": [ "instance" ], "type": "tidb", "legend": "Total", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "qps_error", "promql": "sum(rate(tidb_server_execute_error_total{@LABEL}[1m])) ", "promMetric": "tidb_server_execute_error_total", "labels": [ "instance" ], "type": "tidb", "legend": "Failed", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "tiflash_related", "order": 1, "displayName": "TiFlash CPU Usage", "name": "tiflash_cpu_usage", "description": "CPU utilization percentage of TiFlash instances, indicating computational resource consumption for analytical processing", "metric": { "name": "tiflash_cpu_usage", "unit": "percentunit", "description": "Shows CPU usage percentage and core limits for TiFlash instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_cpu_usage_percentage", "promql": "rate(tiflash_proxy_process_cpu_seconds_total{job=\"tiflash\",@LABEL}[1m])", "promMetric": "tiflash_proxy_process_cpu_seconds_total", "labels": [ "instance" ], "type": "tiflash", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tiflash_logic_cpu_cores", "promql": "sum(tiflash_system_current_metric_LogicalCPUCores{@LABEL}) by (instance)", "promMetric": "tiflash_system_current_metric_LogicalCPUCores", "labels": [ "instance" ], "type": "tiflash", "legend": "limit-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "connection", "order": 1, "displayName": "Connection Count", "name": "connection_count", "description": "Number of active connections to TiDB", "metric": { "name": "connection_count", "unit": "short", "description": "Tracks the number of active database connections, helping monitor client connection patterns and resource usage", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "connection_count", "promql": "tidb_server_connections{@LABEL}", "promMetric": "tidb_server_connections", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "connection_count_sum", "promql": "sum(tidb_server_connections{@LABEL})", "promMetric": "tidb_server_connections", "labels": [ "instance" ], "type": "tidb", "legend": "total", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "pd_leader", "order": 1, "displayName": "Region health", "name": "pd_region_health", "description": "Health status of TiKV regions, including normal and abnormal states", "metric": { "name": "pd_region_health", "unit": "short", "description": "Monitors the health status of regions across the TiKV cluster", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_regions_status", "promql": "sum(pd_regions_status{@LABEL}) by (instance, type)", "promMetric": "pd_regions_status", "labels": [ "instance", "type" ], "type": "pd", "legend": "{instance}-{type}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_regions_offline_status", "promql": "pd_regions_offline_status{type=\"offline-peer-region-count\",@LABEL}", "promMetric": "pd_regions_offline_status", "labels": [ "instance", "type" ], "type": "pd", "legend": "{instance}-{type}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "raft_log", "order": 1, "displayName": "Append Log Duration", "name": "append_log_duration", "description": "Time taken to append a raft log at different percentiles", "metric": { "name": "append_log_duration", "unit": "s", "description": "Shows average and 99th percentile durations for appending Raft logs", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_raftstore_append_log_duration", "promql": "(sum(rate(tikv_raftstore_append_log_duration_seconds_sum{@LABEL}[1m])) / sum(rate(tikv_raftstore_append_log_duration_seconds_count{@LABEL}[1m])))", "promMetric": "tikv_raftstore_append_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_raftstore_append_log_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_append_log_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_append_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "analyze_statistics", "order": 1, "displayName": "Auto Analyze Queries Per Minute", "name": "auto_analyze_queries_per_minute", "description": "Rate of automatic table statistics analysis operations per minute", "metric": { "name": "auto_analyze_queries_per_minute", "unit": "short", "description": "Frequency of automatic statistics collection operations by type", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "auto_analyze_queries_per_minute", "promql": "sum(increase(tidb_statistics_auto_analyze_total{@LABEL}[1m])) by (type)", "promMetric": "tidb_statistics_auto_analyze_total", "labels": [ "instance" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "sql_tuning", "order": 2, "displayName": "999 Duration", "name": "999_duration", "description": "99.9th percentile of query duration, indicating worst-case query performance", "metric": { "name": "999_duration", "unit": "s", "description": "Tracks the 99.9th percentile of query execution times by SQL type", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p999_query_duration", "promql": "histogram_quantile(0.999, sum(rate(tidb_server_handle_query_duration_seconds_bucket{@LABEL}[1m])) by (le,sql_type))", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "{sqlType}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "tiflash_related", "order": 2, "displayName": "TiFlash Memory", "name": "tiflash_memory_usage", "description": "Memory usage of TiFlash instances, showing RAM consumption for columnar storage and processing", "metric": { "name": "tiflash_memory_usage", "unit": "bytes", "description": "Shows memory usage and limits for TiFlash instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_memory_usage", "promql": "tiflash_proxy_process_resident_memory_bytes{job=\"tiflash\",@LABEL}", "promMetric": "tiflash_proxy_process_resident_memory_bytes", "labels": [ "instance" ], "type": "tiflash", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tiflash_memory_limit", "promql": "sum(tiflash_system_current_metric_MemoryCapacity{@LABEL}) by (instance)", "promMetric": "tiflash_system_current_metric_MemoryCapacity", "labels": [ "instance" ], "type": "tiflash", "legend": "limit-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "io_env", "order": 2, "displayName": "Ping Latency", "name": "host_ping_latency", "description": "Time taken to ping a host", "metric": { "name": "host_ping_latency", "unit": "s", "description": "Time taken to ping a host", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_ping_latency", "promql": "probe_duration_seconds{job=~\".*blackbox_exporter.*\",@LABEL}", "promMetric": "probe_duration_seconds", "labels": [ "instance" ], "type": "host", "legend": "instance: {instance}, ping: {ping}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "raft_log", "order": 2, "displayName": "Commit Log Duration", "name": "commit_log_duration", "description": "Time taken to commit a raft log at different percentiles", "metric": { "name": "commit_log_duration", "unit": "s", "description": "Shows average and 99th percentile durations for committing Raft logs", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_raftstore_commit_log_duration", "promql": "(sum(rate(tikv_raftstore_commit_log_duration_seconds_sum{@LABEL}[1m])) / sum(rate(tikv_raftstore_commit_log_duration_seconds_count{@LABEL}[1m])))", "promMetric": "tikv_raftstore_commit_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_raftstore_commit_log_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_commit_log_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_commit_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "connection", "order": 2, "displayName": "Events OPM", "name": "events_opm", "description": "Number of TiDB server events per minute", "metric": { "name": "events_opm", "unit": "short", "description": "Events per minute by instance and type", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "events_opm", "promql": "increase(tidb_server_event_total{@LABEL}[10m])", "promMetric": "tidb_server_event_total", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}-server {type}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "load_analysis", "order": 2, "displayName": "Database time by SQL Type", "name": "database_time_by_sql_type", "description": "Time spent by different types of SQL statements in the database, helping identify which SQL types consume the most resources", "metric": { "name": "database_time_by_sql_type", "unit": "s", "description": "Shows the database time consumed by different types of SQL statements", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_handle_query_duration", "promql": "sum(rate(tidb_server_handle_query_duration_seconds_sum{ sql_type!=\"internal\",@LABEL}[1m])) by (sql_type)", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "sql_type", "instance" ], "type": "tidb", "legend": "{sqlType}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_tokens", "promql": "sum(tidb_server_tokens{@LABEL})", "promMetric": "tidb_server_tokens", "labels": [ "instance" ], "type": "tidb", "legend": "database time", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "pd_leader", "order": 2, "displayName": "Storage used", "name": "pd_storage_usage", "description": "Storage space utilization across the TiKV cluster", "metric": { "name": "pd_storage_usage", "unit": "percentunit", "description": "Percentage of total storage capacity currently in use", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_storage_usage", "promql": "(sum(pd_cluster_status{type=\"storage_size\",@LABEL}) by (instance))/ (sum(pd_cluster_status{type=\"storage_capacity\",@LABEL}) by (instance))", "promMetric": "pd_cluster_status", "labels": [ "instance" ], "type": "pd", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "analyze_statistics", "order": 2, "displayName": "Sync Load QPS", "name": "sync_load_qps", "description": "Rate of sync-load operations per second", "metric": { "name": "sync_load_qps", "unit": "short", "description": "Rate of sync-load operations per second", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "sync_load_qps_total", "promql": "sum(increase(tidb_statistics_sync_load_total{@LABEL}[1m])) by (type)", "promMetric": "tidb_statistics_sync_load_total", "labels": [ "instance" ], "type": "tidb", "legend": "total sync-load", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "sync_load_timeout", "promql": "sum(increase(tidb_statistics_sync_load_timeout_total{@LABEL}[1m])) by (type)", "promMetric": "tidb_statistics_sync_load_timeout_total", "labels": [ "instance" ], "type": "tidb", "legend": "timeout", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "sync_load_qps_dedup", "promql": "sum(increase(tidb_statistics_sync_load_dedup_total{@LABEL}[1m])) by (type)", "promMetric": "tidb_statistics_sync_load_dedup_total", "labels": [ "instance" ], "type": "tidb", "legend": "dedup sync-load", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "optimizer_behavior", "order": 2, "displayName": "Compile Duration", "name": "compile_duration", "description": "Time taken by TiDB to compile SQL statements into execution plans", "metric": { "name": "compile_duration", "unit": "s", "description": "Shows average and percentile compile durations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "avg_tidb_session_compile_duration", "promql": "(sum(rate(tidb_session_compile_duration_seconds_sum{sql_type=\"general\",@LABEL}[1m])) / sum(rate(tidb_session_compile_duration_seconds_count{sql_type=\"general\",@LABEL}[1m])))", "promMetric": "tidb_session_compile_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tidb_session_compile_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_session_compile_duration_seconds_bucket{sql_type=\"general\",@LABEL}[1m])) by (le))", "promMetric": "tidb_session_compile_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "io_env", "order": 3, "displayName": "TiKV Write Pipeline Duration", "name": "tikv_write_pipeline_duration", "description": "Duration metrics for TiKV write operations pipeline", "metric": { "name": "tikv_write_pipeline_duration", "unit": "s", "description": "Various duration metrics in the TiKV write pipeline process", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p99_tikv_write_raft_log_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_append_log_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_append_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "Write Raft Log .99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_propose_wait_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_request_wait_time_duration_secs_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_request_wait_time_duration_secs", "labels": [ "instance" ], "type": "tikv", "legend": "Propose Wait .99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_apply_wait_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_apply_wait_time_duration_secs_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_apply_wait_time_duration_secs", "labels": [ "instance" ], "type": "tikv", "legend": "Apply Wait .99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_replicate_raft_log_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_commit_log_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_commit_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "Replicate Raft Log .99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_apply_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_apply_log_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_apply_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "Apply Duration .99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "tiflash_related", "order": 3, "displayName": "TiFlash Request QPS", "name": "tiflash_request_qps", "description": "Number of requests processed by TiFlash coprocessor per second", "metric": { "name": "tiflash_request_qps", "unit": "short", "description": "Query processing rate in TiFlash, broken down by request type", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_request_qps", "promql": "sum(rate(tiflash_coprocessor_request_count{@LABEL}[1m])) by (type)", "promMetric": "tiflash_coprocessor_request_count", "labels": [ "instance" ], "type": "tiflash", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "pd_leader", "order": 3, "displayName": "Number of Regions", "name": "pd_number_of_regions", "description": "Total number of regions in the TiKV cluster", "metric": { "name": "pd_number_of_regions", "unit": "short", "description": "Count of total regions managed by PD", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_number_of_regions", "promql": "sum(pd_cluster_status{type=\"leader_count\",@LABEL})", "promMetric": "pd_cluster_status", "labels": [ "instance" ], "type": "pd", "legend": "count", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "load_analysis", "order": 3, "displayName": "TiDB CPU", "name": "tidb_cpu_usage", "description": "CPU utilization percentage of TiDB servers, indicating computational resource consumption", "metric": { "name": "tidb_cpu_usage", "unit": "percentunit", "description": "Shows CPU usage percentage and MAXPROCS limit for TiDB instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_cpu_usage_percentage", "promql": "irate(process_cpu_seconds_total{ job=\"tidb\",@LABEL}[1m])", "promMetric": "process_cpu_seconds_total", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_server_maxprocs", "promql": "tidb_server_maxprocs{job=\"tidb\",@LABEL}", "promMetric": "tidb_server_maxprocs", "labels": [ "instance" ], "type": "tidb", "legend": "limit-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "connection", "order": 3, "displayName": "Connection Idle Duration", "name": "connection_idle_duration", "description": "Duration of idle connections in TiDB", "metric": { "name": "connection_idle_duration", "unit": "s", "description": "Various percentiles of connection idle time for both transaction and non-transaction states", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p99_tidb_in_txn_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_server_conn_idle_duration_seconds_bucket{ in_txn='1',@LABEL}[1m])) by (le,in_txn))", "promMetric": "tidb_server_conn_idle_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "99-in-txn", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tidb_not_in_txn_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_server_conn_idle_duration_seconds_bucket{ in_txn='0',@LABEL}[1m])) by (le,in_txn))", "promMetric": "tidb_server_conn_idle_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "99-not-in-txn", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p90_tidb_in_txn_duration", "promql": "histogram_quantile(0.90, sum(rate(tidb_server_conn_idle_duration_seconds_bucket{ in_txn='1',@LABEL}[1m])) by (le,in_txn))", "promMetric": "tidb_server_conn_idle_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "90-in-txn", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p90_tidb_not_in_txn_duration", "promql": "histogram_quantile(0.90, sum(rate(tidb_server_conn_idle_duration_seconds_bucket{ in_txn='0',@LABEL}[1m])) by (le,in_txn))", "promMetric": "tidb_server_conn_idle_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "90-not-in-txn", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p80_tidb_in_txn_duration", "promql": "histogram_quantile(0.80, sum(rate(tidb_server_conn_idle_duration_seconds_bucket{ in_txn='1',@LABEL}[1m])) by (le,in_txn))", "promMetric": "tidb_server_conn_idle_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "80-in-txn", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p80_tidb_not_in_txn_duration", "promql": "histogram_quantile(0.80, sum(rate(tidb_server_conn_idle_duration_seconds_bucket{ in_txn='0',@LABEL}[1m])) by (le,in_txn))", "promMetric": "tidb_server_conn_idle_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "80-not-in-txn", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "optimizer_behavior", "order": 3, "displayName": "Queries Using Plan Cache OPS", "name": "queries_using_plan_cache_ops", "description": "Number of queries per second that utilize the execution plan cache", "metric": { "name": "queries_using_plan_cache_ops", "unit": "short", "description": "Shows plan cache hit and miss rates", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "queries_hit_plan_cache", "promql": "sum(rate(tidb_server_plan_cache_total{@LABEL}[1m]))", "promMetric": "tidb_server_plan_cache_total", "labels": [ "instance" ], "type": "tidb", "legend": "avg_hit", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "queries_miss_plan_cache", "promql": "sum(rate(tidb_server_plan_cache_miss_total{@LABEL}[1m]))", "promMetric": "tidb_server_plan_cache_miss_total", "labels": [ "instance" ], "type": "tidb", "legend": "avg_miss", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "analyze_statistics", "order": 3, "displayName": "Stats Healthy Distribution", "name": "stats_healthy_distribution", "description": "Distribution of healthy statistics across the cluster", "metric": { "name": "stats_healthy_distribution", "unit": "short", "description": "Distribution of healthy statistics across the cluster", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "stats_healthy_distribution", "promql": "avg(tidb_statistics_stats_healthy{@LABEL}) by (type)", "promMetric": "tidb_statistics_stats_healthy", "labels": [ "instance" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "raft_log", "order": 3, "displayName": "Apply Log Duration", "name": "apply_log_duration", "description": "Time taken to apply a raft log at different percentiles", "metric": { "name": "apply_log_duration", "unit": "s", "description": "Shows average and 99th percentile durations for applying Raft logs", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_raftstore_apply_log_duration", "promql": "(sum(rate(tikv_raftstore_apply_log_duration_seconds_sum{@LABEL}[1m])) / sum(rate(tikv_raftstore_apply_log_duration_seconds_count{@LABEL}[1m])))", "promMetric": "tikv_raftstore_apply_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_raftstore_apply_log_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_apply_log_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_apply_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "sql_tuning", "order": 3, "displayName": "Expensive Executors OPS", "name": "expensive_executors_ops", "description": "Operations per second for resource-intensive query executors", "metric": { "name": "expensive_executors_ops", "unit": "short", "description": "Monitors the rate of expensive query execution operations by executor type", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "expensive_executors_ops_sum_by_type", "promql": "sum(rate(tidb_executor_expensive_total{@LABEL}[1m])) by (type)", "promMetric": "tidb_executor_expensive_total", "labels": [ "instance" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "analyze_statistics", "order": 4, "displayName": "Auto Analyze Duration", "name": "auto_analyze_duration", "description": "Time taken to perform automatic table statistics analysis at different percentiles", "metric": { "name": "auto_analyze_duration", "unit": "s", "description": "Time taken to perform automatic table statistics analysis at different percentiles", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p95_auto_analyze_duration", "promql": "histogram_quantile(0.95, sum(rate(tidb_statistics_auto_analyze_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tidb_statistics_auto_analyze_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "95", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p80_auto_analyze_duration", "promql": "histogram_quantile(0.80, sum(rate(tidb_statistics_auto_analyze_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tidb_statistics_auto_analyze_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "80", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "pd_leader", "order": 4, "displayName": "CPU usage", "name": "pd_cpu_usage_percentage", "description": "CPU utilization of PD leader instance, showing processing overhead of cluster management", "metric": { "name": "pd_cpu_usage_percentage", "unit": "percentunit", "description": "Shows CPU usage percentage for PD instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_cpu_usage_percentage", "promql": "irate(process_cpu_seconds_total{job=~\".*pd.*\",@LABEL}[30s])", "promMetric": "process_cpu_seconds_total", "labels": [ "instance" ], "type": "pd", "legend": "{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "load_analysis", "order": 4, "displayName": "TiDB Memory", "name": "tidb_memory_usage", "description": "Memory usage of TiDB servers, showing the amount of RAM being consumed", "metric": { "name": "tidb_memory_usage", "unit": "bytes", "description": "Shows detailed memory usage metrics for TiDB instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_process_memory_usage", "promql": "process_resident_memory_bytes{ job=\"tidb\",@LABEL}", "promMetric": "process_resident_memory_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "process-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_heap_sys_memory_usage", "promql": "go_memory_classes_heap_objects_bytes{ job=\"tidb\",@LABEL} + go_memory_classes_heap_unused_bytes{ job=\"tidb\",@LABEL} + go_memory_classes_heap_released_bytes{ job=\"tidb\",@LABEL} + go_memory_classes_heap_free_bytes{ job=\"tidb\",@LABEL}", "promMetric": "go_memory_classes_heap_objects_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "HeapSys-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_heap_inuse_memory_usage", "promql": "go_memory_classes_heap_objects_bytes{ job=\"tidb\",@LABEL} + go_memory_classes_heap_unused_bytes{ job=\"tidb\",@LABEL}", "promMetric": "go_memory_classes_heap_objects_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "HeapInuse-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_heap_alloc_memory_usage", "promql": "go_memory_classes_heap_objects_bytes{ job=\"tidb\",@LABEL}", "promMetric": "go_memory_classes_heap_objects_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "HeapAlloc-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_heap_idle_memory_usage", "promql": "go_memory_classes_heap_released_bytes{job=\"tidb\",@LABEL} + go_memory_classes_heap_free_bytes{ job=\"tidb\",@LABEL}", "promMetric": "go_memory_classes_heap_released_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "HeapIdle-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_heap_released_memory_usage", "promql": "go_memory_classes_heap_released_bytes{job=\"tidb\",@LABEL}", "promMetric": "go_memory_classes_heap_released_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "HeapReleased-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_gc_trigger_memory_usage", "promql": "go_gc_heap_goal_bytes{job=\"tidb\",@LABEL}", "promMetric": "go_gc_heap_goal_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "GCTrigger-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_server_memory_usage", "promql": "tidb_server_memory_usage{job=\"tidb\",@LABEL}", "promMetric": "tidb_server_memory_usage", "labels": [ "instance" ], "type": "tidb", "legend": "{module}-{type}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_server_memory_usage_by_module", "promql": "sum(tidb_server_memory_usage{job=\"tidb\",@LABEL}) by (module, instance)", "promMetric": "tidb_server_memory_usage", "labels": [ "instance" ], "type": "tidb", "legend": "{module}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "io_env", "order": 4, "displayName": "TiKV Cop Read Duration", "name": "tikv_cop_read_duration", "description": "Duration metrics for TiKV coprocessor read operations", "metric": { "name": "tikv_cop_read_duration", "unit": "s", "description": "Duration metrics for TiKV coprocessor read operations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p99_tikv_get_snapshot_duration", "promql": "histogram_quantile(0.99, sum(delta(tikv_storage_engine_async_request_duration_seconds_bucket{type=\"snapshot\",@LABEL}[1m])) by (le))", "promMetric": "tikv_storage_engine_async_request_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "Get Snapshot .99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_cop_wait_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_coprocessor_request_wait_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_coprocessor_request_wait_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "Cop Wait .99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_cop_handle_duration", "promql": "histogram_quantile(0.95, sum(rate(tikv_coprocessor_request_handle_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_coprocessor_request_handle_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "Cop Handle .99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "tiflash_related", "order": 4, "displayName": "TiFlash Request Duration", "name": "tiflash_request_duration", "description": "Time taken to process requests in TiFlash coprocessor at different percentiles", "metric": { "name": "tiflash_request_duration", "unit": "s", "description": "Distribution of request processing times in TiFlash, showing performance at various percentiles", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p999_tiflash_request_duration", "promql": "histogram_quantile(0.999, sum(rate(tiflash_coprocessor_request_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tiflash_coprocessor_request_duration_seconds", "labels": [ "instance" ], "type": "tiflash", "legend": "999", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tiflash_request_duration", "promql": "histogram_quantile(0.99, sum(rate(tiflash_coprocessor_request_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tiflash_coprocessor_request_duration_seconds", "labels": [ "instance" ], "type": "tiflash", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p95_tiflash_request_duration", "promql": "histogram_quantile(0.95, sum(rate(tiflash_coprocessor_request_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tiflash_coprocessor_request_duration_seconds", "labels": [ "instance" ], "type": "tiflash", "legend": "95", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p80_tiflash_request_duration", "promql": "histogram_quantile(0.80, sum(rate(tiflash_coprocessor_request_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tiflash_coprocessor_request_duration_seconds", "labels": [ "instance" ], "type": "tiflash", "legend": "80", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "load_analysis", "order": 5, "displayName": "TiKV CPU", "name": "tikv_cpu_usage", "description": "CPU utilization percentage of TiKV storage nodes, indicating storage layer computational load", "metric": { "name": "tikv_cpu_usage", "unit": "percentunit", "description": "Shows CPU usage percentage for TiKV instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_cpu_usage_percentage", "promql": "sum(rate(process_cpu_seconds_total{ job=~\".*tikv\",@LABEL}[1m])) by (instance)", "promMetric": "process_cpu_seconds_total", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "pd_leader", "order": 5, "displayName": "Memory Usage", "name": "pd_memory_usage", "description": "Memory consumption of PD leader instance, indicating resource usage for cluster management", "metric": { "name": "pd_memory_usage", "unit": "bytes", "description": "Shows detailed memory usage metrics for PD instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_memory_usage", "promql": "process_resident_memory_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "process_resident_memory_bytes", "labels": [ "instance" ], "type": "pd", "legend": "process-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_sys_memory_usage", "promql": "go_memstats_heap_sys_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_sys_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapSys-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_inuse_memory_usage", "promql": "go_memstats_heap_inuse_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_inuse_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapInuse-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_alloc_memory_usage", "promql": "go_memstats_heap_alloc_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_alloc_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapAlloc-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_idle_memory_usage", "promql": "go_memstats_heap_idle_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_idle_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapIdle-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_released_memory_usage", "promql": "go_memstats_heap_released_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_released_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapReleased-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_gc_trigger_memory_usage", "promql": "go_memstats_next_gc_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_next_gc_bytes", "labels": [ "instance" ], "type": "pd", "legend": "GCTrigger-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "load_analysis", "order": 6, "displayName": "TiKV Memory", "name": "tikv_memory_usage", "description": "Memory usage of TiKV storage nodes, showing storage layer RAM consumption", "metric": { "name": "tikv_memory_usage", "unit": "bytes", "description": "Shows memory usage for TiKV instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_memory_usage", "promql": "avg(process_resident_memory_bytes{job=~\".*tikv\",@LABEL}) by (instance)", "promMetric": "process_resident_memory_bytes", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "pd_leader", "order": 6, "displayName": "Goroutine Count", "name": "pd_go_routine_count", "description": "Number of active goroutines in PD processes", "metric": { "name": "pd_go_routine_count", "unit": "short", "description": "Current count of active goroutines in PD instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_go_routine_count", "promql": "go_goroutines{job=~\".*pd.*\",@LABEL}", "promMetric": "go_goroutines", "labels": [ "instance" ], "type": "pd", "legend": "{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "pd_leader", "order": 7, "displayName": "Schedule operator create", "name": "pd_schedule_operator_create", "description": "Rate of schedule operator creation by PD", "metric": { "name": "pd_schedule_operator_create", "unit": "opm", "description": "Monitors the creation rate of different types of scheduling operators", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_schedule_operator_create", "promql": "sum(delta(pd_schedule_operators_count{event=\"create\",@LABEL}[1m])) by (type)", "promMetric": "pd_schedule_operators_count", "labels": [ "instance" ], "type": "pd", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "load_analysis", "order": 7, "displayName": "PD CPU", "name": "pd_cpu_usage_percentage", "description": "CPU utilization percentage of PD (Placement Driver) servers, indicating cluster management overhead", "metric": { "name": "pd_cpu_usage_percentage", "unit": "percentunit", "description": "Shows CPU usage percentage for PD instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_cpu_usage_percentage", "promql": "irate(process_cpu_seconds_total{job=~\".*pd.*\",@LABEL}[30s])", "promMetric": "process_cpu_seconds_total", "labels": [ "instance" ], "type": "pd", "legend": "{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "pd_leader", "order": 8, "displayName": "Scheduler is running", "name": "pd_scheduler_is_running", "description": "Status of different PD schedulers", "metric": { "name": "pd_scheduler_is_running", "unit": "short", "description": "Indicates whether specific PD schedulers are currently active", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_scheduler_is_running", "promql": "pd_scheduler_status{ type=\"allow\",@LABEL}", "promMetric": "pd_scheduler_status", "labels": [ "instance" ], "type": "pd", "legend": "{kind}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "load_analysis", "order": 8, "displayName": "PD Memory", "name": "pd_memory_usage", "description": "Memory usage of PD (Placement Driver) servers, showing cluster management resource consumption", "metric": { "name": "pd_memory_usage", "unit": "bytes", "description": "Shows detailed memory usage metrics for PD instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_memory_usage", "promql": "process_resident_memory_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "process_resident_memory_bytes", "labels": [ "instance" ], "type": "pd", "legend": "process-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_sys_memory_usage", "promql": "go_memstats_heap_sys_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_sys_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapSys-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_inuse_memory_usage", "promql": "go_memstats_heap_inuse_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_inuse_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapInuse-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_alloc_memory_usage", "promql": "go_memstats_heap_alloc_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_alloc_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapAlloc-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_idle_memory_usage", "promql": "go_memstats_heap_idle_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_idle_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapIdle-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_released_memory_usage", "promql": "go_memstats_heap_released_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_released_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapReleased-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_gc_trigger_memory_usage", "promql": "go_memstats_next_gc_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_next_gc_bytes", "labels": [ "instance" ], "type": "pd", "legend": "GCTrigger-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "load_analysis", "order": 9, "displayName": "Connection Count", "name": "connection_count", "description": "Number of active client connections to the TiDB cluster, both per instance and total", "metric": { "name": "connection_count", "unit": "short", "description": "Tracks the number of active database connections, helping monitor client connection patterns and resource usage", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "connection_count", "promql": "tidb_server_connections{@LABEL}", "promMetric": "tidb_server_connections", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "connection_count_sum", "promql": "sum(tidb_server_connections{@LABEL})", "promMetric": "tidb_server_connections", "labels": [ "instance" ], "type": "tidb", "legend": "total", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "advanced", "type": "pd_leader", "order": 9, "displayName": "Patrol Region time", "name": "pd_patrol_region_time", "description": "Time taken by PD to patrol and check region health", "metric": { "name": "pd_patrol_region_time", "unit": "s", "description": "Duration of region patrol operations by PD checker", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_patrol_region_time", "promql": "pd_checker_patrol_regions_time{@LABEL}", "promMetric": "pd_checker_patrol_regions_time", "labels": [ "instance" ], "type": "pd", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 1, "displayName": "Database Time by SQL Type", "name": "database_time_by_sql_type", "description": "Distribution of database time by SQL type", "metric": { "name": "database_time_by_sql_type", "unit": "s", "description": "Shows the database time consumed by different types of SQL statements", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_handle_query_duration", "promql": "sum(rate(tidb_server_handle_query_duration_seconds_sum{ sql_type!=\"internal\",@LABEL}[1m])) by (sql_type)", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "sql_type", "instance" ], "type": "tidb", "legend": "{sqlType}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_tokens", "promql": "sum(tidb_server_tokens{@LABEL})", "promMetric": "tidb_server_tokens", "labels": [ "instance" ], "type": "tidb", "legend": "database time", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "throughput", "order": 1, "displayName": "QPS", "name": "qps", "description": "Overview of queries per second metrics", "metric": { "name": "qps", "unit": "short", "description": "Shows total QPS, QPS by type, and failed query rates", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "qps_by_type", "promql": "sum(rate(tidb_executor_statement_total{@LABEL}[1m])) by (type)", "promMetric": "tidb_executor_statement_total", "labels": [ "instance", "type" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "qps", "promql": "sum(rate(tidb_executor_statement_total{@LABEL}[1m]))", "promMetric": "tidb_executor_statement", "labels": [ "instance" ], "type": "tidb", "legend": "Total", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "qps_error", "promql": "sum(rate(tidb_server_execute_error_total{@LABEL}[1m])) ", "promMetric": "tidb_server_execute_error_total", "labels": [ "instance" ], "type": "tidb", "legend": "Failed", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "transaction", "order": 1, "displayName": "Transaction OPS", "name": "transaction_ops_by_type_and_txn_mode", "description": "Overview of transaction operations by type and transaction mode", "metric": { "name": "transaction_ops_by_type_and_txn_mode", "unit": "short", "description": "Shows transaction rates categorized by type and transaction mode", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tps_by_type_and_txn_mode", "promql": "sum(rate(tidb_session_transaction_duration_seconds_count{@LABEL}[1m])) by (type, txn_mode)", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "type", "txn_mode" ], "type": "tidb", "legend": "{type}-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "raft_log", "order": 1, "displayName": "Append Log Duration", "name": "append_log_duration", "description": "Overview of Raft log append duration metrics", "metric": { "name": "append_log_duration", "unit": "s", "description": "Shows average and 99th percentile durations for appending Raft logs", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_raftstore_append_log_duration", "promql": "(sum(rate(tikv_raftstore_append_log_duration_seconds_sum{@LABEL}[1m])) / sum(rate(tikv_raftstore_append_log_duration_seconds_count{@LABEL}[1m])))", "promMetric": "tikv_raftstore_append_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_raftstore_append_log_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_append_log_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_append_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 2, "displayName": "Database Time by SQL Phase", "name": "database_time_by_sql_phase", "description": "Distribution of database time by SQL execution phase", "metric": { "name": "database_time_by_sql_phase", "unit": "s", "description": "Shows the database time consumed by each phase of SQL execution", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_tokens", "promql": "sum(tidb_server_tokens{@LABEL})", "promMetric": "tidb_server_tokens", "labels": [ "instance" ], "type": "tidb", "legend": "database time", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_session_parse_duration", "promql": "sum(rate(tidb_session_parse_duration_seconds_sum{sql_type=\"general\",@LABEL}[1m]))", "promMetric": "tidb_session_parse_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "parse", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_session_compile_duration", "promql": "sum(rate(tidb_session_compile_duration_seconds_sum{sql_type=\"general\",@LABEL}[1m]))", "promMetric": "tidb_session_compile_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "compile", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_session_execute_duration", "promql": "sum(rate(tidb_session_execute_duration_seconds_sum{sql_type=\"general\",@LABEL}[1m]))", "promMetric": "tidb_session_execute_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "execute", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_server_get_token_duration", "promql": "sum(rate(tidb_server_get_token_duration_seconds_sum{@LABEL}[1m]))/1000000", "promMetric": "tidb_server_get_token_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "get token", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "transaction", "order": 2, "displayName": "Duration", "name": "transaction_duration", "description": "Overview of transaction duration metrics", "metric": { "name": "transaction_duration", "unit": "s", "description": "Shows transaction duration percentiles by transaction mode", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p99_transaction_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_session_transaction_duration_seconds_bucket{@LABEL}[1m])) by (le, txn_mode))", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "txn_mode" ], "type": "tidb", "legend": "99-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p95_transaction_duration", "promql": "histogram_quantile(0.95, sum(rate(tidb_session_transaction_duration_seconds_bucket{@LABEL}[1m])) by (le, txn_mode))", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "txn_mode" ], "type": "tidb", "legend": "95-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p80_transaction_duration", "promql": "histogram_quantile(0.80, sum(rate(tidb_session_transaction_duration_seconds_bucket{@LABEL}[1m])) by (le, txn_mode))", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "txn_mode" ], "type": "tidb", "legend": "80-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "throughput", "order": 2, "displayName": "CPS By Type", "name": "cps_by_type", "description": "Overview of commands per second metrics by type", "metric": { "name": "cps_by_type", "unit": "short", "description": "Shows the rate of different types of commands being processed", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "cps_by_type", "promql": "sum(rate(tidb_server_query_total{@LABEL}[1m])) by (type)", "promMetric": "tidb_server_query_total", "labels": [ "instance", "type" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "raft_log", "order": 2, "displayName": "Commit Log Duration", "name": "commit_log_duration", "description": "Overview of Raft log commit duration metrics", "metric": { "name": "commit_log_duration", "unit": "s", "description": "Shows average and 99th percentile durations for committing Raft logs", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_raftstore_commit_log_duration", "promql": "(sum(rate(tikv_raftstore_commit_log_duration_seconds_sum{@LABEL}[1m])) / sum(rate(tikv_raftstore_commit_log_duration_seconds_count{@LABEL}[1m])))", "promMetric": "tikv_raftstore_commit_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_raftstore_commit_log_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_commit_log_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_commit_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 3, "displayName": "SQL Execute Time Overview", "name": "sql_execute_time_overview", "description": "Overview of SQL execution time", "metric": { "name": "sql_execute_time_overview", "unit": "s", "description": "Shows the time taken for various SQL execution components", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_tikvclient_request_seconds", "promql": "sum(rate(tidb_tikvclient_request_seconds_sum{store!=\"0\",@LABEL}[1m])) by (type)", "promMetric": "tidb_tikvclient_request_seconds", "labels": [ "instance", "type" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_client_cmd_handle_cmds_wait_duration", "promql": "sum(rate(pd_client_cmd_handle_cmds_duration_seconds_sum{type=\"wait\",@LABEL}[1m]))", "promMetric": "pd_client_cmd_handle_cmds_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "tso_wait", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_session_execute_duration", "promql": "sum(rate(tidb_session_execute_duration_seconds_sum{sql_type=\"general\",@LABEL}[1m]))", "promMetric": "tidb_session_execute_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "execute time", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tiflash_coprocessor_request_duration", "promql": "sum(rate(tiflash_coprocessor_request_duration_seconds_sum{@LABEL}[1m]))", "promMetric": "tiflash_coprocessor_request_duration_seconds", "labels": [ "" ], "type": "tiflash", "legend": "tiflash_mpp", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "raft_log", "order": 3, "displayName": "Apply Log Duration", "name": "apply_log_duration", "description": "Overview of Raft log apply duration metrics", "metric": { "name": "apply_log_duration", "unit": "s", "description": "Shows average and 99th percentile durations for applying Raft logs", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_raftstore_apply_log_duration", "promql": "(sum(rate(tikv_raftstore_apply_log_duration_seconds_sum{@LABEL}[1m])) / sum(rate(tikv_raftstore_apply_log_duration_seconds_count{@LABEL}[1m])))", "promMetric": "tikv_raftstore_apply_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_raftstore_apply_log_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_apply_log_duration_seconds_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_apply_log_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "throughput", "order": 3, "displayName": "Queries Using Plan Cache OPS", "name": "queries_using_plan_cache_ops", "description": "Overview of plan cache usage metrics", "metric": { "name": "queries_using_plan_cache_ops", "unit": "short", "description": "Shows plan cache hit and miss rates", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "queries_hit_plan_cache", "promql": "sum(rate(tidb_server_plan_cache_total{@LABEL}[1m]))", "promMetric": "tidb_server_plan_cache_total", "labels": [ "instance" ], "type": "tidb", "legend": "avg_hit", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "queries_miss_plan_cache", "promql": "sum(rate(tidb_server_plan_cache_miss_total{@LABEL}[1m]))", "promMetric": "tidb_server_plan_cache_miss_total", "labels": [ "instance" ], "type": "tidb", "legend": "avg_miss", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "transaction", "order": 3, "displayName": "Commit Token Wait Duration", "name": "commit_token_wait_duration", "description": "Overview of commit token wait duration metrics", "metric": { "name": "commit_token_wait_duration", "unit": "ns", "description": "Shows percentile measurements of time spent waiting for commit tokens", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p99_commit_token_wait_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_tikvclient_batch_executor_token_wait_duration_bucket{@LABEL}[1m])) by (le))", "promMetric": "tidb_tikvclient_batch_executor_token_wait_duration", "labels": [ "instance" ], "type": "tidb", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p95_commit_token_wait_duration", "promql": "histogram_quantile(0.95, sum(rate(tidb_tikvclient_batch_executor_token_wait_duration_bucket{@LABEL}[1m])) by (le))", "promMetric": "tidb_tikvclient_batch_executor_token_wait_duration", "labels": [ "instance" ], "type": "instance", "legend": "95", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p80_commit_token_wait_duration", "promql": "histogram_quantile(0.80, sum(rate(tidb_tikvclient_batch_executor_token_wait_duration_bucket{@LABEL}[1m])) by (le))", "promMetric": "tidb_tikvclient_batch_executor_token_wait_duration", "labels": [ "instance" ], "type": "instance", "legend": "80", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "transaction", "order": 4, "displayName": "KV Transaction OPS", "name": "kv_transaction_ops", "description": "Overview of Key-Value transaction operations", "metric": { "name": "kv_transaction_ops", "unit": "short", "description": "Shows the rate of Key-Value transaction operations by instance", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "kv_transaction_ops", "promql": "sum(rate(tidb_tikvclient_txn_cmd_duration_seconds_count{type=\"commit\",@LABEL}[1m])) by (instance)", "promMetric": "tidb_tikvclient_txn_cmd_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "throughput", "order": 4, "displayName": "KV/TSO Request OPS", "name": "kv_tso_request_ops", "description": "Overview of KV and TSO request operation metrics", "metric": { "name": "kv_tso_request_ops", "unit": "short", "description": "Shows rates of KV and TSO requests by type", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_tikvclient_request_ops_by_type", "promql": "sum(rate(tidb_tikvclient_request_seconds_count{@LABEL}[1m])) by (type)", "promMetric": "tidb_tikvclient_request_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_tikvclient_request_ops", "promql": "sum(rate(tidb_tikvclient_request_seconds_count{@LABEL}[1m]))", "promMetric": "tidb_tikvclient_request_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "kv request total", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_client_cmd_handle_cmds_tso_duration", "promql": "sum(rate(pd_client_cmd_handle_cmds_duration_seconds_count{type=\"tso\",@LABEL}[1m]))", "promMetric": "pd_client_cmd_handle_cmds_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "tso - cmd", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "kv_tso_request_ops_tso_request", "promql": "sum(rate(pd_client_request_handle_requests_duration_seconds_count{type=\"tso\",@LABEL}[1m]))", "promMetric": "pd_client_request_handle_requests_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "tso - request", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 4, "displayName": "Duration", "name": "duration", "description": "Overview of query duration metrics", "metric": { "name": "duration", "unit": "s", "description": "Shows average and percentile query durations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_query_duration", "promql": "sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!=\"internal\",@LABEL}[1m])) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!=\"internal\",@LABEL}[1m]))", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tidb_query_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{sql_type!=\"internal\",@LABEL}[1m])) by (le))", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_query_duration_by_sql_type", "promql": "sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!=\"internal\",@LABEL}[1m])) by (sql_type) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!=\"internal\",@LABEL}[1m])) by (sql_type)", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance", "sql_type" ], "type": "tidb", "legend": "avg-{sqlType}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 5, "displayName": "Parse Duration", "name": "parse_duration", "description": "Overview of SQL parse duration metrics", "metric": { "name": "parse_duration", "unit": "s", "description": "Shows average and percentile parse durations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p95_tidb_session_parse_duration", "promql": "histogram_quantile(0.95, sum(rate(tidb_session_parse_duration_seconds_bucket{sql_type=\"general\",@LABEL}[1m])) by (le))", "promMetric": "tidb_session_parse_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "95", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_session_parse_duration", "promql": "sum(rate(tidb_session_parse_duration_seconds_sum{sql_type=\"general\",@LABEL}[1m]))", "promMetric": "tidb_session_parse_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tidb_session_parse_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_session_parse_duration_seconds_bucket{sql_type=\"general\",@LABEL}[1m])) by (le))", "promMetric": "tidb_session_parse_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 6, "displayName": "Compile Duration", "name": "compile_duration", "description": "Overview of SQL compile duration metrics", "metric": { "name": "compile_duration", "unit": "s", "description": "Shows average and percentile compile durations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "avg_tidb_session_compile_duration", "promql": "(sum(rate(tidb_session_compile_duration_seconds_sum{sql_type=\"general\",@LABEL}[1m])) / sum(rate(tidb_session_compile_duration_seconds_count{sql_type=\"general\",@LABEL}[1m])))", "promMetric": "tidb_session_compile_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tidb_session_compile_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_session_compile_duration_seconds_bucket{sql_type=\"general\",@LABEL}[1m])) by (le))", "promMetric": "tidb_session_compile_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 7, "displayName": "Execute Duration", "name": "execute_duration", "description": "Overview of SQL execution duration metrics", "metric": { "name": "execute_duration", "unit": "s", "description": "Shows average and 95th percentile SQL execution durations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p95_tidb_session_execute_duration", "promql": "histogram_quantile(0.95, sum(rate(tidb_session_execute_duration_seconds_bucket{sql_type=\"general\",@LABEL}[1m])) by (le))", "promMetric": "tidb_session_execute_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "95", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "avg_tidb_session_execute_duration", "promql": "(sum(rate(tidb_session_execute_duration_seconds_sum{sql_type=\"general\",@LABEL}[1m])) / sum(rate(tidb_session_execute_duration_seconds_count{sql_type=\"general\",@LABEL}[1m])))", "promMetric": "tidb_session_execute_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 8, "displayName": "Avg TiDB KV Request Duration", "name": "avg_tidb_kv_request_duration", "description": "Overview of TiDB KV request duration metrics", "metric": { "name": "avg_tidb_kv_request_duration", "unit": "s", "description": "Shows average duration of KV requests by type", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_tikvclient_request_duration", "promql": "sum(rate(tidb_tikvclient_request_seconds_sum{store!=\"0\",@LABEL}[1m])) by (type)/ sum(rate(tidb_tikvclient_request_seconds_count{store!=\"0\",@LABEL}[1m])) by (type)", "promMetric": "tidb_tikvclient_request_seconds", "labels": [ "type", "instance" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 9, "displayName": "Avg TiKV GRPC Duration", "name": "avg_tikv_grpc_duration", "description": "Overview of TiKV gRPC request duration metrics", "metric": { "name": "avg_tikv_grpc_duration", "unit": "s", "description": "Shows average duration of gRPC requests by type", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "avg_tikv_grpc_duration", "promql": "sum(rate(tikv_grpc_msg_duration_seconds_sum{store!=\"0\",@LABEL}[1m])) by (type)/ sum(rate(tikv_grpc_msg_duration_seconds_count{store!=\"0\",@LABEL}[1m])) by (type)", "promMetric": "tikv_grpc_msg_duration_seconds", "labels": [ "type", "instance" ], "type": "tikv", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 10, "displayName": "PD TSO Wait/RPC Duration", "name": "pd_tso_wait_rpc_duration", "description": "Overview of PD TSO request processing duration metrics", "metric": { "name": "pd_tso_wait_rpc_duration", "unit": "s", "description": "Shows average and 99th percentile durations for TSO wait, RPC, and handle times", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_handle_cmds_wait_duration", "promql": "(sum(rate(pd_client_cmd_handle_cmds_duration_seconds_sum{type=\"wait\",@LABEL}[1m])) / sum(rate(pd_client_cmd_handle_cmds_duration_seconds_count{type=\"wait\",@LABEL}[1m])))", "promMetric": "pd_client_cmd_handle_cmds_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "wait - avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_handle_cmds_rpc_duration", "promql": "(sum(rate(pd_client_request_handle_requests_duration_seconds_sum{type=\"tso,@LABEL\"}[1m])) / sum(rate(pd_client_request_handle_requests_duration_seconds_count{type=\"tso\",@LABEL}[1m])))", "promMetric": "pd_client_request_handle_requests_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "rpc - avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_handle_cmds_duration", "promql": "(sum(rate(pd_server_handle_tso_duration_seconds_sum{@LABEL}[1m])) / sum(rate(pd_server_handle_tso_duration_seconds_count{@LABEL}[1m])))", "promMetric": "pd_server_handle_tso_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "handle - avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_pd_handle_cmds_wait_duration", "promql": "histogram_quantile(0.99, sum(rate(pd_client_request_handle_requests_duration_seconds_bucket{type=\"tso\",@LABEL}[1m])) by (le))", "promMetric": "pd_client_request_handle_requests_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "wait - 99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_pd_handle_cmds_rpc_duration", "promql": "histogram_quantile(0.99, sum(rate(pd_client_request_handle_requests_duration_seconds_bucket{type=\"tso\",@LABEL}[1m])) by (le))", "promMetric": "pd_client_request_handle_requests_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "rpc - 99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_pd_handle_cmds_duration", "promql": "histogram_quantile(0.99, sum(rate(pd_server_handle_tso_duration_seconds_bucket{@LABEL}[30s])) by (type, le))", "promMetric": "pd_server_handle_tso_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "handle - 99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 11, "displayName": "Storage Async Write Duration", "name": "storage_async_write_duration", "description": "Overview of storage asynchronous write duration metrics", "metric": { "name": "storage_async_write_duration", "unit": "s", "description": "Shows average and 99th percentile durations for asynchronous write operations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_storage_engine_async_request_duration", "promql": "sum(rate(tikv_storage_engine_async_request_duration_seconds_sum{type=\"write\",@LABEL}[1m])) / sum(rate(tikv_storage_engine_async_request_duration_seconds_count{type=\"write\",@LABEL}[1m]))", "promMetric": "tikv_storage_engine_async_request_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_storage_engine_async_request_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_storage_engine_async_request_duration_seconds_bucket{type=\"write\",@LABEL}[1m])) by (le))", "promMetric": "tikv_storage_engine_async_request_duration_seconds", "labels": [ "instance" ], "type": "tikv", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 12, "displayName": "Store Duration", "name": "store_duration", "description": "Overview of TiKV store operation duration metrics", "metric": { "name": "store_duration", "unit": "s", "description": "Shows average and 99th percentile durations for store operations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_raftstore_store_duration", "promql": "sum(rate(tikv_raftstore_store_duration_secs_sum{@LABEL}[1m])) / sum(rate(tikv_raftstore_store_duration_secs_count{@LABEL}[1m]))", "promMetric": "tikv_raftstore_store_duration_secs", "labels": [ "instance" ], "type": "tikv", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_raftstore_store_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_store_duration_secs_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_store_duration_secs", "labels": [ "instance" ], "type": "tikv", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "basic", "type": "database_time", "order": 13, "displayName": "Apply Duration", "name": "apply_duration", "description": "Overview of Raft log apply duration metrics", "metric": { "name": "apply_duration", "unit": "s", "description": "Shows average and 99th percentile durations for applying Raft logs", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_raftstore_apply_duration", "promql": "(sum(rate(tikv_raftstore_apply_duration_secs_sum{@LABEL}[1m])) / sum(rate(tikv_raftstore_apply_duration_secs_count{@LABEL}[1m])))", "promMetric": "tikv_raftstore_apply_duration_secs", "labels": [ "instance" ], "type": "tikv", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tikv_raftstore_apply_duration", "promql": "histogram_quantile(0.99, sum(rate(tikv_raftstore_apply_duration_secs_bucket{@LABEL}[1m])) by (le))", "promMetric": "tikv_raftstore_apply_duration_secs", "labels": [ "instance" ], "type": "tikv", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 1, "displayName": "Duration", "name": "duration", "description": "Overall query execution duration across the cluster", "metric": { "name": "duration", "unit": "s", "description": "Shows average and percentile query durations", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_query_duration", "promql": "sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!=\"internal\",@LABEL}[1m])) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!=\"internal\",@LABEL}[1m]))", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "avg", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p99_tidb_query_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{sql_type!=\"internal\",@LABEL}[1m])) by (le))", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "99", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_query_duration_by_sql_type", "promql": "sum(rate(tidb_server_handle_query_duration_seconds_sum{sql_type!=\"internal\",@LABEL}[1m])) by (sql_type) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!=\"internal\",@LABEL}[1m])) by (sql_type)", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance", "sql_type" ], "type": "tidb", "legend": "avg-{sqlType}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 2, "displayName": "QPS", "name": "qps", "description": "Total Queries Per Second across all TiDB instances", "metric": { "name": "qps", "unit": "short", "description": "Shows total QPS, QPS by type, and failed query rates", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "qps_by_type", "promql": "sum(rate(tidb_executor_statement_total{@LABEL}[1m])) by (type)", "promMetric": "tidb_executor_statement_total", "labels": [ "instance", "type" ], "type": "tidb", "legend": "{type}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "qps", "promql": "sum(rate(tidb_executor_statement_total{@LABEL}[1m]))", "promMetric": "tidb_executor_statement", "labels": [ "instance" ], "type": "tidb", "legend": "Total", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "qps_error", "promql": "sum(rate(tidb_server_execute_error_total{@LABEL}[1m])) ", "promMetric": "tidb_server_execute_error_total", "labels": [ "instance" ], "type": "tidb", "legend": "Failed", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 3, "displayName": "Transaction OPS", "name": "transaction_ops_by_type_and_txn_mode", "description": "Transaction operations per second categorized by type and transaction mode", "metric": { "name": "transaction_ops_by_type_and_txn_mode", "unit": "short", "description": "Shows transaction rates categorized by type and transaction mode", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tps_by_type_and_txn_mode", "promql": "sum(rate(tidb_session_transaction_duration_seconds_count{@LABEL}[1m])) by (type, txn_mode)", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "type", "txn_mode" ], "type": "tidb", "legend": "{type}-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 4, "displayName": "Transaction Duration", "name": "transaction_duration", "description": "Time taken to execute transactions, indicating transaction processing efficiency", "metric": { "name": "transaction_duration", "unit": "s", "description": "Shows transaction duration percentiles by transaction mode", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "p99_transaction_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_session_transaction_duration_seconds_bucket{@LABEL}[1m])) by (le, txn_mode))", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "txn_mode" ], "type": "tidb", "legend": "99-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p95_transaction_duration", "promql": "histogram_quantile(0.95, sum(rate(tidb_session_transaction_duration_seconds_bucket{@LABEL}[1m])) by (le, txn_mode))", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "txn_mode" ], "type": "tidb", "legend": "95-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "p80_transaction_duration", "promql": "histogram_quantile(0.80, sum(rate(tidb_session_transaction_duration_seconds_bucket{@LABEL}[1m])) by (le, txn_mode))", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance", "txn_mode" ], "type": "tidb", "legend": "80-{txnMode}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 5, "displayName": "TiKV Increase of Storage Usage", "name": "tikv_increase_of_storage_usage", "description": "Rate of storage space consumption increase in TiKV nodes", "metric": { "name": "tikv_increase_of_storage_usage", "unit": "Bps", "description": "Shows the rate of storage usage increase in TiKV instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_increase_of_storage_usage", "promql": "rate(tikv_store_size_bytes{type=\"used\",@LABEL}[5m])", "promMetric": "tikv_store_size_bytes", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "overview", "type": "", "order": 6, "displayName": "TiFlash Increase of Storage Usage", "name": "tiflash_increase_of_storage_usage", "description": "Rate of storage space consumption increase in TiFlash nodes", "metric": { "name": "tiflash_increase_of_storage_usage", "unit": "Bps", "description": "Shows the rate of storage usage increase in TiFlash instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_increase_of_storage_usage", "promql": "rate(tiflash_system_current_metric_StoreSizeUsed{@LABEL}[5m])", "promMetric": "tiflash_system_current_metric_StoreSizeUsed", "labels": [ "instance" ], "type": "tiflash", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tidb", "order": 1, "displayName": "TiDB CPU", "name": "tidb_cpu_usage", "description": "Overview of TiDB CPU usage metrics", "metric": { "name": "tidb_cpu_usage", "unit": "percentunit", "description": "Shows CPU usage percentage and MAXPROCS limit for TiDB instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_cpu_usage_percentage", "promql": "irate(process_cpu_seconds_total{ job=\"tidb\",@LABEL}[1m])", "promMetric": "process_cpu_seconds_total", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_server_maxprocs", "promql": "tidb_server_maxprocs{job=\"tidb\",@LABEL}", "promMetric": "tidb_server_maxprocs", "labels": [ "instance" ], "type": "tidb", "legend": "limit-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tiflash", "order": 1, "displayName": "TiFlash CPU", "name": "tiflash_cpu_usage", "description": "Overview of TiFlash CPU usage metrics", "metric": { "name": "tiflash_cpu_usage", "unit": "percentunit", "description": "Shows CPU usage percentage and core limits for TiFlash instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_cpu_usage_percentage", "promql": "rate(tiflash_proxy_process_cpu_seconds_total{job=\"tiflash\",@LABEL}[1m])", "promMetric": "tiflash_proxy_process_cpu_seconds_total", "labels": [ "instance" ], "type": "tiflash", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tiflash_logic_cpu_cores", "promql": "sum(tiflash_system_current_metric_LogicalCPUCores{@LABEL}) by (instance)", "promMetric": "tiflash_system_current_metric_LogicalCPUCores", "labels": [ "instance" ], "type": "tiflash", "legend": "limit-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tikv", "order": 1, "displayName": "TiKV CPU", "name": "tikv_cpu_usage", "description": "Overview of TiKV CPU usage metrics", "metric": { "name": "tikv_cpu_usage", "unit": "percentunit", "description": "Shows CPU usage percentage for TiKV instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_cpu_usage_percentage", "promql": "sum(rate(process_cpu_seconds_total{ job=~\".*tikv\",@LABEL}[1m])) by (instance)", "promMetric": "process_cpu_seconds_total", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "pd", "order": 1, "displayName": "PD CPU", "name": "pd_cpu_usage_percentage", "description": "Overview of PD CPU usage metrics", "metric": { "name": "pd_cpu_usage_percentage", "unit": "percentunit", "description": "Shows CPU usage percentage for PD instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_cpu_usage_percentage", "promql": "irate(process_cpu_seconds_total{job=~\".*pd.*\",@LABEL}[30s])", "promMetric": "process_cpu_seconds_total", "labels": [ "instance" ], "type": "pd", "legend": "{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "pd", "order": 2, "displayName": "PD Memory", "name": "pd_memory_usage", "description": "Overview of PD memory usage metrics", "metric": { "name": "pd_memory_usage", "unit": "bytes", "description": "Shows detailed memory usage metrics for PD instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "pd_memory_usage", "promql": "process_resident_memory_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "process_resident_memory_bytes", "labels": [ "instance" ], "type": "pd", "legend": "process-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_sys_memory_usage", "promql": "go_memstats_heap_sys_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_sys_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapSys-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_inuse_memory_usage", "promql": "go_memstats_heap_inuse_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_inuse_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapInuse-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_alloc_memory_usage", "promql": "go_memstats_heap_alloc_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_alloc_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapAlloc-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_idle_memory_usage", "promql": "go_memstats_heap_idle_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_idle_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapIdle-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_heap_released_memory_usage", "promql": "go_memstats_heap_released_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_heap_released_bytes", "labels": [ "instance" ], "type": "pd", "legend": "HeapReleased-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "pd_gc_trigger_memory_usage", "promql": "go_memstats_next_gc_bytes{job=~\".*pd.*\",@LABEL}", "promMetric": "go_memstats_next_gc_bytes", "labels": [ "instance" ], "type": "pd", "legend": "GCTrigger-{job}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tiflash", "order": 2, "displayName": "TiFlash Memory", "name": "tiflash_memory_usage", "description": "Overview of TiFlash memory usage metrics", "metric": { "name": "tiflash_memory_usage", "unit": "bytes", "description": "Shows memory usage and limits for TiFlash instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_memory_usage", "promql": "tiflash_proxy_process_resident_memory_bytes{job=\"tiflash\",@LABEL}", "promMetric": "tiflash_proxy_process_resident_memory_bytes", "labels": [ "instance" ], "type": "tiflash", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tiflash_memory_limit", "promql": "sum(tiflash_system_current_metric_MemoryCapacity{@LABEL}) by (instance)", "promMetric": "tiflash_system_current_metric_MemoryCapacity", "labels": [ "instance" ], "type": "tiflash", "legend": "limit-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tidb", "order": 2, "displayName": "TiDB Memory", "name": "tidb_memory_usage", "description": "Overview of TiDB memory usage metrics", "metric": { "name": "tidb_memory_usage", "unit": "bytes", "description": "Shows detailed memory usage metrics for TiDB instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_process_memory_usage", "promql": "process_resident_memory_bytes{ job=\"tidb\",@LABEL}", "promMetric": "process_resident_memory_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "process-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_heap_sys_memory_usage", "promql": "go_memory_classes_heap_objects_bytes{ job=\"tidb\",@LABEL} + go_memory_classes_heap_unused_bytes{ job=\"tidb\",@LABEL} + go_memory_classes_heap_released_bytes{ job=\"tidb\",@LABEL} + go_memory_classes_heap_free_bytes{ job=\"tidb\",@LABEL}", "promMetric": "go_memory_classes_heap_objects_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "HeapSys-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_heap_inuse_memory_usage", "promql": "go_memory_classes_heap_objects_bytes{ job=\"tidb\",@LABEL} + go_memory_classes_heap_unused_bytes{ job=\"tidb\",@LABEL}", "promMetric": "go_memory_classes_heap_objects_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "HeapInuse-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_heap_alloc_memory_usage", "promql": "go_memory_classes_heap_objects_bytes{ job=\"tidb\",@LABEL}", "promMetric": "go_memory_classes_heap_objects_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "HeapAlloc-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_heap_idle_memory_usage", "promql": "go_memory_classes_heap_released_bytes{job=\"tidb\",@LABEL} + go_memory_classes_heap_free_bytes{ job=\"tidb\",@LABEL}", "promMetric": "go_memory_classes_heap_released_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "HeapIdle-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_heap_released_memory_usage", "promql": "go_memory_classes_heap_released_bytes{job=\"tidb\",@LABEL}", "promMetric": "go_memory_classes_heap_released_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "HeapReleased-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_gc_trigger_memory_usage", "promql": "go_gc_heap_goal_bytes{job=\"tidb\",@LABEL}", "promMetric": "go_gc_heap_goal_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "GCTrigger-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_server_memory_usage", "promql": "tidb_server_memory_usage{job=\"tidb\",@LABEL}", "promMetric": "tidb_server_memory_usage", "labels": [ "instance" ], "type": "tidb", "legend": "{module}-{type}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tidb_server_memory_usage_by_module", "promql": "sum(tidb_server_memory_usage{job=\"tidb\",@LABEL}) by (module, instance)", "promMetric": "tidb_server_memory_usage", "labels": [ "instance" ], "type": "tidb", "legend": "{module}-{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tikv", "order": 2, "displayName": "TiKV Memory", "name": "tikv_memory_usage", "description": "Overview of TiKV memory usage metrics", "metric": { "name": "tikv_memory_usage", "unit": "bytes", "description": "Shows memory usage for TiKV instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_memory_usage", "promql": "avg(process_resident_memory_bytes{job=~\".*tikv\",@LABEL}) by (instance)", "promMetric": "process_resident_memory_bytes", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tikv", "order": 3, "displayName": "TiKV IO MBps", "name": "tikv_io_mbps", "description": "Overview of TiKV IO throughput metrics", "metric": { "name": "tikv_io_mbps", "unit": "Bps", "description": "Shows read and write IO throughput for TiKV instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_io_mbps_write", "promql": "sum(rate(tikv_engine_flow_bytes{db=\"kv\", type=\"wal_file_bytes\",@LABEL}[1m])) by (instance)", "promMetric": "tikv_engine_flow_bytes", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}-write", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tikv_io_mbps_read", "promql": "sum(rate(tikv_engine_flow_bytes{db=\"kv\", type=~\"bytes_read|iter_bytes_read\",@LABEL}[1m])) by (instance)", "promMetric": "tikv_engine_flow_bytes", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}-read", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tiflash", "order": 3, "displayName": "TiFlash IO MBps", "name": "tiflash_io_mbps", "description": "Overview of TiFlash IO throughput metrics", "metric": { "name": "tiflash_io_mbps", "unit": "Bps", "description": "Shows read and write IO throughput for TiFlash instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_io_mbps_write", "promql": "sum(rate(tiflash_system_profile_event_WriteBufferFromFileDescriptorWriteBytes{@LABEL}[1m])) by (instance) + sum(rate(tiflash_system_profile_event_PSMWriteBytes{@LABEL}[1m])) by (instance) + sum(rate(tiflash_system_profile_event_WriteBufferAIOWriteBytes{@LABEL}[1m])) by (instance)", "promMetric": "tiflash_system_profile_event_WriteBufferFromFileDescriptorWriteBytes", "labels": [ "instance" ], "type": "tiflash", "legend": "{instance}-write", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "tiflash_io_mbps_read", "promql": "sum(rate(tiflash_system_profile_event_ReadBufferFromFileDescriptorReadBytes{@LABEL}[1m])) by (instance) + sum(rate(tiflash_system_profile_event_PSMReadBytes{@LABEL}[1m])) by (instance) + sum(rate(tiflash_system_profile_event_ReadBufferAIOReadBytes{@LABEL}[1m])) by (instance)", "promMetric": "tiflash_system_profile_event_ReadBufferFromFileDescriptorReadBytes", "labels": [ "instance" ], "type": "tiflash", "legend": "{instance}-read", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tikv", "order": 4, "displayName": "TiKV Available Storage", "name": "tikv_available_storage", "description": "Overview of TiKV storage availability metrics", "metric": { "name": "tikv_available_storage", "unit": "bytes", "description": "Shows available storage space for TiKV instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_store_size_available", "promql": "sum(tikv_store_size_bytes{type=\"available\",@LABEL}) by (instance)", "promMetric": "tikv_store_size_bytes", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tiflash", "order": 4, "displayName": "TiFlash Available Storage", "name": "tiflash_available_storage", "description": "Overview of TiFlash storage availability metrics", "metric": { "name": "tiflash_available_storage", "unit": "bytes", "description": "Shows available storage space for TiFlash instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_available_store_size", "promql": "sum(tiflash_system_current_metric_StoreSizeAvailable{@LABEL}) by (instance)", "promMetric": "tiflash_system_current_metric_StoreSizeAvailable", "labels": [ "instance" ], "type": "tiflash", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tiflash", "order": 5, "displayName": "TiFlash Increase of Storage Usage", "name": "tiflash_increase_of_storage_usage", "description": "Overview of TiFlash storage usage growth metrics", "metric": { "name": "tiflash_increase_of_storage_usage", "unit": "Bps", "description": "Shows the rate of storage usage increase in TiFlash instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_increase_of_storage_usage", "promql": "rate(tiflash_system_current_metric_StoreSizeUsed{@LABEL}[5m])", "promMetric": "tiflash_system_current_metric_StoreSizeUsed", "labels": [ "instance" ], "type": "tiflash", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "cluster", "group": "resource", "type": "tikv", "order": 5, "displayName": "TiKV Increase of Storage Usage", "name": "tikv_increase_of_storage_usage", "description": "Overview of TiKV storage usage growth metrics", "metric": { "name": "tikv_increase_of_storage_usage", "unit": "Bps", "description": "Shows the rate of storage usage increase in TiKV instances", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_increase_of_storage_usage", "promql": "rate(tikv_store_size_bytes{type=\"used\",@LABEL}[5m])", "promMetric": "tikv_store_size_bytes", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } } ] } ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/metrics-config-host.json ================================================ { "metrics": [ { "class": "host", "group": "basic", "type": "resource", "order": 1, "displayName": "Memory", "name": "host_memory", "description": "Total and available memory statistics of the host machine", "metric": { "name": "host_memory", "unit": "bytes", "description": "Overview of system memory usage", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_memory_total", "promql": "node_memory_MemTotal_bytes{@LABEL}", "promMetric": "node_memory_MemTotal_bytes", "labels": [ "" ], "type": "host", "legend": "total", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "host_memory_used", "promql": "node_memory_MemTotal_bytes{@LABEL} - (node_memory_MemAvailable_bytes{@LABEL} or (node_memory_MemFree_bytes{@LABEL} + node_memory_Buffers_bytes{@LABEL} + node_memory_Cached_bytes{@LABEL}))", "promMetric": "node_memory_MemAvailable_bytes", "labels": [ "" ], "type": "host", "legend": "used", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "host_memory_available", "promql": "node_memory_MemAvailable_bytes{@LABEL} or (node_memory_MemFree_bytes{@LABEL} + node_memory_Buffers_bytes{@LABEL} + node_memory_Cached_bytes{@LABEL})", "promMetric": "node_memory_MemAvailable_bytes", "labels": [ "" ], "type": "host", "legend": "available", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 1, "displayName": "Linux System Load", "name": "host_linux_system_load", "description": "System load averages for the past 1, 5, and 15 minutes", "metric": { "name": "host_linux_system_load", "unit": "short", "description": "System load averages for the past 1, 5, and 15 minutes", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_load_1", "promql": "node_load1{@LABEL}", "promMetric": "node_load1", "labels": [ "" ], "type": "host", "legend": "Load 1m", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "host_load_5", "promql": "node_load5{@LABEL}", "promMetric": "node_load5", "labels": [ "" ], "type": "host", "legend": "Load 5m", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "host_load_15", "promql": "node_load15{@LABEL}", "promMetric": "node_load15", "labels": [ "" ], "type": "host", "legend": "Load 15m", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "process", "order": 1, "displayName": "Process Memory", "name": "host_process_memory", "description": "Memory consumption of specific processes running on the host", "metric": { "name": "host_process_memory", "unit": "bytes", "description": "Memory consumption of specific processes", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_process_memory", "promql": "process_resident_memory_bytes{@LABEL}", "promMetric": "process_resident_memory_bytes", "labels": [ "job", "instance" ], "type": "host", "legend": "{job} {instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 2, "displayName": "CPU Usage", "name": "host_cpu_usage", "description": "Percentage of CPU utilization across all cores of the host machine", "metric": { "name": "host_cpu_usage", "unit": "percentunit", "description": "Overall CPU usage of the host machine, expressed as a percentage.", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_cpu_usage", "promql": "1-(avg by(instance)(irate(node_cpu_seconds_total{mode=\"idle\",@LABEL}[1m])))", "promMetric": "node_cpu_seconds_total", "labels": [ "instance" ], "type": "host", "legend": "cpu_usage_percent", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "process", "order": 2, "displayName": "Process CPU Usage", "name": "host_process_cpu_usage", "description": "CPU utilization of specific processes running on the host", "metric": { "name": "host_process_cpu_usage", "unit": "percentunit", "description": "CPU utilization percentage for specific processes", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_process_cpu_usage", "promql": "rate(process_cpu_seconds_total{@LABEL}[1m])", "promMetric": "process_cpu_seconds_total", "labels": [ "job", "instance" ], "type": "host", "legend": "{job} {instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "resource", "order": 2, "displayName": "Memory Usage", "name": "host_memory_usage", "description": "Percentage of total memory currently in use by the system", "metric": { "name": "host_memory_usage", "unit": "percentunit", "description": "Total memory usage of the host machine, indicating system-wide RAM utilization", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_memory_usage", "promql": "(node_memory_MemTotal_bytes{@LABEL}-node_memory_MemFree_bytes{@LABEL}-node_memory_Buffers_bytes{@LABEL}-node_memory_Cached_bytes{@LABEL})/node_memory_MemTotal_bytes{@LABEL}", "promMetric": "node_memory_MemTotal_bytes", "labels": [], "type": "host", "legend": "memory_usage_percentage", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 3, "displayName": "IO Usage", "name": "host_io_usage", "description": "Percentage of time the system spent on I/O operations", "metric": { "name": "host_io_usage", "unit": "percentunit", "description": "Percentage of time the storage device is busy processing I/O requests", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_io_usage", "promql": "rate(node_disk_io_time_seconds_total{@LABEL}[30m]) or irate(node_disk_io_time_seconds_total{@LABEL}[1m])", "promMetric": "node_disk_io_time_seconds_total", "labels": [ "device" ], "type": "host", "legend": "{device}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "resource", "order": 3, "displayName": "Disk Usage", "name": "host_disk_usage", "description": "Percentage of disk space utilized across mounted filesystems", "metric": { "name": "host_disk_usage", "unit": "percentunit", "description": "Percentage of used disk space for each filesystem mount point", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_disk_usage", "promql": "1 - node_filesystem_avail_bytes{ device=~'^/.*',@LABEL} / node_filesystem_size_bytes{device=~'^/.*',@LABEL}", "promMetric": "node_filesystem_avail_bytes", "labels": [ "device", "fstype", "mountpoint" ], "type": "host", "legend": "{device} {fstype} {mountpoint}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 4, "displayName": "IO Queue Size", "name": "host_io_queue_size", "description": "Number of I/O requests waiting in the device queue", "metric": { "name": "host_io_queue_size", "unit": "short", "description": "Average number of I/O requests waiting in the device queue", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_io_queue_size", "promql": "rate(node_disk_io_time_weighted_seconds_total{@LABEL}[5m])", "promMetric": "node_disk_io_time_weighted_seconds_total", "labels": [ "device" ], "type": "host", "legend": "{device}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 5, "displayName": "IOPs", "name": "host_iops", "description": "Number of I/O operations (reads and writes) per second", "metric": { "name": "host_iops", "unit": "short", "description": "Number of I/O operations (reads and writes) per second", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_iops", "promql": "sum(rate(node_disk_reads_completed_total{@LABEL}[5m])+rate(node_disk_writes_completed_total{@LABEL}[5m]))by(instance)", "promMetric": "node_disk_reads_completed_total", "labels": [ "instance" ], "type": "host", "legend": "iops", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 6, "displayName": "IO Latency", "name": "host_io_latency", "description": "Average time taken to complete I/O operations", "metric": { "name": "host_io_latency", "unit": "s", "description": "Average time taken for read and write I/O operations to complete", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_io_write_latency", "promql": "(rate(node_disk_write_time_seconds_total{@LABEL}[5m])/ rate(node_disk_writes_completed_total{@LABEL}[5m]))", "promMetric": "node_disk_write_time_seconds_total", "labels": [ "instance" ], "type": "host", "legend": "Write Latency: [{device}]", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "host_io_read_latency", "promql": "(rate(node_disk_read_time_seconds_total{@LABEL}[5m])/ rate(node_disk_reads_completed_total{@LABEL}[5m]))", "promMetric": "node_disk_read_time_seconds_total", "labels": [ "instance" ], "type": "host", "legend": "Read Latency: [{device}]", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 7, "displayName": "IO Throughput", "name": "host_io_throughput", "description": "Rate of data transfer for I/O operations", "metric": { "name": "host_io_throughput", "unit": "bytes", "description": "Rate of data transfer to and from storage devices", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_io_throughput", "promql": "irate(node_disk_read_bytes_total{@LABEL}[5m]) + irate(node_disk_written_bytes_total{@LABEL}[5m])", "promMetric": "node_disk_read_bytes_total", "labels": [ "" ], "type": "host", "legend": "{device}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 8, "displayName": "Network Throughput", "name": "host_network_throughput", "description": "Rate of data transfer over the network interfaces", "metric": { "name": "host_network_throughput", "unit": "bytes", "description": "Network traffic throughput of the host machine, measuring inbound and outbound data transfer", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_network_received", "promql": "sum(increase(node_network_receive_bytes_total{device!=\"lo\",@LABEL}[5m]))by(instance)", "promMetric": "node_network_receive_bytes_total", "labels": [], "type": "host", "legend": "received", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "host_network_sent", "promql": "sum(increase(node_network_transmit_bytes_total{device!=\"lo\",@LABEL}[5m]))by(instance)", "promMetric": "node_network_transmit_bytes_total", "labels": [], "type": "host", "legend": "sent", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 9, "displayName": "Network In/Out Packets", "name": "host_network_in_out_packets", "description": "Number of network packets received and transmitted per second", "metric": { "name": "host_network_in_out_packets", "unit": "pps", "description": "Number of network packets received (inbound) and transmitted (outbound) per second for each network interface", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_network_in_packets", "promql": "rate(node_network_receive_packets_total{device!=\"lo\",@LABEL}[30m]) or irate(node_network_receive_packets_total{device!=\"lo\",@LABEL}[5m])", "promMetric": "node_network_receive_packets_total", "labels": [ "device" ], "type": "host", "legend": "Inbound: {device}", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "host_network_out_packets", "promql": "rate(node_network_transmit_packets_total{device!=\"lo\",@LABEL}[30m]) or irate(node_network_transmit_packets_total{device!=\"lo\",@LABEL}[5m])", "promMetric": "node_network_transmit_packets_total", "labels": [ "device" ], "type": "host", "legend": "Outbound: {device}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 10, "displayName": "TCP Retransmission Percentage", "name": "host_tcp_retrans_percentage", "description": "Percentage of TCP packets that needed to be retransmitted", "metric": { "name": "host_tcp_retrans_percentage", "unit": "percentunit", "description": "Percentage of TCP packets that had to be retransmitted", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_tcp_retrans_percentage", "promql": "rate(node_netstat_Tcp_RetransSegs{@LABEL}[5m]) / rate(node_netstat_Tcp_InSegs{@LABEL}[5m])", "promMetric": "node_netstat_Tcp_RetransSegs", "labels": [ "" ], "type": "host", "legend": "tcp_retrans_percent", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "host", "group": "basic", "type": "performance", "order": 11, "displayName": "Inode Usage", "name": "host_inode_usage", "description": "Percentage of used inodes in the filesystem", "metric": { "name": "host_inode_usage", "unit": "percent", "description": "Percentage of used inodes for each filesystem", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_inode_usage", "promql": "node_filesystem_files_free{@LABEL} / node_filesystem_files{@LABEL}", "promMetric": "node_filesystem_files_free", "labels": [ "fstype", "device", "mountpoint" ], "type": "host", "legend": "{fstype} {device} {mountpoint}", "minTidbVersion": "", "maxTidbVersion": "" } ] } } ] } ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/metrics-config-overview.json ================================================ { "metrics": [ { "class": "overview", "group": "overview", "type": "instance_top", "order": 1, "displayName": "TiDB CPU", "name": "overview_tidb_cpu_usage_percentage", "description": "CPU usage of TiDB instances, showing the percentage of CPU resources being utilized", "metric": { "name": "overview_tidb_cpu_usage_percentage", "unit": "percentunit", "description": "CPU usage of TiDB instances, showing the percentage of CPU resources being utilized", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_cpu_usage_percentage", "promql": "irate(process_cpu_seconds_total{ job=\"tidb\",@LABEL}[1m])", "promMetric": "process_cpu_seconds_total", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "cluster_top", "order": 1, "displayName": "Transaction OPS", "name": "overview_transaction_ops", "description": "Number of transactions processed per second across the cluster", "metric": { "name": "overview_transaction_ops", "unit": "short", "description": "Number of transactions processed per second across the cluster", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tps", "promql": "sum(rate(tidb_session_transaction_duration_seconds_count{@LABEL}[1m]))", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "host_top", "order": 1, "displayName": "CPU Usage", "name": "overview_host_cpu_usage", "description": "Overall CPU usage of the host machine, showing system-wide CPU utilization", "metric": { "name": "overview_host_cpu_usage", "unit": "percentunit", "description": "Overall CPU usage of the host machine, showing system-wide CPU utilization", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_cpu_usage", "promql": "1-(avg by(instance)(irate(node_cpu_seconds_total{mode=\"idle\",@LABEL}[1m])))", "promMetric": "node_cpu_seconds_total", "labels": [ "instance" ], "type": "host", "legend": "cpu_usage_percent", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "instance_top", "order": 2, "displayName": "TiDB Memory", "name": "overview_tidb_memory_usage", "description": "Memory usage of TiDB instances, indicating the amount of RAM being consumed", "metric": { "name": "overview_tidb_memory_usage", "unit": "bytes", "description": "Memory usage of TiDB instances, indicating the amount of RAM being consumed", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tidb_memory_usage", "promql": "process_resident_memory_bytes{job=\"tidb\",@LABEL}", "promMetric": "process_resident_memory_bytes", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "cluster_top", "order": 2, "displayName": "QPS", "name": "overview_qps", "description": "Queries per second processed by the cluster", "metric": { "name": "overview_qps", "unit": "short", "description": "Queries per second processed by the cluster", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "qps", "promql": "sum(rate(tidb_executor_statement_total{@LABEL}[1m]))", "promMetric": "tidb_executor_statement", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "host_top", "order": 2, "displayName": "Memory Usage", "name": "overview_host_memory_usage", "description": "Total memory usage of the host machine, indicating system-wide RAM utilization", "metric": { "name": "overview_host_memory_usage", "unit": "percentunit", "description": "Total memory usage of the host machine, indicating system-wide RAM utilization", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_memory_usage", "promql": "(node_memory_MemTotal_bytes{@LABEL}-node_memory_MemFree_bytes{@LABEL}-node_memory_Buffers_bytes{@LABEL}-node_memory_Cached_bytes{@LABEL})/node_memory_MemTotal_bytes{@LABEL}", "promMetric": "node_memory_MemTotal_bytes", "labels": [], "type": "host", "legend": "memory_usage_percent", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "host_top", "order": 3, "displayName": "IO Usage", "name": "overview_host_avg_io_usage", "description": "Disk avg I/O usage of the host machine, monitoring disk I/O performance", "metric": { "name": "overview_host_avg_io_usage", "unit": "percentunit", "description": "Disk avg I/O usage of the host machine, monitoring disk I/O performance", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_avg_io_usage", "promql": "avg(rate(node_disk_io_time_seconds_total{@LABEL}[5m])) by (instance)", "promMetric": "node_disk_io_time_seconds_total", "labels": [ "instance" ], "type": "host", "legend": "io_usage", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "instance_top", "order": 3, "displayName": "TiKV Storage", "name": "overview_tikv_storage_usage", "description": "Storage space usage of TiKV instances, showing disk space consumption", "metric": { "name": "overview_tikv_storage_usage", "unit": "bytes", "description": "Storage space usage of TiKV instances, showing disk space consumption", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tikv_storage_usage", "promql": "sum(tikv_store_size_bytes{type=\"used\",@LABEL})by (instance)", "promMetric": "sum(tikv_store_size_bytes{type=\"used\",@LABEL})by (instance)", "labels": [ "instance" ], "type": "tikv", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "cluster_top", "order": 3, "displayName": "Duration", "name": "overview_query_duration", "description": "Average time taken to execute queries in the cluster", "metric": { "name": "overview_query_duration", "unit": "s", "description": "Average time taken to execute queries in the cluster, measured in seconds", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "avg_query_duration", "promql": "sum(rate(tidb_server_handle_query_duration_seconds_sum{ sql_type!=\"internal\",@LABEL}[1m])) / sum(rate(tidb_server_handle_query_duration_seconds_count{sql_type!=\"internal\",@LABEL}[1m]))", "promMetric": "tidb_server_handle_query_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "host_top", "order": 4, "displayName": "Network Throughput", "name": "overview_host_network_throughput", "description": "Network traffic throughput of the host machine, measuring inbound and outbound data transfer", "metric": { "name": "overview_host_network_throughput", "unit": "bytes", "description": "Network traffic throughput of the host machine, measuring inbound and outbound data transfer", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "host_network_received", "promql": "sum(increase(node_network_receive_bytes_total{device!=\"lo\",@LABEL}[5m]))by(instance)", "promMetric": "node_network_receive_bytes_total", "labels": [], "type": "host", "legend": "received", "minTidbVersion": "", "maxTidbVersion": "" }, { "name": "host_network_sent", "promql": "sum(increase(node_network_transmit_bytes_total{device!=\"lo\",@LABEL}[5m]))by(instance)", "promMetric": "node_network_transmit_bytes_total", "labels": [], "type": "host", "legend": "sent", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "instance_top", "order": 4, "displayName": "TiFlash Storage", "name": "overview_tiflash_storage_usage", "description": "Storage space usage of TiFlash instances, monitoring disk space utilization", "metric": { "name": "overview_tiflash_storage_usage", "unit": "bytes", "description": "Storage space usage of TiFlash instances, monitoring disk space utilization", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "tiflash_storage_usage", "promql": "sum(tiflash_system_current_metric_StoreSizeUsed{@LABEL}) by (instance)", "promMetric": "tiflash_system_current_metric_StoreSizeUsed", "labels": [], "type": "tiflash", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "cluster_top", "order": 4, "displayName": "Transaction Duration", "name": "overview_transaction_duration", "description": "Average time taken to complete transactions in the cluster", "metric": { "name": "overview_transaction_duration", "unit": "s", "description": "Average time taken to complete transactions in the cluster, measured in seconds", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "avg_transaction_duration", "promql": "sum(rate(tidb_session_transaction_duration_seconds_sum{@LABEL}[1m])) / sum(rate(tidb_session_transaction_duration_seconds_count{@LABEL}[1m])) ", "promMetric": "tidb_session_transaction_duration_seconds", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "cluster_top", "order": 5, "displayName": "Commit Token Wait Duration", "name": "overview_commit_token_wait_duration", "description": "Time spent waiting for commit tokens, indicating potential transaction bottlenecks", "metric": { "name": "overview_commit_token_wait_duration", "unit": "ns", "description": "Time spent waiting for commit tokens in the cluster", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "commit_token_wait_duration", "promql": "histogram_quantile(0.99, sum(rate(tidb_tikvclient_batch_executor_token_wait_duration_bucket{@LABEL}[1m]))by(le))", "promMetric": "tidb_tikvclient_batch_executor_token_wait_duration", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } }, { "class": "overview", "group": "overview", "type": "cluster_top", "order": 6, "displayName": "Connection Count", "name": "overview_connection_count", "description": "Total number of active connections to the cluster", "metric": { "name": "overview_connection_count", "unit": "short", "description": "Total number of active connections to the cluster", "minTidbVersion": "", "maxTidbVersion": "", "isBuiltin": true, "expressions": [ { "name": "connection_count_sum", "promql": "sum(tidb_server_connections{@LABEL})", "promMetric": "tidb_server_connections", "labels": [ "instance" ], "type": "tidb", "legend": "{instance}", "minTidbVersion": "", "maxTidbVersion": "" } ] } } ] } ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/metrics-data-cpu-usage.json ================================================ { "status": "success", "data": [ { "expr": "irate(process_cpu_seconds_total{ job=\"tidb\",}[1m])", "legend": "{instance}", "result": [ { "metric": { "instance": "tidb-810cad05/172.18.3.6:10080", "sqlType": "", "type": "", "result": "", "txnMode": "", "job": "tidb", "device": "", "fstype": "", "mountpoint": "", "module": "", "kind": "", "ping": "" }, "values": [ { "timestamp": 1734188847, "value": "0.03933333333358557" }, { "timestamp": 1734188997, "value": "0.04266666666662786" }, { "timestamp": 1734189147, "value": "0.022666666666433837" }, { "timestamp": 1734189297, "value": "0.04733333333327513" }, { "timestamp": 1734189447, "value": "0.0226666666669189" }, { "timestamp": 1734189597, "value": "0.05" }, { "timestamp": 1734189747, "value": "0.04333333333343035" }, { "timestamp": 1734189897, "value": "0.04600000000015522" }, { "timestamp": 1734190047, "value": "0.016666666666666666" }, { "timestamp": 1734190197, "value": "0.01933333333339154" }, { "timestamp": 1734190347, "value": "0.018666666666589057" }, { "timestamp": 1734190497, "value": "0.018666666666589057" }, { "timestamp": 1734190647, "value": "0.015999999999864182" }, { "timestamp": 1734190797, "value": "0.01933333333339154" }, { "timestamp": 1734190947, "value": "0.017333333332984088" }, { "timestamp": 1734191097, "value": "0.020000000000194026" }, { "timestamp": 1734191247, "value": "0.016666666666666666" }, { "timestamp": 1734191397, "value": "0.02133333333331393" }, { "timestamp": 1734191547, "value": "0.018666666666589057" }, { "timestamp": 1734191697, "value": "0.027333333333566166" }, { "timestamp": 1734191847, "value": "0.016666666666666666" }, { "timestamp": 1734191997, "value": "0.019999999999708962" }, { "timestamp": 1734192147, "value": "0.017335644752284392" }, { "timestamp": 1734192297, "value": "0.01933333333339154" }, { "timestamp": 1734192447, "value": "0.01733333333346915" } ] } ] }, { "expr": "irate(process_cpu_seconds_total{ job=\"tidb\",}[1m])", "legend": "{instance}", "result": [ { "metric": { "instance": "tidb-810cad05/172.18.3.8:10080", "sqlType": "", "type": "", "result": "", "txnMode": "", "job": "tidb", "device": "", "fstype": "", "mountpoint": "", "module": "", "kind": "", "ping": "" }, "values": [ { "timestamp": 1734188847, "value": "0.012666666666336823" }, { "timestamp": 1734188997, "value": "0.013999999999941792" }, { "timestamp": 1734189147, "value": "0.011333333333216919" }, { "timestamp": 1734189297, "value": "0.013333333333139307" }, { "timestamp": 1734189447, "value": "0.028666666666686068" }, { "timestamp": 1734189597, "value": "0.014666666666744276" }, { "timestamp": 1734189747, "value": "0.012000000000019403" }, { "timestamp": 1734189897, "value": "0.014666666666744276" }, { "timestamp": 1734190047, "value": "0.012666666666821888" }, { "timestamp": 1734190197, "value": "0.014666666666259213" }, { "timestamp": 1734190347, "value": "0.012000000000019403" }, { "timestamp": 1734190497, "value": "0.013333333333624372" }, { "timestamp": 1734190647, "value": "0.011333333333216919" }, { "timestamp": 1734190797, "value": "0.014000000000426857" }, { "timestamp": 1734190947, "value": "0.03999999999990299" }, { "timestamp": 1734191097, "value": "0.03400000000013582" }, { "timestamp": 1734191247, "value": "0.04066666666622041" }, { "timestamp": 1734191397, "value": "0.041333333333022894" }, { "timestamp": 1734191547, "value": "0.03866666666678308" }, { "timestamp": 1734191697, "value": "0.03666666666686069" }, { "timestamp": 1734191847, "value": "0.03866666666678308" }, { "timestamp": 1734191997, "value": "0.03600000000005821" }, { "timestamp": 1734192147, "value": "0.03400000000013582" }, { "timestamp": 1734192297, "value": "0.04733333333376019" }, { "timestamp": 1734192447, "value": "0.04800000000007761" } ] } ] }, { "expr": "irate(process_cpu_seconds_total{ job=\"tidb\",}[1m])", "legend": "{instance}", "result": [ { "metric": { "instance": "tidb-c1237e04/172.18.3.4:10080", "sqlType": "", "type": "", "result": "", "txnMode": "", "job": "tidb", "device": "", "fstype": "", "mountpoint": "", "module": "", "kind": "", "ping": "" }, "values": [ { "timestamp": 1734188847, "value": "0.02133333333331393" }, { "timestamp": 1734188997, "value": "0.031999999999728364" }, { "timestamp": 1734189147, "value": "0.020666666666511447" }, { "timestamp": 1734189297, "value": "0.02333333333323632" }, { "timestamp": 1734189447, "value": "0.022000000000116416" }, { "timestamp": 1734189597, "value": "0.030666666666608458" }, { "timestamp": 1734189747, "value": "0.021999999999631353" }, { "timestamp": 1734189897, "value": "0.02333333333323632" }, { "timestamp": 1734190047, "value": "0.021999999999631353" }, { "timestamp": 1734190197, "value": "0.029333333333488552" }, { "timestamp": 1734190347, "value": "0.02066666666699651" }, { "timestamp": 1734190497, "value": "0.022666666666433837" }, { "timestamp": 1734190647, "value": "0.02133333333331393" }, { "timestamp": 1734190797, "value": "0.03133333333341094" }, { "timestamp": 1734190947, "value": "0.02066666666699651" }, { "timestamp": 1734191097, "value": "0.0226666666669189" }, { "timestamp": 1734191247, "value": "0.02333333333323632" }, { "timestamp": 1734191397, "value": "0.030000000000291037" }, { "timestamp": 1734191547, "value": "0.01533333333354676" }, { "timestamp": 1734191697, "value": "0.016666666666666666" }, { "timestamp": 1734191847, "value": "0.015999999999864182" }, { "timestamp": 1734191997, "value": "0.02466666666684129" }, { "timestamp": 1734192147, "value": "0.015333333333061697" }, { "timestamp": 1734192297, "value": "0.018000000000271636" }, { "timestamp": 1734192447, "value": "0.015999999999864182" } ] } ] }, { "expr": "irate(process_cpu_seconds_total{ job=\"tidb\",}[1m])", "legend": "{instance}", "result": [ { "metric": { "instance": "tidb-810cad05/172.18.3.7:10080", "sqlType": "", "type": "", "result": "", "txnMode": "", "job": "tidb", "device": "", "fstype": "", "mountpoint": "", "module": "", "kind": "", "ping": "" }, "values": [ { "timestamp": 1734188847, "value": "0.012000000000019403" }, { "timestamp": 1734188997, "value": "0.01533333333354676" }, { "timestamp": 1734189147, "value": "0.012666666666821888" }, { "timestamp": 1734189297, "value": "0.013999999999941792" }, { "timestamp": 1734189447, "value": "0.011333333333216919" }, { "timestamp": 1734189597, "value": "0.014666666666744276" }, { "timestamp": 1734189747, "value": "0.012000000000019403" }, { "timestamp": 1734189897, "value": "0.013999999999941792" }, { "timestamp": 1734190047, "value": "0.025333333333158712" }, { "timestamp": 1734190197, "value": "0.029333333333488552" }, { "timestamp": 1734190347, "value": "0.011333333333701982" }, { "timestamp": 1734190497, "value": "0.013999999999941792" }, { "timestamp": 1734190647, "value": "0.012000000000019403" }, { "timestamp": 1734190797, "value": "0.01533333333354676" }, { "timestamp": 1734190947, "value": "0.012000000000019403" }, { "timestamp": 1734191097, "value": "0.014666666666744276" }, { "timestamp": 1734191247, "value": "0.012000000000019403" }, { "timestamp": 1734191397, "value": "0.03866666666678308" }, { "timestamp": 1734191547, "value": "0.03400000000013582" }, { "timestamp": 1734191697, "value": "0.04133333333350796" }, { "timestamp": 1734191847, "value": "0.0346666666669383" }, { "timestamp": 1734191997, "value": "0.03666666666637563" }, { "timestamp": 1734192147, "value": "0.029999999999805974" }, { "timestamp": 1734192297, "value": "0.03133333333292588" }, { "timestamp": 1734192447, "value": "0.03933333333358557" } ] } ] }, { "expr": "irate(process_cpu_seconds_total{ job=\"tidb\",}[1m])", "legend": "{instance}", "result": [ { "metric": { "instance": "tidb-2bd7c6e1/172.18.3.5:10080", "sqlType": "", "type": "", "result": "", "txnMode": "", "job": "tidb", "device": "", "fstype": "", "mountpoint": "", "module": "", "kind": "", "ping": "" }, "values": [ { "timestamp": 1734188847, "value": "0.017999999999786572" }, { "timestamp": 1734188997, "value": "0.014666666666744276" }, { "timestamp": 1734189147, "value": "0.017333333332984088" }, { "timestamp": 1734189297, "value": "0.014666666666259213" }, { "timestamp": 1734189447, "value": "0.017999999999786572" }, { "timestamp": 1734189597, "value": "0.014666666666744276" }, { "timestamp": 1734189747, "value": "0.016666666666666666" }, { "timestamp": 1734189897, "value": "0.01533333333354676" }, { "timestamp": 1734190047, "value": "0.017999999999786572" }, { "timestamp": 1734190197, "value": "0.013999999999941792" }, { "timestamp": 1734190347, "value": "0.018000000000271636" }, { "timestamp": 1734190497, "value": "0.014666666666744276" }, { "timestamp": 1734190647, "value": "0.03733333333317811" }, { "timestamp": 1734190797, "value": "0.01533333333354676" }, { "timestamp": 1734190947, "value": "0.01733333333346915" }, { "timestamp": 1734191097, "value": "0.015333333333061697" }, { "timestamp": 1734191247, "value": "0.01866666666707412" }, { "timestamp": 1734191397, "value": "0.015999999999864182" }, { "timestamp": 1734191547, "value": "0.01933333333339154" }, { "timestamp": 1734191697, "value": "0.01533333333354676" }, { "timestamp": 1734191847, "value": "0.017999999999786572" }, { "timestamp": 1734191997, "value": "0.014666666666744276" }, { "timestamp": 1734192147, "value": "0.018000000000271636" }, { "timestamp": 1734192297, "value": "0.014666666666744276" }, { "timestamp": 1734192447, "value": "0.017999999999786572" } ] } ] } ] } ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/slow-query-detail.json ================================================ { "digest": "877ddf60c6084ae30353cc484f375e5159c231f0d9363213d1d1cc2ffbd272ce", "query": "SELECT *, FIELD(LOWER(A.TYPE), 'tiflash', 'tikv', 'pd', 'tidb') AS _ORDER FROM ( SELECT TYPE, INSTANCE, DEVICE_TYPE, DEVICE_NAME, JSON_OBJECTAGG(NAME, VALUE) AS JSON_VALUE FROM INFORMATION_SCHEMA.CLUSTER_LOAD WHERE DEVICE_TYPE IN (?,?) GROUP BY TYPE, INSTANCE, DEVICE_TYPE, DEVICE_NAME ) AS A ORDER BY _ORDER DESC, INSTANCE, DEVICE_TYPE, DEVICE_NAME [arguments: (\"memory\", \"cpu\")];", "instance": "127.0.0.1:10080", "db": "", "connection_id": "5414958427354957035", "success": 1, "timestamp": 1690272708.823209, "query_time": 1.51515176, "parse_time": 0, "compile_time": 0.00103503, "rewrite_time": 0.000332248, "preproc_subqueries_time": 0, "optimize_time": 0.000354698, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0.000068678, "exec_retry_time": 0, "memory_max": 220680, "disk_max": 0, "txn_start_ts": "0", "prev_stmt": "", "plan": "\tid \ttask\testRows\toperator info \tactRows\texecution info \tmemory \tdisk\n\tSort_7 \troot\t8000 \tColumn#8:desc, Column#2, Column#3, Column#4 \t12 \ttime:1.51s, loops:2 \t18.4 KB \t0 Bytes\n\t└─Projection_9 \troot\t8000 \tColumn#1, Column#2, Column#3, Column#4, Column#7, field(lower(Column#1), tiflash, tikv, pd, tidb)-\u003eColumn#8 \t12 \ttime:1.51s, loops:6, Concurrency:5 \t33.8 KB \tN/A\n\t └─HashAgg_10 \troot\t8000 \tgroup by:Column#1, Column#2, Column#3, Column#4, funcs:json_objectagg(Column#5, Column#6)-\u003eColumn#7, funcs:firstrow(Column#1)-\u003eColumn#1, funcs:firstrow(Column#2)-\u003eColumn#2, funcs:firstrow(Column#3)-\u003eColumn#3, funcs:firstrow(Column#4)-\u003eColumn#4\t12 \ttime:1.51s, loops:6, partial_worker:{wall_time:1.513240352s, concurrency:5, task_num:1, tot_wait:7.565194284s, tot_exec:149.691µs, tot_time:7.56536502s, max:1.513153823s, p95:1.513153823s}, final_worker:{wall_time:1.5133523s, concurrency:5, task_num:5, tot_wait:7.566142195s, tot_exec:223.043µs, tot_time:7.566371614s, max:1.513316486s, p95:1.513316486s}\t47.4 KB \tN/A\n\t └─Selection_11 \troot\t8000 \tin(Column#3, \"memory\", \"cpu\") \t69 \ttime:1.51s, loops:2 \t135.2 KB\tN/A\n\t └─MemTableScan_12\troot\t10000 \ttable:CLUSTER_LOAD \t1193 \ttime:1.51s, loops:3 \tN/A \tN/A", "binary_plan": "{\"discardedDueToTooLong\":false,\"main\":{\"accessObjects\":[],\"actRows\":12,\"children\":[{\"accessObjects\":[],\"actRows\":12,\"children\":[{\"accessObjects\":[],\"actRows\":12,\"children\":[{\"accessObjects\":[],\"actRows\":69,\"children\":[{\"accessObjects\":[{\"scanObject\":{\"database\":\"INFORMATION_SCHEMA\",\"table\":\"CLUSTER_LOAD\"}}],\"actRows\":1193,\"copExecInfo\":{},\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"1.51s\",\"estRows\":10000,\"labels\":[],\"memoryBytes\":\"N/A\",\"name\":\"MemTableScan_12\",\"rootBasicExecInfo\":{\"loops\":\"3\",\"time\":\"1.51s\"},\"rootGroupExecInfo\":[],\"storeType\":\"tidb\",\"taskType\":\"root\"}],\"copExecInfo\":{},\"cost\":499000,\"diagnosis\":[\"high_est_error\"],\"diskBytes\":\"N/A\",\"duration\":\"1.51s\",\"estRows\":8000,\"labels\":[],\"memoryBytes\":\"138400\",\"name\":\"Selection_11\",\"operatorInfo\":\"in(Column#3, \\\"memory\\\", \\\"cpu\\\")\",\"rootBasicExecInfo\":{\"loops\":\"2\",\"time\":\"1.51s\"},\"rootGroupExecInfo\":[],\"storeType\":\"tidb\",\"taskType\":\"root\"}],\"copExecInfo\":{},\"cost\":1772970.6,\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"1.51s\",\"estRows\":8000,\"labels\":[],\"memoryBytes\":\"48580\",\"name\":\"HashAgg_10\",\"operatorInfo\":\"group by:Column#1, Column#2, Column#3, Column#4, funcs:json_objectagg(Column#5, Column#6)-\\u003eColumn#7, funcs:firstrow(Column#1)-\\u003eColumn#1, funcs:firstrow(Column#2)-\\u003eColumn#2, funcs:firstrow(Column#3)-\\u003eColumn#3, funcs:firstrow(Column#4)-\\u003eColumn#4\",\"rootBasicExecInfo\":{\"loops\":\"6\",\"time\":\"1.51s\"},\"rootGroupExecInfo\":[{\"final_worker\":{\"concurrency\":\"5\",\"max\":\"1.513316486s\",\"p95\":\"1.513316486s\",\"task_num\":\"5\",\"tot_exec\":\"223.043µs\",\"tot_time\":\"7.566371614s\",\"tot_wait\":\"7.566142195s\",\"wall_time\":\"1.5133523s\"},\"partial_worker\":{\"concurrency\":\"5\",\"max\":\"1.513153823s\",\"p95\":\"1.513153823s\",\"task_num\":\"1\",\"tot_exec\":\"149.691µs\",\"tot_time\":\"7.56536502s\",\"tot_wait\":\"7.565194284s\",\"wall_time\":\"1.513240352s\"}}],\"storeType\":\"tidb\",\"taskType\":\"root\"}],\"copExecInfo\":{},\"cost\":1856802.6,\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"1.51s\",\"estRows\":8000,\"labels\":[],\"memoryBytes\":\"34596\",\"name\":\"Projection_9\",\"operatorInfo\":\"Column#1, Column#2, Column#3, Column#4, Column#7, field(lower(Column#1), tiflash, tikv, pd, tidb)-\\u003eColumn#8\",\"rootBasicExecInfo\":{\"loops\":\"6\",\"time\":\"1.51s\"},\"rootGroupExecInfo\":[{\"Concurrency\":\"5\"}],\"storeType\":\"tidb\",\"taskType\":\"root\"}],\"copExecInfo\":{},\"cost\":7403943.686437106,\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"1.51s\",\"estRows\":8000,\"labels\":[],\"memoryBytes\":\"18792\",\"name\":\"Sort_7\",\"operatorInfo\":\"Column#8:desc, Column#2, Column#3, Column#4\",\"rootBasicExecInfo\":{\"loops\":\"2\",\"time\":\"1.51s\"},\"rootGroupExecInfo\":[],\"storeType\":\"tidb\",\"taskType\":\"root\"},\"withRuntimeStats\":true}", "binary_plan_text": "\n| id | estRows | estCost | actRows | task | access object | execution info | operator info | memory | disk |\n| Sort_7 | 8000.00 | 7403943.69 | 12 | root | | time:1.51s, loops:2 | Column#8:desc, Column#2, Column#3, Column#4 | 18.4 KB | 0 Bytes |\n| └─Projection_9 | 8000.00 | 1856802.60 | 12 | root | | time:1.51s, loops:6, Concurrency:5 | Column#1, Column#2, Column#3, Column#4, Column#7, field(lower(Column#1), tiflash, tikv, pd, tidb)-\u003eColumn#8 | 33.8 KB | N/A |\n| └─HashAgg_10 | 8000.00 | 1772970.60 | 12 | root | | time:1.51s, loops:6, partial_worker:{wall_time:1.513240352s, concurrency:5, task_num:1, tot_wait:7.565194284s, tot_exec:149.691µs, tot_time:7.56536502s, max:1.513153823s, p95:1.513153823s}, final_worker:{wall_time:1.5133523s, concurrency:5, task_num:5, tot_wait:7.566142195s, tot_exec:223.043µs, tot_time:7.566371614s, max:1.513316486s, p95:1.513316486s} | group by:Column#1, Column#2, Column#3, Column#4, funcs:json_objectagg(Column#5, Column#6)-\u003eColumn#7, funcs:firstrow(Column#1)-\u003eColumn#1, funcs:firstrow(Column#2)-\u003eColumn#2, funcs:firstrow(Column#3)-\u003eColumn#3, funcs:firstrow(Column#4)-\u003eColumn#4 | 47.4 KB | N/A |\n| └─Selection_11 | 8000.00 | 499000.00 | 69 | root | | time:1.51s, loops:2 | in(Column#3, \"memory\", \"cpu\") | 135.2 KB | N/A |\n| └─MemTableScan_12 | 10000.00 | 0.00 | 1193 | root | table:CLUSTER_LOAD | time:1.51s, loops:3 | | N/A | N/A |\n", "warnings": [ { "Level": "Warning", "Message": "skip prepared plan-cache: PhysicalMemTable plan is un-cacheable" } ], "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 1, "plan_from_cache": 0, "plan_from_binding": 0, "user": "root", "host": "127.0.0.1", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0 } ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/slow-query-list.json ================================================ { "data": [ { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568085", "success": 0, "timestamp": 1731395083.834432, "query_time": 42.813718709, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731395676.372656, "query_time": 36.06388146, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567961", "success": 0, "timestamp": 1731395492.146849, "query_time": 31.88501126, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731395911.519522, "query_time": 31.13266307, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731396090.233227, "query_time": 29.793699554, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "query": "SELECT MAX(`stackoverflow`.`users`.`last_modified_date`) FROM `stackoverflow`.`users`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567965", "success": 0, "timestamp": 1731394720.186855, "query_time": 29.674765804, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1335, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567969", "success": 0, "timestamp": 1731394886.522594, "query_time": 25.564702573, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567969", "success": 0, "timestamp": 1731395125.504682, "query_time": 25.427689653, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731394705.139473, "query_time": 24.193893241, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567975", "success": 0, "timestamp": 1731395453.978445, "query_time": 23.970207449, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "69667039b51ac90afcda120edbbd61cb1f5db69fdde9314d56a9a94c027074c2", "query": "INSERT INTO `github_events` (`repo_id`,`repo_name`,`language`,`actor_id`,`actor_login`,`additions`,`deletions`,`action`,`commit_id`,`number`,`org_id`,`org_login`,`pr_merged`,`state`,`pr_merged_at`,`closed_at`,`comments`,`pr_or_issue_id`,`pr_changed_files`,`pr_review_comments`,`event_day`,`event_month`,`event_year`,`push_size`,`push_distinct_size`,`id`,`type`,`created_at`,`pr_or_issue_created_at`,`creator_user_id`,`creator_user_login`) VALUES (841416687, 'DigistoreAdmin/backend_v2', '', 120089549, 'Fibin-kummil', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 4, 1, 43726827610, 'PushEvent', '2024-11-12 06:00:00', '1970-01-01 00:00:00', 0, ''), (882610803, 'domi00248/upload2', '', 172067999, 'domi00248', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43726827623, 'PushEvent', '2024-11-12 06:00:00', '1970-01-01 00:00:00', 0, ''), (730591456, 'RekhaAparna01/openpower-vpd-parser', '', 139200172, 'RekhaAparna01', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 0, 0, 43726827625, 'DeleteEvent', '2024-11-12 06:00:00', '1970-01-01 00:00:00', 0, ''), (813161388, 'husnapupita/WedusKripto18', '', 41898282, 'github-actions[bot]', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43726827627, 'PushEvent', '2024-11-12 06:00:00', '1970-01-01 00:00:00', 0, ''), (885612399, 'nutters1000/tsto-mayhemids-5', '', 41898282, 'github-actions[bot]', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43726827648, 'PushEvent', '2024-11-12 06:00:00', '1970-01-01 00:00:00', 0, ''), (718389500, 'yuuk-64-gi-0/unofficial_LiveSchedule_Data', '', 83411350, 'yuuk-64-gi-0', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43726827652, 'PushEvent', '2024-11-12 06:00:00', '1970-01-01 00:00:00', 0, ''), (652340321, 'morgenm/basicgopot', '', 65553080, 'codecov-commenter', 0, 0, 'created', '', 59, 0, '', FALSE, 'open', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 1, 2651133191, 0, 0, '2024-11-12', '2024-11-01', 2024, 0, 0, 43726827662, 'IssueCommentEvent', '2024-11-12 06:00:00', '2024-11-12 05:58:54', 65553080, 'codecov-commenter'), (851017538, 'muazhari/dti-se', '', 39398937, 'muazhari', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43726827690, 'PushEvent', '2024-11-12 06:00:00', '1970-01-01 00:00:00', 0, ''), (818342606, 'wusiuyu/hknews', '', 23726356, 'wusiuyu', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43726827697, 'PushEvent', '2024-11-12 06:00:00', '1970-01-01 00:00:00', 0, ''), (863361087, 'Happy-pixelpulse/remdy', '', 152676310, 'Happy-pixelpulse', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43726827702, 'PushEvent', '2024-11-12 06:00:00', '1970-01-01 00:00:00', 0, ''), (878375686, 'kysports-cn/kytiyuhui', '', 41898282, 'github-actions[bot]', 0, 0, 'opened', '', 87154, 0, '', FALSE, 'open', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 2651134931, 0, 0, '2024-11-12', '2024-11-01', 2024, 0, 0, 43726827711, 'IssuesEvent', '2024-11-12 06:00:00', '2024-11-12 05:59:59', 41898282, 'github-actions[bot]'), (880063275, 'matthewlu2/plot_spatial', '', 135487014, 'matthewlu2', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43726827713, 'PushEvent', '2024-11-12 06:00:00', '1970-01-01 00:00:00', 0, ''), (887073784, 'erwinfauzy/erwinfauzy', '', 148672470, 'erwinfauzy', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-0(len:26915689);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396162.887493, "query_time": 23.02075085, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 492977264, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "14409372a64bb05b14c68b644eaf2e7559cde6adee1f0208b8e38cf27d0bc4ac", "query": "INSERT INTO `github_events` (`repo_id`,`repo_name`,`language`,`actor_id`,`actor_login`,`additions`,`deletions`,`action`,`commit_id`,`number`,`org_id`,`org_login`,`pr_merged`,`state`,`pr_merged_at`,`closed_at`,`comments`,`pr_or_issue_id`,`pr_changed_files`,`pr_review_comments`,`event_day`,`event_month`,`event_year`,`push_size`,`push_distinct_size`,`id`,`type`,`created_at`,`pr_or_issue_created_at`,`creator_user_id`,`creator_user_login`) VALUES (859113880, 'buituandev/FashionWebProject', '', 147406431, 'buituandev', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388870, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (373797896, 'Johnsondhoni07/Johnsondhoni07', '', 41898282, 'github-actions[bot]', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388882, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (575019540, 'cpp-johnny/cpp-johnny', '', 41898282, 'github-actions[bot]', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388885, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (457470784, 'sebast759/sebast759.github.io', '', 41928138, 'sebast759', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388888, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (684869383, '37xxxphillipsce/one_green', '', 41898282, 'github-actions[bot]', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388895, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (677255002, 'dgfdacd/dgfdacd', '', 41898282, 'github-actions[bot]', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388904, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (167107390, 'lets-mica/mica', '', 30398684, 'yu-wade', 0, 0, '', '', 0, 46373530, 'lets-mica', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 0, 0, 43727388909, 'ForkEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (887067611, 'gyu-iin/CreatAi', '', 188141647, 'gyu-iin', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388917, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (381137107, 'The-Forbidden-Trove/character_name_blacklist', '', 75438007, 'Jzir', 0, 0, '', '', 0, 74618880, 'The-Forbidden-Trove', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388922, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (811116430, 'Tunan81/weread2notion-pro', '', 41898282, 'github-actions[bot]', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388936, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (778600850, 'mattapong/lava-rpc', '', 41898282, 'github-actions[bot]', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388951, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (205775816, 'madoodia/Configs', '', 3639157, 'madoodia', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727388953, 'PushEvent', '2024-11-12 06:24:54', '1970-01-01 00:00:00', 0, ''), (795909164, 'odczynflnpm/iste-ad-facilis', '', 41898282, 'github-actions[bot]', 0, 0, '', '', 0, 167699334, 'odczynflnpm', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12',(len:27432718);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396210.970711, "query_time": 20.506392327, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 493337264, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568085", "success": 0, "timestamp": 1731395300.460164, "query_time": 20.321525171, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cc17b10e96dd94b2ec106c9733b6168ec984890773d5cade120f17f6f09ebb4b", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 12 HOUR) ORDER BY record_time DESC LIMIT 1 ), cte AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS current_events_total FROM github_events ) SELECT current_max_event_time AS record_time, current_events_total - COALESCE((SELECT last_events_total FROM last_record), 0) AS events_increment, current_events_total AS events_total FROM cte ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568085", "success": 0, "timestamp": 1731394820.386959, "query_time": 19.428609419, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 1, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567959", "success": 0, "timestamp": 1731395929.105154, "query_time": 19.096130682, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567961", "success": 0, "timestamp": 1731395714.338041, "query_time": 13.976654642, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731395413.97186, "query_time": 13.805915836, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567961", "success": 0, "timestamp": 1731394994.788962, "query_time": 13.803781269, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731394934.205035, "query_time": 13.245276475, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567969", "success": 0, "timestamp": 1731395353.12341, "query_time": 12.953681993, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568085", "success": 0, "timestamp": 1731395772.752351, "query_time": 12.404848074, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567969", "success": 0, "timestamp": 1731395832.209703, "query_time": 11.699750477, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567975", "success": 0, "timestamp": 1731395321.654226, "query_time": 11.645562838, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "02bcfcb6b50a132f26b2722e9b822039ddd7eed8257c9f8cf8c6a3a9b5572fe3", "query": "WITH repos_with_prs_24h AS ( SELECT /*+ READ_FROM_STORAGE(tiflash[ge]) */ ge.repo_id, COUNT(DISTINCT IF(action = 'opened', ge.pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND ge.pr_merged = false, ge.pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND ge.pr_merged = true, ge.pr_or_issue_id, NULL)) AS merged_prs, COUNT(*) AS total_pr_events, COUNT(DISTINCT actor_id) AS developers FROM github_events ge WHERE ge.type = 'PullRequestEvent' AND (ge.action = 'opened' OR ge.action = 'closed') AND ge.additions \u003e 10 AND ge.created_at \u003e DATE_SUB(NOW(), INTERVAL 1 DAY) AND ge.repo_name NOT IN( 'NixOS/nixpkgs', 'firstcontributions/first-contributions', 'Homebrew/homebrew-cask', 'microsoft/winget-pkgs', 'microsoft/vcpkg' ) AND ge.actor_login NOT IN( 'robodoo', 'r-ryantm', 'scala-steward' ) AND LOWER(ge.actor_login) NOT REGEXP '^(bot-.+|.+bot|.+\\\\[bot\\\\]|.*-bot-.*|robot-.+|.+-ci-.+|.+-ci|.+-testing|.*clabot.*|.+-gerrit|k8s-.+|.+-machine|.+-automation|github-.+|.+-github|.+-service|.+-builds|codecov-.+|.*teamcity.*|jenkins-.+|.+-jira-.+|witness.+|.+witness|signcla.+|.+signcla|.+-cicd-.+|.*autotester.*)$' GROUP BY ge.repo_id HAVING developers \u003e 5 ORDER BY total_pr_events DESC LIMIT 100 ) SELECT gr.repo_id, gr.repo_name, developers, total_pr_events, opened_prs, closed_prs, merged_prs FROM repos_with_prs_24h r JOIN github_repos gr ON r.repo_id = gr.repo_id WHERE gr.stars \u003e 100 ORDER BY total_pr_events DESC LIMIT 5;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567959", "success": 0, "timestamp": 1731394941.60428, "query_time": 11.59451965, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 191124, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 100, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568043", "success": 0, "timestamp": 1731396040.969341, "query_time": 10.959927937, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "8df59f50fb66c4473c5a484fc95287cd5776718a8ad2f2d22b673a8700277569", "query": "WITH developers_with_prs_24h AS ( SELECT /*+ READ_FROM_STORAGE(tiflash[ge]) */ ge.actor_id, COUNT(DISTINCT CASE WHEN action = 'opened' THEN ge.pr_or_issue_id END) AS opened_prs, COUNT(DISTINCT CASE WHEN action = 'closed' AND ge.pr_merged = false THEN ge.pr_or_issue_id END) AS closed_prs, COUNT(DISTINCT CASE WHEN action = 'closed' AND ge.pr_merged = true THEN ge.pr_or_issue_id END) AS merged_prs, COUNT(*) AS total_pr_events FROM github_events ge WHERE ge.type = 'PullRequestEvent' AND (ge.action = 'opened' OR ge.action = 'closed') AND ge.created_at \u003e DATE_SUB(NOW(), INTERVAL 1 DAY) AND ge.actor_login NOT IN (SELECT login FROM blacklist_users) AND ge.actor_login NOT REGEXP '^(bot-.+|.+bot|.+\\\\[bot\\\\]|.+-bot-.+|robot-.+|.+-ci-.+|.+-ci|.+-testing|.+clabot.+|.+-gerrit|k8s-.+|.+-machine|.+-automation|.+-sdk|.+-integration|github-.+|.+-github|.+-service|.+-builds|codecov-.+|.+teamcity.+|jenkins-.+|.+-jira-.+|witness.+|.+witness|signcla.+|.+signcla|.+-cicd-.+|.+autotester.+)$' AND ge.additions \u003e 10 GROUP BY ge.actor_id ORDER BY total_pr_events DESC ) SELECT gu.id AS actor_id, gu.login AS actor_login, d.total_pr_events, d.opened_prs, d.closed_prs, d.merged_prs FROM developers_with_prs_24h d JOIN github_users gu ON gu.id = d.actor_id WHERE gu.followers \u003e 5 ORDER BY total_pr_events DESC LIMIT 5;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568111", "success": 0, "timestamp": 1731394880.440971, "query_time": 10.411373758, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4608326, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 998, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568111", "success": 0, "timestamp": 1731395560.413131, "query_time": 10.404900406, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567975", "success": 0, "timestamp": 1731395679.850997, "query_time": 9.842890208, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "fd11911b34dd73778e4ff46ebab8cb132b2f2cc11be66a7296de48055061ac83", "query": "INSERT INTO `github_events` (`repo_id`,`repo_name`,`language`,`actor_id`,`actor_login`,`additions`,`deletions`,`action`,`commit_id`,`number`,`org_id`,`org_login`,`pr_merged`,`state`,`pr_merged_at`,`closed_at`,`comments`,`pr_or_issue_id`,`pr_changed_files`,`pr_review_comments`,`event_day`,`event_month`,`event_year`,`push_size`,`push_distinct_size`,`id`,`type`,`created_at`,`pr_or_issue_created_at`,`creator_user_id`,`creator_user_login`) VALUES (884180087, 'hthsports-cn/hthtiyuhth', '', 41898282, 'github-actions[bot]', 0, 0, 'opened', '', 33500, 0, '', FALSE, 'open', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 2651222208, 0, 0, '2024-11-12', '2024-11-01', 2024, 0, 0, 43727937790, 'IssuesEvent', '2024-11-12 06:47:50', '2024-11-12 06:47:48', 41898282, 'github-actions[bot]'), (599412601, 'binhle59/playwright-saucedemo.com', '', 92701382, 'binhle59', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727937814, 'PushEvent', '2024-11-12 06:47:50', '1970-01-01 00:00:00', 0, ''), (733377750, 'prepheadrus/A_06', '', 83468174, 'prepheadrus', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727937816, 'PushEvent', '2024-11-12 06:47:50', '1970-01-01 00:00:00', 0, ''), (884235033, 'YogaFerdiansya/Final-Project_MySkill', '', 186820366, 'YogaFerdiansya', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727937817, 'PushEvent', '2024-11-12 06:47:50', '1970-01-01 00:00:00', 0, ''), (884361731, 'jnhsports-cn/jnhtiyu-game', '', 41898282, 'github-actions[bot]', 0, 0, 'opened', '', 31640, 0, '', FALSE, 'open', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 2651222207, 0, 0, '2024-11-12', '2024-11-01', 2024, 0, 0, 43727937823, 'IssuesEvent', '2024-11-12 06:47:50', '2024-11-12 06:47:48', 41898282, 'github-actions[bot]'), (884181201, 'hthsports-cn/hthtiyuj9', '', 41898282, 'github-actions[bot]', 0, 0, 'opened', '', 33408, 0, '', FALSE, 'open', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 2651222210, 0, 0, '2024-11-12', '2024-11-01', 2024, 0, 0, 43727937835, 'IssuesEvent', '2024-11-12 06:47:50', '2024-11-12 06:47:48', 41898282, 'github-actions[bot]'), (886529506, 'teknik-github/nginx-kubernetes', '', 136105599, 'teknik-github', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727937837, 'PushEvent', '2024-11-12 06:47:50', '1970-01-01 00:00:00', 0, ''), (885795523, 'slerman12/BrokenWisdoms-V2-in-progress', '', 9126603, 'slerman12', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727937838, 'PushEvent', '2024-11-12 06:47:50', '1970-01-01 00:00:00', 0, ''), (887068329, 'freeukapp/uk', '', 171124844, 'freeukapp', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 1, 1, 43727937841, 'PushEvent', '2024-11-12 06:47:50', '1970-01-01 00:00:00', 0, ''), (884181357, 'hthsports-cn/hthtiyusports', '', 41898282, 'github-actions[bot]', 0, 0, 'opened', '', 33081, 0, '', FALSE, 'open', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 2651222205, 0, 0, '2024-11-12', '2024-11-01', 2024, 0, 0, 43727937842, 'IssuesEvent', '2024-11-12 06:47:50', '2024-11-12 06:47:48', 41898282, 'github-actions[bot]'), (887096036, 'jiangsp/test-repo', '', 12456337, 'jiangsp', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 0, 0, 43727937855, 'CreateEvent', '2024-11-12 06:47:50', '1970-01-01 00:00:00', 0, ''), (636962086, 'bitomic/cronstruct', '', 29139614, 'renovate[bot]', 0, 0, '', '', 0, 0, '', FALSE, '', '1970-01-01 00:00:00', '1970-01-01 00:00:00', 0, 0, 0, 0, '2024-11-12', '2024-11-01', 2024, 2, 1, 43727937871, 'PushEvent', '2024-11-12 06:47:50', '1970-01-01 00:00:00', 0, ''), (549661928, 'lightsats(len:13163817);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396235.93062, "query_time": 9.720509906, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 268427264, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568043", "success": 0, "timestamp": 1731395799.041102, "query_time": 9.032250736, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568043", "success": 0, "timestamp": 1731395078.826332, "query_time": 8.8163619, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568111", "success": 0, "timestamp": 1731394717.1922, "query_time": 7.182756459, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567961", "success": 0, "timestamp": 1731395946.467787, "query_time": 6.046596884, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731396005.952455, "query_time": 5.467857452, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568111", "success": 0, "timestamp": 1731394955.416653, "query_time": 5.407257378, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1860, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731396125.741657, "query_time": 5.297103595, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731396185.526266, "query_time": 5.07917745, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "query": "SELECT MAX(`stackoverflow`.`users`.`last_modified_date`) FROM `stackoverflow`.`users`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567965", "success": 0, "timestamp": 1731395051.318045, "query_time": 4.682126417, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1331, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "f75457b4116b4eccab69e99d34bf6f1669feb167d818bacfc9f20cb8b642dfde", "query": "select id,title,status,user_id,query_sql,result,created_at from explorer_questions where unix_timestamp(created_at) between 1731392644 and 1731394451 limit 0, 100;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724570355", "success": 0, "timestamp": 1731394756.951821, "query_time": 4.425153747, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 381221885, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.readonly", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 37454, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c980164ab6815c14e9af370c01d70c07627c89fef262f8b088bb4c83f15051ac", "query": "SELECT MAX(`stackoverflow`.`comments`.`creation_date`) FROM `stackoverflow`.`comments`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569861", "success": 0, "timestamp": 1731395642.640544, "query_time": 4.318355414, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568043", "success": 0, "timestamp": 1731395194.032638, "query_time": 4.022916922, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567969", "success": 0, "timestamp": 1731394624.795916, "query_time": 3.857108275, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568111", "success": 0, "timestamp": 1731396153.419079, "query_time": 3.409466191, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567961", "success": 0, "timestamp": 1731395223.288161, "query_time": 3.167641698, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731395163.237968, "query_time": 3.14813826, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568085", "success": 0, "timestamp": 1731395522.865902, "query_time": 2.623152299, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567969", "success": 0, "timestamp": 1731395582.691315, "query_time": 2.446044816, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "be8d3f0f91956f4da4dd19972a61dd3a7d478ff60c67cfe863e1ab52a73ea48b", "query": "SELECT actor_login, COUNT(*) AS events FROM github_events ge WHERE repo_id = 41986369 AND ( (type = 'PullRequestEvent' AND action = 'opened') OR (type = 'IssuesEvent' AND action = 'opened') OR (type = 'IssueCommentEvent' AND action = 'created') OR (type = 'PullRequestReviewEvent' AND action = 'created') OR (type = 'PullRequestReviewCommentEvent' AND action = 'created') OR (type = 'PushEvent' AND action = '') ) AND actor_login NOT LIKE '%bot' AND actor_login NOT LIKE '%[bot]' AND actor_login NOT IN (SELECT login FROM blacklist_users bu) GROUP BY actor_login ORDER BY events DESC LIMIT 200;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724570953", "success": 0, "timestamp": 1731395253.868425, "query_time": 1.7582347390000002, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 14108752, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 715631, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c980164ab6815c14e9af370c01d70c07627c89fef262f8b088bb4c83f15051ac", "query": "SELECT MAX(`stackoverflow`.`comments`.`creation_date`) FROM `stackoverflow`.`comments`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569861", "success": 0, "timestamp": 1731395241.950735, "query_time": 1.585105232, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "28994526549b36b499a9c31f545c6b5d99df9823651bcc69b8080e9d77ef1c8f", "query": "DELETE FROM `event_logs` WHERE (created_at \u003c= '2024-11-12 07:16:03.455697') LIMIT 100000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572875", "success": 0, "timestamp": 1731396065.084168, "query_time": 1.469898559, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 5450866, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 813142, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568085", "success": 0, "timestamp": 1731394742.332813, "query_time": 1.384022751, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "query": "SELECT MAX(`stackoverflow`.`users`.`last_modified_date`) FROM `stackoverflow`.`users`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567965", "success": 0, "timestamp": 1731394843.499413, "query_time": 1.369542649, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1333, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c980164ab6815c14e9af370c01d70c07627c89fef262f8b088bb4c83f15051ac", "query": "SELECT MAX(`stackoverflow`.`comments`.`creation_date`) FROM `stackoverflow`.`comments`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569861", "success": 0, "timestamp": 1731395456.173311, "query_time": 1.365610137, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "query": "SELECT MAX(`stackoverflow`.`users`.`last_modified_date`) FROM `stackoverflow`.`users`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567965", "success": 0, "timestamp": 1731396028.246033, "query_time": 1.309044044, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1333, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "28994526549b36b499a9c31f545c6b5d99df9823651bcc69b8080e9d77ef1c8f", "query": "DELETE FROM `event_logs` WHERE (created_at \u003c= '2024-11-12 07:09:02.923132') LIMIT 100000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572057", "success": 0, "timestamp": 1731395644.595067, "query_time": 1.293512121, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 5450065, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 788894, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "query": "SELECT MAX(`stackoverflow`.`users`.`last_modified_date`) FROM `stackoverflow`.`users`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567965", "success": 0, "timestamp": 1731394650.302318, "query_time": 1.282091282, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1333, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "28994526549b36b499a9c31f545c6b5d99df9823651bcc69b8080e9d77ef1c8f", "query": "DELETE FROM `event_logs` WHERE (created_at \u003c= '2024-11-12 07:02:03.428522') LIMIT 100000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724571329", "success": 0, "timestamp": 1731395225.057753, "query_time": 1.182898929, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 6385875, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 764696, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "1f1f79dc86b52642303cfba02d397ce6239dc00add3917b979bf1d76f23da47e", "query": "WITH pr_with_merged_at AS ( SELECT number, DATE_FORMAT(created_at, '%Y-%m-01') AS t_month, created_at AS merged_at FROM github_events ge WHERE type = 'PullRequestEvent' -- Considering that some repositories accept the code of the contributor by closing the PR and push commit directly, -- here is not distinguished whether it is the merged event. -- See: https://github.com/mongodb/mongo/pulls?q=is%3Apr+is%3Aclosed AND action = 'closed' AND repo_id = 724712 ), pr_with_opened_at AS ( SELECT number, created_at AS opened_at FROM github_events ge WHERE type = 'PullRequestEvent' AND action = 'opened' -- Exclude Bots -- AND actor_login NOT LIKE '%bot%' -- AND actor_login NOT IN (SELECT login FROM blacklist_users bu) AND repo_id = 724712 ), tdiff AS ( SELECT t_month, (UNIX_TIMESTAMP(pwm.merged_at) - UNIX_TIMESTAMP(pwo.opened_at)) AS diff FROM pr_with_opened_at pwo JOIN pr_with_merged_at pwm ON pwo.number = pwm.number AND pwm.merged_at \u003e pwo.opened_at ), tdiff_with_rank AS ( SELECT tdiff.t_month, diff / 60 / 60 AS diff, ROW_NUMBER() OVER (PARTITION BY tdiff.t_month ORDER BY diff) AS r, COUNT(*) OVER (PARTITION BY tdiff.t_month) AS cnt, FIRST_VALUE(diff / 60 / 60) OVER (PARTITION BY tdiff.t_month ORDER BY diff) AS p0, FIRST_VALUE(diff / 60 / 60) OVER (PARTITION BY tdiff.t_month ORDER BY diff DESC) AS p100 FROM tdiff ), tdiff_p25 AS ( SELECT t_month, diff AS p25 FROM tdiff_with_rank tr WHERE r = ROUND(cnt * 0.25) ), tdiff_p50 AS ( SELECT t_month, diff AS p50 FROM tdiff_with_rank tr WHERE r = ROUND(cnt * 0.5) ), tdiff_p75 AS ( SELECT t_month, diff AS p75 FROM tdiff_with_rank tr WHERE r = ROUND(cnt * 0.75) ) SELECT tr.t_month AS event_month, p0, p25, p50, p75, p100 FROM tdiff_with_rank tr LEFT JOIN tdiff_p25 p25 ON tr.t_month = p25.t_month LEFT JOIN tdiff_p50 p50 ON tr.t_month = p50.t_month LEFT JOIN tdiff_p75 p75 ON tr.t_month = p75.t_month WHERE r = 1 ORDER BY event_month ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569263", "success": 0, "timestamp": 1731395414.605844, "query_time": 1.098174003, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 39824163, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 147573, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c980164ab6815c14e9af370c01d70c07627c89fef262f8b088bb4c83f15051ac", "query": "SELECT MAX(`stackoverflow`.`comments`.`creation_date`) FROM `stackoverflow`.`comments`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569861", "success": 0, "timestamp": 1731394844.376842, "query_time": 1.045544638, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1186, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c980164ab6815c14e9af370c01d70c07627c89fef262f8b088bb4c83f15051ac", "query": "SELECT MAX(`stackoverflow`.`comments`.`creation_date`) FROM `stackoverflow`.`comments`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569861", "success": 0, "timestamp": 1731396223.352117, "query_time": 1.028242696, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1183, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "query": "INSERT INTO mv_events_total(record_time, events_increment, events_total) WITH last_record AS ( SELECT /*+ MERGE() */ record_time AS last_event_time, events_total AS last_events_total FROM mv_events_total WHERE record_time \u003e (NOW() - INTERVAL 1 HOUR) ORDER BY record_time DESC LIMIT 1 ), inc_events AS ( SELECT MAX(created_at) AS current_max_event_time, COUNT(1) AS inc_events FROM github_events ge WHERE created_at \u003e= (SELECT last_event_time FROM last_record) ) SELECT current_max_event_time AS record_time, inc_events AS new_events_increment, inc_events + (SELECT last_events_total FROM last_record) AS new_events_total FROM inc_events WHERE EXISTS(SELECT * FROM last_record) ON DUPLICATE KEY UPDATE events_increment = VALUES(events_increment), events_total = VALUES(events_total);", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567971", "success": 0, "timestamp": 1731394801.965975, "query_time": 0.996480012, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 16468, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.pipeline", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 4, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c980164ab6815c14e9af370c01d70c07627c89fef262f8b088bb4c83f15051ac", "query": "SELECT MAX(`stackoverflow`.`comments`.`creation_date`) FROM `stackoverflow`.`comments`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569861", "success": 0, "timestamp": 1731395826.337301, "query_time": 0.993633424, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "query": "SELECT MAX(`stackoverflow`.`users`.`last_modified_date`) FROM `stackoverflow`.`users`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567965", "success": 0, "timestamp": 1731394801.838705, "query_time": 0.910571422, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1326, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "b2f17178f468a7841cdd97cad6f34e6a6304b77a70230c642b58a9db9b98aa38", "query": "WITH stars_per_company AS ( SELECT IF( gu.organization_formatted IS NOT NULL AND LENGTH(gu.organization_formatted) != 0, gu.organization_formatted, 'Unknown' ) AS company_name, COUNT(DISTINCT ge.actor_login) AS stargazers FROM github_events ge LEFT JOIN github_users gu ON ge.actor_login = gu.login WHERE ge.repo_id IN (724712) AND ge.type = 'WatchEvent' AND ge.action = 'started' GROUP BY company_name ), stars_total AS ( SELECT SUM(stargazers) AS total FROM stars_per_company ) SELECT spc.company_name, spc.stargazers, spc.stargazers / st.total AS proportion FROM stars_per_company spc, stars_total st WHERE spc.company_name != 'Unknown' ORDER BY spc.stargazers DESC LIMIT 50;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568001", "success": 0, "timestamp": 1731395393.409098, "query_time": 0.864050077, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 18486024, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 208195, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "query": "SELECT MAX(`stackoverflow`.`users`.`last_modified_date`) FROM `stackoverflow`.`users`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567965", "success": 0, "timestamp": 1731395864.962533, "query_time": 0.830027895, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1331, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "bcd7135d761d7237779b21d885710f72f0757e287d7d82d37597f8a924f5cbeb", "query": "WITH group_by_org AS ( SELECT CASE WHEN (TRIM(gu.organization) = '' OR gu.organization IS NULL) THEN 'UNKNOWN' ELSE LOWER(REPLACE(gu.organization, '@', '')) END AS org_name, COUNT(DISTINCT ge.actor_login) AS stargazers FROM github_events ge LEFT JOIN github_users gu ON ge.actor_login = gu.login WHERE ge.repo_id = (SELECT repo_id FROM github_repos WHERE repo_name = CONCAT(?, '/', ?) LIMIT 1) AND ge.type = 'WatchEvent' AND ge.action = 'started' AND ge.created_at \u003e= ? AND ge.created_at \u003c= ? AND IF(? = TRUE, gu.organization != '', TRUE) GROUP BY org_name ), summary AS ( SELECT SUM(stargazers) AS total FROM group_by_org ) SELECT org_name, stargazers, stargazers / total AS percentage FROM group_by_org, summary ORDER BY stargazers DESC [arguments: (\"uutils\", \"coreutils\", \"2000-01-01\", \"2099-01-01\", 1)];", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724571205", "success": 0, "timestamp": 1731395242.070591, "query_time": 0.824970564, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 6365124, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.EXLV7STi_APIKEY", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 52081, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "b4d08c7d6845f9df9d89f57d88d4a9bfe0dd4068d1771a08051a2983d0e77ef1", "query": "WITH group_by_area AS ( SELECT gu.country_code AS country_or_area, COUNT(1) as cnt FROM github_events ge LEFT JOIN github_users gu ON ge.actor_login = gu.login WHERE repo_id IN (724712) AND ge.type = 'WatchEvent' AND ge.action = 'started' AND gu.country_code NOT IN ('', 'N/A', 'UND') GROUP BY country_or_area ), summary AS ( SELECT SUM(cnt) AS total FROM group_by_area ) SELECT country_or_area, cnt AS count, cnt / summary.total AS percentage FROM group_by_area, summary ORDER BY cnt DESC ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569997", "success": 0, "timestamp": 1731395393.389929, "query_time": 0.824057292, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 6853796, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 208195, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396065.825409, "query_time": 0.817365226, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22131613, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 41840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396078.349497, "query_time": 0.810693408, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22572520, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 211840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "f45ac600a1a4f54840eb3a5de4b4a4f8d8ba7b50626671878f3d9ccd750b271f", "query": "WITH pull_requests AS ( SELECT 724712 AS repo_id, IFNULL(COUNT(DISTINCT number), 0) AS total FROM github_events WHERE type = 'PullRequestEvent' AND repo_id = 724712 ), pull_request_creators AS ( SELECT 724712 AS repo_id, IFNULL(COUNT(DISTINCT actor_login), 0) AS total FROM github_events WHERE type = 'PullRequestEvent' AND repo_id = 724712 AND action = 'opened' ), pull_request_reviews AS ( SELECT 724712 AS repo_id, IFNULL(COUNT(1), 0) AS total FROM github_events WHERE type = 'PullRequestReviewEvent' AND repo_id = 724712 AND action = 'created' ), pull_request_reviewers AS ( SELECT 724712 AS repo_id, IFNULL(COUNT(DISTINCT actor_login), 0) AS total FROM github_events WHERE type = 'PullRequestReviewEvent' AND repo_id = 724712 AND action = 'created' ) SELECT 724712 AS repo_id, pr.total AS pull_requests, prc.total AS pull_request_creators, prr.total AS pull_request_reviews, prrc.total AS pull_request_reviewers FROM ( SELECT 724712 AS repo_id ) sub LEFT JOIN pull_requests pr ON sub.repo_id = pr.repo_id LEFT JOIN pull_request_creators prc ON sub.repo_id = prc.repo_id LEFT JOIN pull_request_reviews prr ON sub.repo_id = prr.repo_id LEFT JOIN pull_request_reviewers prrc ON sub.repo_id = prrc.repo_id ;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569997", "success": 0, "timestamp": 1731395413.995461, "query_time": 0.787534813, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 8471003, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 411670, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396070.19148, "query_time": 0.782179986, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22212212, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 101840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396068.003281, "query_time": 0.777597014, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22223182, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 71840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396076.072635, "query_time": 0.750265403, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22466287, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 181840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396075.30868, "query_time": 0.739032597, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 20989128, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 172898, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396073.087358, "query_time": 0.738901277, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22310114, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 141840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c102b8c3be5e5ae3f7591257488aa52e329c3eee03be817d49ac974705d7ef4c", "query": "SELECT gr.repo_id, gr.repo_name FROM github_repos gr WHERE gr.owner_id = 530995217 LIMIT 9999;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724568131", "success": 0, "timestamp": 1731394755.871201, "query_time": 0.736586614, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 19520, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.gh_api", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396080.492737, "query_time": 0.733412968, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 21960392, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 244682, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396072.334653, "query_time": 0.728839053, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22214549, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 131840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396074.555971, "query_time": 0.726463072, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22357299, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 161840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396082.593559, "query_time": 0.725887943, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 21139139, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 270252, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396064.994371, "query_time": 0.719222946, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22118597, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 31840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396076.804077, "query_time": 0.717092904, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22402880, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 191840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "query": "SELECT MAX(`stackoverflow`.`users`.`last_modified_date`) FROM `stackoverflow`.`users`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567965", "success": 0, "timestamp": 1731395661.18532, "query_time": 0.716139968, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1332, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "query": "SELECT MAX(`stackoverflow`.`users`.`last_modified_date`) FROM `stackoverflow`.`users`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567965", "success": 0, "timestamp": 1731396231.333459, "query_time": 0.71560644, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1333, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396073.815622, "query_time": 0.714527288, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22359513, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 151840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396077.525107, "query_time": 0.707241735, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 20769634, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 201428, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c980164ab6815c14e9af370c01d70c07627c89fef262f8b088bb4c83f15051ac", "query": "SELECT MAX(`stackoverflow`.`comments`.`creation_date`) FROM `stackoverflow`.`comments`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569861", "success": 0, "timestamp": 1731395272.815385, "query_time": 0.69981582, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "query": "SELECT MAX(`stackoverflow`.`users`.`last_modified_date`) FROM `stackoverflow`.`users`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724567965", "success": 0, "timestamp": 1731395458.115337, "query_time": 0.695422673, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1333, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396066.530357, "query_time": 0.691207091, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22240113, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 51840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396068.707627, "query_time": 0.690437198, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22291517, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 81840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396081.197017, "query_time": 0.690322873, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22569676, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 251840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c980164ab6815c14e9af370c01d70c07627c89fef262f8b088bb4c83f15051ac", "query": "SELECT MAX(`stackoverflow`.`comments`.`creation_date`) FROM `stackoverflow`.`comments`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569861", "success": 0, "timestamp": 1731394660.67075, "query_time": 0.690175173, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396071.591309, "query_time": 0.68949899, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22212310, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 121840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396064.261355, "query_time": 0.6893115, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 21930234, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 21840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396079.048383, "query_time": 0.685035091, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 21853561, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 224146, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396079.745406, "query_time": 0.683370142, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22113114, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 231509, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396070.887834, "query_time": 0.682037997, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22309224, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 111840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396069.39572, "query_time": 0.674223256, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22384852, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 91840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "2bdfb5a55c4869bfbfe70e16c45611165ca1f635c07372d2c3c8d15ab7e2fdb0", "query": "DELETE FROM `github_events` WHERE `github_events`.`created_at` BETWEEN '2024-11-12 06:00:00' AND '2024-11-12 06:59:59' LIMIT 10000;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724572873", "success": 0, "timestamp": 1731396067.211877, "query_time": 0.667787653, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 22182138, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 61840, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "c980164ab6815c14e9af370c01d70c07627c89fef262f8b088bb4c83f15051ac", "query": "SELECT MAX(`stackoverflow`.`comments`.`creation_date`) FROM `stackoverflow`.`comments`;", "instance": "", "db": "gharchive_dev", "connection_id": "3483529913724569861", "success": 0, "timestamp": 1731396040.397937, "query_time": 0.666126679, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 4096, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "3EDFHZJX5iSzvfr.etl", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" } ], "nextPageToken": "1", "totalSize": 10 } ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/statement-list.json ================================================ { "data": [ { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `mv_events_total` ( `record_time` , `events_increment` , `events_total` ) with `last_record` as ( select `record_time` as `last_event_time` , `events_total` as `last_events_total` from `mv_events_total` where `record_time` \u003e ( now ( ) - interval ? hour ) order by `record_time` desc limit ? ) , `inc_events` as ( select max ( `created_at` ) as `current_max_event_time` , count ( ? ) as `inc_events` from `github_events` `ge` where `created_at` \u003e= ( select `last_event_time` from `last_record` ) ) select `current_max_event_time` as `record_time` , `inc_events` as `new_events_increment` , `inc_events` + ( select `last_events_total` from `last_record` ) as `new_events_total` from `inc_events` where exists ( select * from `last_record` ) on duplicate key update `events_increment` = values ( `events_increment` ) , `events_total` = values ( `events_total` )", "digest": "c33c8e181899149210aaab99716ac1b1d0bb432b6721d57cbf2e5cbf00177356", "exec_count": 25, "sum_latency": 398962145114, "max_latency": 55391695564, "min_latency": 545751314, "avg_latency": 15958485804, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 6755.099868774415, "avg_ru": 270.2039947509766 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "", "digest": "993fb73ba8b2e351a7d757e4bf993da779cf584af0fd6857cdbeaeb14cb4268c", "exec_count": 33292, "sum_latency": 216292010854, "max_latency": 190233380, "min_latency": 3125034, "avg_latency": 6496816, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 476595.389825428, "avg_ru": 14.31561305495098 }, { "summary_begin_time": 1731382199, "summary_end_time": 1731383772, "digest_text": "with `stars` as ( select `ge` . `repo_id` as `repo_id` , count ( ? ) as `total` , count ( distinct `actor_id` ) as `actors` , sum ( timestampdiff ( second , date_sub ( now ( ) , interval ? hour ) , `ge` . `created_at` ) / timestampdiff ( second , date_sub ( now ( ) , interval ? hour ) , now ( ) ) * ? + ? ) as `score` from `github_events` `ge` where type = ? and `ge` . `created_at` \u003e= date_sub ( now ( ) , interval ? hour ) group by `ge` . `repo_id` having `actors` \u003e ? * `total` ) , `forks` as ( select `ge` . `repo_id` as `repo_id` , count ( ? ) as `total` , count ( distinct `actor_id` ) as `actors` , sum ( timestampdiff ( second , date_sub ( now ( ) , interval ? hour ) , `ge` . `created_at` ) / timestampdiff ( second , date_sub ( now ( ) , interval ? hour ) , now ( ) ) * ? + ? ) as `score` from `github_events` `ge` where type = ? and `ge` . `created_at` \u003e= date_sub ( now ( ) , interval ? hour ) group by `ge` . `repo_id` having `actors` \u003e ? * `total` ) , `toprepos` as ( select `r` . `...", "digest": "37603db7d950f437baab77c402ca278dfb81fb201725dda0253d5cf54243f208", "exec_count": 4, "sum_latency": 200039457150, "max_latency": 75740429058, "min_latency": 22151604389, "avg_latency": 50009864287, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1466585.291414369, "avg_ru": 366646.3228535922 }, { "summary_begin_time": 1731382199, "summary_end_time": 1731383772, "digest_text": "select count ( distinct `actor_id` ) as `developers` , count ( distinct `repo_id` ) as `repos` , sum ( if ( action = ? and `pr_merged` = true , `additions` , ? ) ) as `additions` , sum ( if ( action = ? and `pr_merged` = true , `deletions` , ? ) ) as `deletions` , count ( distinct if ( action = ? , `pr_or_issue_id` , ? ) ) as `opened_prs` , count ( distinct if ( action = ? and `pr_merged` = false , `pr_or_issue_id` , ? ) ) as `closed_prs` , count ( distinct if ( action = ? and `pr_merged` = true , `pr_or_issue_id` , ? ) ) as `merged_prs` from `github_events` `ge` where type = ? and `created_at` \u003e date_sub ( now ( ) , interval ? hour ) ;", "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "exec_count": 13, "sum_latency": 179523117963, "max_latency": 50954683596, "min_latency": 4227281524, "avg_latency": 13809470612, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 9830.1027730306, "avg_ru": 756.1617517715846 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select max ( `stackoverflow` . `users` . `last_modified_date` ) from `stackoverflow` . `users`", "digest": "36ceb598d0e5d79b1d82c4d5e3033d3180ae92a2c65fb6560a7fb0f7887c7a4f", "exec_count": 35, "sum_latency": 137883610182, "max_latency": 59020613749, "min_latency": 256525386, "avg_latency": 3939531719, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 267490.8553009033, "avg_ru": 7642.595865740095 }, { "summary_begin_time": 1731382440, "summary_end_time": 1731383591, "digest_text": "with `stars` as ( select `ge` . `repo_id` as `repo_id` , count ( ? ) as `total` , count ( distinct `actor_id` ) as `actors` , sum ( timestampdiff ( second , date_sub ( now ( ) , interval ? month ) , `ge` . `created_at` ) / timestampdiff ( second , date_sub ( now ( ) , interval ? month ) , now ( ) ) * ? + ? ) as `score` from `github_events` `ge` where type = ? and `ge` . `created_at` \u003e= date_sub ( now ( ) , interval ? month ) group by `ge` . `repo_id` having `actors` \u003e ? * `total` ) , `forks` as ( select `ge` . `repo_id` as `repo_id` , count ( ? ) as `total` , count ( distinct `actor_id` ) as `actors` , sum ( timestampdiff ( second , date_sub ( now ( ) , interval ? month ) , `ge` . `created_at` ) / timestampdiff ( second , date_sub ( now ( ) , interval ? month ) , now ( ) ) * ? + ? ) as `score` from `github_events` `ge` where type = ? and `ge` . `created_at` \u003e= date_sub ( now ( ) , interval ? month ) group by `ge` . `repo_id` having `actors` \u003e ? * `total` ) , `toprepos` as ( select `...", "digest": "639d0ae6f3e83e86f46c41d84f99618d2aeebfdb552f5fe42d66b6695e9eba4a", "exec_count": 6, "sum_latency": 136248108048, "max_latency": 45894677166, "min_latency": 8308606188, "avg_latency": 22708018008, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 2113696.351765869, "avg_ru": 352282.72529431153 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select max ( `stackoverflow` . `comments` . `creation_date` ) from `stackoverflow` . `comments`", "digest": "c980164ab6815c14e9af370c01d70c07627c89fef262f8b088bb4c83f15051ac", "exec_count": 46, "sum_latency": 107660476771, "max_latency": 59558360148, "min_latency": 109787132, "avg_latency": 2340445147, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 543444.5157470703, "avg_ru": 11814.011211892834 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `stats_query_summary` ( `query_name` , `digest_text` , `executed_at` ) values ( ... ) , ( ... ) ;", "digest": "1588d48073c5ecbfa5ca85ed3108156075b3d3868c6541f97e82a27f68b5b46c", "exec_count": 16285, "sum_latency": 96927632912, "max_latency": 101021767, "min_latency": 2938746, "avg_latency": 5951957, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 308226.4697265625, "avg_ru": 18.927016869914798 }, { "summary_begin_time": 1731382805, "summary_end_time": 1731382865, "digest_text": "insert into `mv_repo_participants` ( `repo_id` , `user_login` , `first_engagement_at` , `last_engagement_at` ) select `ge` . `repo_id` , `ge` . `actor_login` as `user_login` , min ( `ge` . `created_at` ) as `new_first_engagement_at` , max ( `ge` . `created_at` ) as `new_last_engagement_at` from `github_events` `ge` where `ge` . `type` != ? and `ge` . `org_id` != ? and `ge` . `created_at` \u003e= ? and `ge` . `created_at` \u003c ? group by `ge` . `repo_id` , `ge` . `actor_login` on duplicate key update `first_engagement_at` = `least` ( `first_engagement_at` , `new_first_engagement_at` ) , `last_engagement_at` = `greatest` ( `last_engagement_at` , `new_last_engagement_at` ) ;", "digest": "0221e529d7e075c5691837ef86c7e03b774f31557e9913d06a825c0a459f3589", "exec_count": 1, "sum_latency": 65362002994, "max_latency": 65362002994, "min_latency": 65362002994, "avg_latency": 65362002994, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 9954.152366129345, "avg_ru": 9954.152366129345 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select `gr` . `repo_id` , `gr` . `repo_name` from `github_repos` `gr` where `gr` . `owner_id` = ? limit ?", "digest": "c102b8c3be5e5ae3f7591257488aa52e329c3eee03be817d49ac974705d7ef4c", "exec_count": 31719, "sum_latency": 65091639251, "max_latency": 313573815, "min_latency": 1072644, "avg_latency": 2052134, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 17046.923215739298, "avg_ru": 0.5374357078009804 }, { "summary_begin_time": 1731382380, "summary_end_time": 1731383349, "digest_text": "with `stars` as ( select `ge` . `repo_id` as `repo_id` , count ( ? ) as `total` , count ( distinct `actor_id` ) as `actors` , sum ( timestampdiff ( second , date_sub ( now ( ) , interval ? week ) , `ge` . `created_at` ) / timestampdiff ( second , date_sub ( now ( ) , interval ? week ) , now ( ) ) * ? + ? ) as `score` from `github_events` `ge` where type = ? and `ge` . `created_at` \u003e= date_sub ( now ( ) , interval ? week ) group by `ge` . `repo_id` having `actors` \u003e ? * `total` ) , `forks` as ( select `ge` . `repo_id` as `repo_id` , count ( ? ) as `total` , count ( distinct `actor_id` ) as `actors` , sum ( timestampdiff ( second , date_sub ( now ( ) , interval ? week ) , `ge` . `created_at` ) / timestampdiff ( second , date_sub ( now ( ) , interval ? week ) , now ( ) ) * ? + ? ) as `score` from `github_events` `ge` where type = ? and `ge` . `created_at` \u003e= date_sub ( now ( ) , interval ? week ) group by `ge` . `repo_id` having `actors` \u003e ? * `total` ) , `toprepos` as ( select `r` . `...", "digest": "67f2d75158c276205e197776572a8e42914927355e2cb8bb97a64a717de7b034", "exec_count": 3, "sum_latency": 58750446299, "max_latency": 43291828666, "min_latency": 4998831948, "avg_latency": 19583482099, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1063171.3158467377, "avg_ru": 354390.4386155792 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select * , date_add ( `updated_at` , interval `expires` second ) as `expired_at` from cache where `cache_key` = ? and ( ( `expires` = ? ) or ( date_add ( `updated_at` , interval `expires` second ) \u003e= now ( ) ) ) limit ? ;", "digest": "67d06204b8ae62e14dbe085885e7b8abf4bed105f9581577acdb2aade862a61a", "exec_count": 34794, "sum_latency": 56248978757, "max_latency": 171024758, "min_latency": 1033990, "avg_latency": 1616628, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 16740.036560059027, "avg_ru": 0.4811184847979257 }, { "summary_begin_time": 1731382562, "summary_end_time": 1731383591, "digest_text": "with `stars_group_by_repo` as ( select `repo_id` , count ( distinct `actor_login` ) as `prs` from `github_events` where type = ? and `repo_id` in ( select `repo_id` from `collection_items` `ci` where `collection_id` = ? ) group by `repo_id` ) , `stars_group_by_month` as ( select `date_format` ( `created_at` , ? ) as `t_month` , `repo_id` , count ( distinct `actor_login` ) as `stars` from `github_events` where type = ? and action = ? and `repo_id` in ( select `repo_id` from `collection_items` `ci` where `collection_id` = ? ) and `created_at` \u003c `date_format` ( now ( ) , ? ) and `created_at` \u003e= `date_format` ( date_sub ( now ( ) , interval ? month ) , ? ) group by `t_month` , `repo_id` ) , `stars_last_month` as ( select `t_month` , `repo_id` , `stars` , `row_number` ( ) `over` ( order by `stars` desc ) as `rank` from `stars_group_by_month` `sgn` where `t_month` = `date_format` ( date_sub ( now ( ) , interval ? month ) , ? ) ) , `stars_last_2nd_month` as ( select `t_month` , `repo_id` ,...", "digest": "080d1b5f80c312c309d7d3d8993625487427b93347df4af3b29f3e141423b3b4", "exec_count": 89, "sum_latency": 37018190973, "max_latency": 2570278267, "min_latency": 15171765, "avg_latency": 415934730, "schema_name": "gharchive_dev", "plan_count": 3, "sum_ru": 122637.8251403808, "avg_ru": 1377.9530914649526 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "c9000df9044e406a4686db84d5a7290ec0b9fec880f04f6f42cdc2586f245284", "exec_count": 1655, "sum_latency": 32749142277, "max_latency": 1037914617, "min_latency": 10743096, "avg_latency": 19788001, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1533270.2085937527, "avg_ru": 926.4472559478868 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "16c8ace7d0b32a585a11551956d1fa6585524fa9eeaf6dd7c144fc7198aedf75", "exec_count": 2740, "sum_latency": 31568646171, "max_latency": 447417973, "min_latency": 1403457, "avg_latency": 11521403, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 76161.37849121087, "avg_ru": 27.79612353693827 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `repos` as ( select `gr` . `repo_id` from `github_repos` `gr` where `gr` . `owner_id` = ? and `gr` . `repo_id` in ( ? ) ) select `mrde` . `user_login` as `login` , sum ( `mrde` . `engagements` ) as `engagements` from `mv_repo_daily_engagements` `mrde` where `repo_id` in ( select `repo_id` from `repos` ) and `mrde` . `day` \u003e ( now ( ) - interval ? day ) and `lower` ( `mrde` . `user_login` ) not like ? and `mrde` . `user_login` not in ( select `login` from `blacklist_users` limit ? ) and `user_login` != ( select `owner_login` from `github_repos` `gr` where `gr` . `owner_id` = ? limit ? ) and not exists ( select ? from `mv_repo_participants` `mrp` where `mrp` . `repo_id` in ( select `repo_id` from `repos` ) and `mrp` . `user_login` = `mrde` . `user_login` and `mrp` . `first_engagement_at` \u003c ( current_date ( ) - interval ? day ) limit ? ) group by `mrde` . `user_login` order by 2 desc limit ?", "digest": "abaf689215b927b40fc94c8e0bf26e0b48872d55ba3012a07821aa1d3b6ef11a", "exec_count": 318, "sum_latency": 15763251468, "max_latency": 199362543, "min_latency": 6552286, "avg_latency": 49569973, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 15279.601461791992, "avg_ru": 48.049061200603745 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731382623, "digest_text": "with `stars_group_by_repo` as ( select `repo_id` , count ( distinct `actor_login` ) as `prs` from `github_events` where type = ? and action = ? and `repo_id` in ( select `repo_id` from `collection_items` `ci` where `collection_id` = ? ) group by `repo_id` ) , `stars_group_by_period` as ( select ( `datediff` ( current_date ( ) , date ( `created_at` ) ) ) div ? as `period` , `repo_id` , count ( distinct `actor_login` ) as `stars` from `github_events` where type = ? and action = ? and `repo_id` in ( select `repo_id` from `collection_items` `ci` where `collection_id` = ? ) and `created_at` \u003e date_sub ( current_date ( ) , interval ? day ) group by `period` , `repo_id` ) , `stars_last_period` as ( select `repo_id` , `stars` , `row_number` ( ) `over` ( order by `stars` desc ) as `rank` from `stars_group_by_period` `sgp` where `period` = ? ) , `stars_last_2nd_period` as ( select `repo_id` , `stars` , `row_number` ( ) `over` ( order by `stars` desc ) as `rank` from `stars_group_by_period` `s...", "digest": "d8c59e842412f9c79586352ab7a3e59ae1d684595c0ebe5d64d5a81ac1f5ff32", "exec_count": 42, "sum_latency": 15532264833, "max_latency": 4060983047, "min_latency": 25524066, "avg_latency": 369815829, "schema_name": "gharchive_dev", "plan_count": 3, "sum_ru": 40758.88808288573, "avg_ru": 970.449716259184 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select count ( ? ) as `cnt` , max ( `created_at` ) as `latest_created_at` , `unix_timestamp` ( max ( `created_at` ) ) as `latest_timestamp` from `github_events` where `created_at` between `from_unixtime` ( ? ) and ( utc_timestamp - interval ? minute ) and `from_unixtime` ( ? ) \u003e ( utc_timestamp - interval ? hour ) ;", "digest": "85310c3dcab6bcdd1a6c04a6f254ab47b9ce73165d54fb9d1a75931d6fb7f15e", "exec_count": 1074, "sum_latency": 12864785594, "max_latency": 240024191, "min_latency": 646545, "avg_latency": 11978385, "schema_name": "gharchive_dev", "plan_count": 2, "sum_ru": 44520.267921956365, "avg_ru": 41.452763428264774 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select `event_logs` . `id` from `event_logs` where `event_logs` . `id` in ( ... )", "digest": "ce7322d73316c5a7ded2d44f7e1be3444a525185d35475f43cefff8cde04c917", "exec_count": 5503, "sum_latency": 11447587755, "max_latency": 76657589, "min_latency": 1367289, "avg_latency": 2080244, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 2994.613453165679, "avg_ru": 0.5441783487489876 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `repos` as ( select `gr` . `repo_id` from `github_repos` `gr` where `gr` . `owner_id` = ? and `gr` . `repo_id` in ( ? ) ) , `current_period_new_participants` as ( select count ( ? ) as `new_participants` from `mv_repo_participants` `mrp` where `mrp` . `repo_id` in ( select `repo_id` from `repos` ) and `mrp` . `first_engagement_at` between ( now ( ) - interval ? day ) and now ( ) and `lower` ( `user_login` ) not like ? and `user_login` not in ( select `login` from `blacklist_users` limit ? ) ) , `past_period_new_participants` as ( select count ( ? ) as `new_participants` from `mv_repo_participants` `mrp` where `mrp` . `repo_id` in ( select `repo_id` from `repos` ) and `mrp` . `first_engagement_at` between ( now ( ) - interval ? day ) and ( now ( ) - interval ? day ) and `lower` ( `user_login` ) not like ? and `user_login` not in ( select `login` from `blacklist_users` limit ? ) ) select `cpnp` . `new_participants` as `current_period_total` , `ppnp` . `new_participants` as `past_...", "digest": "42f60c15d73afdb89f9cf7e9c9928fae502396a00e4d2e74d902d7622ed82480", "exec_count": 407, "sum_latency": 9450502399, "max_latency": 86183839, "min_latency": 5329637, "avg_latency": 23219907, "schema_name": "gharchive_dev", "plan_count": 2, "sum_ru": 28319.535983276364, "avg_ru": 69.58116949207952 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `hackernews` . `users` ( `created` , `id` , `karma` , `last_fetch_at` , `about` ) values ( ... ) on duplicate key update `created` = values ( `created` ) , `karma` = values ( `karma` ) , `last_fetch_at` = values ( `last_fetch_at` ) , `about` = values ( `about` )", "digest": "f9a8dcc813e4cf3e7939137fe52fd6ff641b693a88c079bdd1040b02af654f9f", "exec_count": 905, "sum_latency": 6067675674, "max_latency": 47886764, "min_latency": 1260951, "avg_latency": 6704614, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 17000.82401021323, "avg_ru": 18.785440895263235 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `hackernews` . `users` ( `about` , `created` , `id` , `karma` , `last_fetch_at` ) values ( ... ) on duplicate key update `about` = values ( `about` ) , `created` = values ( `created` ) , `karma` = values ( `karma` ) , `last_fetch_at` = values ( `last_fetch_at` )", "digest": "d1f5a9105c5b3c95571e77cb8c7f00daf8bf577090a7923cb53bd5b3339a7260", "exec_count": 806, "sum_latency": 5370701263, "max_latency": 127385230, "min_latency": 1060033, "avg_latency": 6663401, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 16681.610009765634, "avg_ru": 20.69678661261245 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `repos` as ( select `gr` . `repo_id` from `github_repos` `gr` where `gr` . `owner_id` = ? and `gr` . `repo_id` in ( ? ) ) , `current_period_active_participants` as ( select count ( distinct `user_login` ) as `active_participants` from `mv_repo_participants` where `repo_id` in ( select `repo_id` from `repos` ) and `last_engagement_at` between ( now ( ) - interval ? day ) and now ( ) and `lower` ( `user_login` ) not like ? and `user_login` not in ( select `login` from `blacklist_users` limit ? ) ) , `past_period_active_participants` as ( select count ( distinct `user_login` ) as `active_participants` from `mv_repo_participants` where `repo_id` in ( select `repo_id` from `repos` ) and `last_engagement_at` between ( now ( ) - interval ? day ) and ( now ( ) - interval ? day ) and `lower` ( `user_login` ) not like ? and `user_login` not in ( select `login` from `blacklist_users` limit ? ) ) select `cpnp` . `active_participants` as `current_period_total` , `ppnp` . `active_participant...", "digest": "b14988c8d83c97fc1e17b5ff334dfd932c00253ad902b79474f249c0ce1a489f", "exec_count": 372, "sum_latency": 4979299666, "max_latency": 67781555, "min_latency": 5331072, "avg_latency": 13385214, "schema_name": "gharchive_dev", "plan_count": 2, "sum_ru": 9547.273452758789, "avg_ru": 25.664713582684918 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `group_by_org` as ( select case when ( trim ( `gu` . `organization` ) = ? or `gu` . `organization` is ? ) then ? else `lower` ( replace ( `gu` . `organization` , ... ) ) end as `org_name` , count ( distinct `ge` . `actor_login` ) as `stargazers` from `github_events` `ge` left join `github_users` `gu` on `ge` . `actor_login` = `gu` . `login` where `ge` . `repo_id` = ( select `repo_id` from `github_repos` where `repo_name` = `concat` ( ... ) limit ? ) and `ge` . `type` = ? and `ge` . `action` = ? and `ge` . `created_at` \u003e= ? and `ge` . `created_at` \u003c= ? and if ( ? = true , `gu` . `organization` != ? , true ) group by `org_name` ) , `summary` as ( select sum ( `stargazers` ) as `total` from `group_by_org` ) select `org_name` , `stargazers` , `stargazers` / `total` as `percentage` from `group_by_org` , `summary` order by `stargazers` desc ;", "digest": "bcd7135d761d7237779b21d885710f72f0757e287d7d82d37597f8a924f5cbeb", "exec_count": 39, "sum_latency": 4509966449, "max_latency": 1881207176, "min_latency": 2863136, "avg_latency": 115640165, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 26318.81174926764, "avg_ru": 674.8413269042984 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `hackernews` . `items` ( `by` , `id` , `parent` , `text` , `time` , `type` , `last_fetch_at` ) values ( ... ) on duplicate key update `by` = values ( `by` ) , `parent` = values ( `parent` ) , `text` = values ( `text` ) , `time` = values ( `time` ) , `type` = values ( `type` ) , `last_fetch_at` = values ( `last_fetch_at` )", "digest": "c75db291418be47315642664cf6986785283a0ea166b17802e9915b0431bced1", "exec_count": 569, "sum_latency": 4430911656, "max_latency": 196957079, "min_latency": 3481355, "avg_latency": 7787190, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 14463.449578857424, "avg_ru": 25.419067801155403 }, { "summary_begin_time": 1731382501, "summary_end_time": 1731383772, "digest_text": "delete from `event_logs` where ( `created_at` \u003c= ? ) limit ?", "digest": "28994526549b36b499a9c31f545c6b5d99df9823651bcc69b8080e9d77ef1c8f", "exec_count": 4, "sum_latency": 4178517412, "max_latency": 1187361710, "min_latency": 856306315, "avg_latency": 1044629353, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 27637.983173624692, "avg_ru": 6909.495793406173 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `repos` as ( select `gr` . `repo_id` , `gr` . `repo_name` from `github_repos` `gr` where `gr` . `owner_id` = ? ) , `stars_per_country` as ( select if ( `gu` . `country_code` in ( ... ) , ? , `gu` . `country_code` ) as `country_code` , count ( ? ) as `stars` from `github_events` `ge` join `github_users` `gu` on `ge` . `actor_login` = `gu` . `login` where `ge` . `repo_id` in ( select `repo_id` from `repos` ) and `ge` . `type` = ? and `ge` . `action` = ? and `ge` . `actor_login` not like ? and `gu` . `country_code` not in ( ... ) and `ge` . `created_at` \u003e ( now ( ) - interval ? month ) group by `gu` . `country_code` ) , `stars_total` as ( select sum ( `stars` ) as `stars_total` from `stars_per_country` ) select `spc` . `country_code` , `spc` . `stars` , `round` ( `spc` . `stars` / `st` . `stars_total` , ? ) as `percentage` from `stars_per_country` `spc` , `stars_total` `st` order by `spc` . `stars` desc limit ?", "digest": "189fb2c80ca6b20954afb7c5078914e9a14fce477e78ca7e037bcb84b56ba5fa", "exec_count": 22, "sum_latency": 4164196437, "max_latency": 241357895, "min_latency": 130952710, "avg_latency": 189281656, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 11402.894204711929, "avg_ru": 518.3133729414513 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `stats_api_requests` ( `client_ip` , `client_origin` , `method` , `path` , query , error , `status_code` , duration , `is_dev` ) values ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ... , false ) , ( ... , false , ....", "digest": "64494b4e9ec3eaf5cce488d01d74bc8a5cf9ea4374fe4ff3e99809630c5fa002", "exec_count": 254, "sum_latency": 3540458226, "max_latency": 33768477, "min_latency": 10935636, "avg_latency": 13938811, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 49261.2087890625, "avg_ru": 193.94176688607283 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `hackernews` . `items` ( `by` , `id` , `kids` , `parent` , `text` , `time` , `type` , `last_fetch_at` ) values ( ... ) on duplicate key update `by` = values ( `by` ) , `kids` = values ( `kids` ) , `parent` = values ( `parent` ) , `text` = values ( `text` ) , `time` = values ( `time` ) , `type` = values ( `type` ) , `last_fetch_at` = values ( `last_fetch_at` )", "digest": "d73c9c9f6f5c69bc0937eaa47c352e516639a4d7188643c2162f5efe04c919f5", "exec_count": 516, "sum_latency": 3318069183, "max_latency": 19601128, "min_latency": 3473463, "avg_latency": 6430366, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 8462.864382934567, "avg_ru": 16.400899966927454 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `hackernews` . `items` ( `by` , `descendants` , `id` , `kids` , `score` , `time` , `title` , `type` , `url` , `last_fetch_at` ) values ( ... ) on duplicate key update `by` = values ( `by` ) , `descendants` = values ( `descendants` ) , `kids` = values ( `kids` ) , `score` = values ( `score` ) , `time` = values ( `time` ) , `title` = values ( `title` ) , `type` = values ( `type` ) , `url` = values ( `url` ) , `last_fetch_at` = values ( `last_fetch_at` )", "digest": "341281b2ef38825d8c7bb1c6aa69a1a4b4c95758f12ab9e10158156821bf9869", "exec_count": 464, "sum_latency": 3188875808, "max_latency": 97469284, "min_latency": 3768896, "avg_latency": 6872577, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 10564.28226928711, "avg_ru": 22.767849718291185 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select * , date_add ( `updated_at` , interval `expires` second ) as `expired_at` from `cached_table_cache` where `cache_key` = ? and ( ( `expires` = ? ) or ( date_add ( `updated_at` , interval `expires` second ) \u003e= now ( ) ) ) limit ? ;", "digest": "b1571d293fa687def09da8c650cd11e3acdec6d29e683c84b74f129cfa3f1bcd", "exec_count": 1567, "sum_latency": 3012144854, "max_latency": 37930523, "min_latency": 1136996, "avg_latency": 1922236, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1016.0132303873709, "avg_ru": 0.6483811297941103 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383591, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) on duplicate key update `id` = `id`", "digest": "474fb646cd29b8a5c8e94e664245c157df9306f11ad004ce0027d079fdebc2c7", "exec_count": 306, "sum_latency": 3008445668, "max_latency": 116401232, "min_latency": 986926, "avg_latency": 9831521, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 4911.326397705077, "avg_ru": 16.050086267010055 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select `hackernews` . `items` . * from `hackernews` . `items` where `hackernews` . `items` . `id` = ? order by `hackernews` . `items` . `id` asc limit ?", "digest": "f91beaf16e268d13d7168f75d001dbea71cda387ee17b8a261cf743bf2a053be", "exec_count": 3293, "sum_latency": 2799228713, "max_latency": 116118557, "min_latency": 572232, "avg_latency": 850054, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1594.247341918947, "avg_ru": 0.4841322022225773 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `repos` as ( select `gr` . `repo_id` from `github_repos` `gr` where `gr` . `owner_id` = ? ) , `stars_per_period` as ( select timestampdiff ( month , `created_at` , now ( ) ) div ? as `period` , count ( ? ) as `stars_total` from `github_events` `ge` where `ge` . `repo_id` in ( select `repo_id` from `repos` ) and type = ? and action = ? and `ge` . `actor_login` not like ? and `created_at` \u003e ( now ( ) - interval ? month ) group by `period` ) , `current_period_stars` as ( select `stars_total` from `stars_per_period` where `period` = ? ) , `past_period_stars` as ( select `stars_total` from `stars_per_period` where `period` = ? ) select `cps` . `stars_total` as `current_period_total` , `pps` . `stars_total` as `past_period_total` , ( `cps` . `stars_total` - `pps` . `stars_total` ) / `pps` . `stars_total` as `growth_percentage` from `current_period_stars` `cps` , `past_period_stars` `pps` ;", "digest": "3315fb3158e57316f8d3f57c85bb6a28ec0728f078d9669902a0993a7d922794", "exec_count": 48, "sum_latency": 2747766820, "max_latency": 79271005, "min_latency": 9272489, "avg_latency": 57245142, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 5546.544455973307, "avg_ru": 115.55300949944389 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383591, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) on duplicate key update `repo_id` = `repo_id`", "digest": "0ba0fe30c41664d40c19a7b47f92ded27c1095e3ea1813ed8be805ab761cde47", "exec_count": 302, "sum_latency": 2418791012, "max_latency": 17961194, "min_latency": 3718574, "avg_latency": 8009241, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 19048.573632812517, "avg_ru": 63.07474712851827 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `cached_table_cache` ( `cache_key` , `cache_value` , `expires` ) values ( ... ) on duplicate key update `cache_value` = values ( `cache_value` ) , `expires` = values ( `expires` ) ;", "digest": "a4e2f6f25e3ed687eabbc0ca100b2ada45d6ac827f15c29c8d92bba58e5a9418", "exec_count": 296, "sum_latency": 2413357290, "max_latency": 62499826, "min_latency": 3821836, "avg_latency": 8153234, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 12473.881120808917, "avg_ru": 42.1414902730031 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select count ( ? ) as `cnt` , max ( `created_at` ) as `latest_created_at` , `unix_timestamp` ( max ( `created_at` ) ) as `latest_timestamp` from `github_events` where `created_at` between ( utc_timestamp - interval ? minute - interval `unix_timestamp` ( utc_timestamp - interval ? minute ) % ? second ) and ( utc_timestamp - interval ? minute - interval `unix_timestamp` ( utc_timestamp - interval ? minute ) % ? second ) group by ( ( `unix_timestamp` ( `created_at` ) - `unix_timestamp` ( utc_timestamp - interval ? minute - interval `unix_timestamp` ( utc_timestamp - interval ? minute ) % ? second ) ) - ( `unix_timestamp` ( `created_at` ) - `unix_timestamp` ( utc_timestamp - interval ? minute - interval `unix_timestamp` ( utc_timestamp - interval ? minute ) % ? second ) ) % ? ) order by `latest_timestamp` ;", "digest": "289f8404a009ab0bcd011b3cc9ebed8f7df4c5dd3b47fe25e53b111ff7c0b6f9", "exec_count": 217, "sum_latency": 2268638819, "max_latency": 156854018, "min_latency": 5560650, "avg_latency": 10454556, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 3546.487251790363, "avg_ru": 16.343259224840384 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "73fcf695719959387dddd6336fa4afd913e2c65c0065d224958d8d2a7a03489f", "exec_count": 67, "sum_latency": 2011301955, "max_latency": 435182144, "min_latency": 1333804, "avg_latency": 30019432, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1591.6728759765629, "avg_ru": 23.756311581739745 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "b3980ccab61a988b0dde58ce44e04270c02aedc948bba60dddcb79138178caff", "exec_count": 53, "sum_latency": 2002076920, "max_latency": 433845589, "min_latency": 1422042, "avg_latency": 37775036, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1484.0759216308595, "avg_ru": 28.00143248360112 }, { "summary_begin_time": 1731382199, "summary_end_time": 1731383772, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "3d79ee2a3fc89a14e75219ec1744f5d048d4beb59d107959c8f321e9db37a212", "exec_count": 59, "sum_latency": 1862758036, "max_latency": 425485660, "min_latency": 1342420, "avg_latency": 31572170, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1392.667840576172, "avg_ru": 23.604539670782575 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `repos` as ( select `gr` . `repo_id` from `github_repos` `gr` where `gr` . `owner_id` = ? and `gr` . `repo_id` in ( ? ) ) select `user_login` as `login` , sum ( `engagements` ) as `engagements` from `mv_repo_daily_engagements` where `repo_id` in ( select `repo_id` from `repos` ) and day \u003e ( now ( ) - interval ? day ) and `lower` ( `user_login` ) not like ? and `user_login` not in ( select `login` from `blacklist_users` limit ? ) group by `user_login` order by 2 desc limit ?", "digest": "4c2414106811c4c9df4766cab0328fd0f3da61540e9e2c5a007189ce97d00bf3", "exec_count": 232, "sum_latency": 1847116557, "max_latency": 94316921, "min_latency": 3842052, "avg_latency": 7961709, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 605.0182647705078, "avg_ru": 2.6078373481487405 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with recursive `seq` ( `idx` , `current_period_day` , `past_period_day` ) as ( select ? as `idx` , `date_format` ( current_date ( ) , ? ) as `current_period_day` , `date_format` ( date_sub ( current_date ( ) , interval ? day ) , ? ) as `past_period_day` union all select `idx` + ? as `idx` , `date_format` ( date_sub ( current_date ( ) , interval `idx` day ) , ? ) as `current_period_day` , `date_format` ( date_sub ( current_date ( ) , interval `idx` + ? day ) , ? ) as `past_period_day` from `seq` where ? = ? and `idx` \u003c ? ) , `repos` as ( select `gr` . `repo_id` from `github_repos` `gr` where `gr` . `owner_id` = ? and `gr` . `repo_id` in ( ? ) ) , `group_by_day` as ( select timestampdiff ( day , day , current_date ( ) ) % ? + ? as `idx` , timestampdiff ( day , day , current_date ( ) ) div ? as `period` , day , `participants` from ( select `date_format` ( day , ? ) as day , count ( distinct `user_login` ) as `participants` from `mv_repo_daily_engagements` `mrde` where `repo_id` in ( se...", "digest": "e0bd74cc8ea679b5d90247541649a83735affae6d3ef2cc0a314c4bb8ed253af", "exec_count": 87, "sum_latency": 1698665560, "max_latency": 60084232, "min_latency": 9893057, "avg_latency": 19524891, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 415.3587677001953, "avg_ru": 4.774238709197647 }, { "summary_begin_time": 1731383228, "summary_end_time": 1731383531, "digest_text": "insert into `user_selected_stocks` ( `symbol` , time , high , low , open , close , `volume` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ...", "digest": "0ae5f259e23bc838c2142dbf86fccd03f22100a5dd9904b9e7b905057d071193", "exec_count": 2, "sum_latency": 1617878877, "max_latency": 939491256, "min_latency": 678387621, "avg_latency": 808939438, "schema_name": "sp500insight", "plan_count": 1, "sum_ru": 153.37692260742188, "avg_ru": 76.68846130371094 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with recursive `seq` ( `idx` , `current_period_day` , `past_period_day` ) as ( select ? as `idx` , `date_format` ( current_date ( ) , ? ) as `current_period_day` , `date_format` ( date_sub ( current_date ( ) , interval ? day ) , ? ) as `past_period_day` union all select `idx` + ? as `idx` , `date_format` ( date_sub ( current_date ( ) , interval `idx` day ) , ? ) as `current_period_day` , `date_format` ( date_sub ( current_date ( ) , interval `idx` + ? day ) , ? ) as `past_period_day` from `seq` where ? = ? and `idx` \u003c ? ) , `repos` as ( select `gr` . `repo_id` from `github_repos` `gr` where `gr` . `owner_id` = ? and `gr` . `repo_id` in ( ? ) ) , `group_by_day` as ( select timestampdiff ( day , day , current_date ( ) ) % ? + ? as `idx` , timestampdiff ( day , day , current_date ( ) ) div ? as `period` , day , `participants` from ( select `date_format` ( day , ? ) as day , count ( distinct `user_login` ) as `participants` from `mv_repo_daily_engagements` where `repo_id` in ( select `r...", "digest": "bcdb049c41409f5eee6e0bab80ffa5983f96e2b72d60430554f7c2002a426f29", "exec_count": 124, "sum_latency": 1590989778, "max_latency": 33730386, "min_latency": 5431784, "avg_latency": 12830562, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 513.531391398112, "avg_ru": 4.141382188694451 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with recursive `seq` ( `idx` , `current_period_day` , `past_period_day` ) as ( select ? as `idx` , `date_format` ( current_date ( ) , ? ) as `current_period_day` , `date_format` ( date_sub ( current_date ( ) , interval ? month ) , ? ) as `past_period_day` union all select `idx` + ? as `idx` , `date_format` ( date_sub ( current_date ( ) , interval `idx` month ) , ? ) as `current_period_day` , `date_format` ( date_sub ( current_date ( ) , interval `idx` + ? month ) , ? ) as `past_period_day` from `seq` where ? = ? and `idx` \u003c ? ) , `repos` as ( select `gr` . `repo_id` from `github_repos` `gr` where `gr` . `owner_id` = ? ) , `group_by_day` as ( select timestampdiff ( month , day , current_date ( ) ) % ? + ? as `idx` , timestampdiff ( month , day , current_date ( ) ) div ? as `period` , day , `stars` from ( select `date_format` ( `created_at` , ? ) as day , count ( ? ) as `stars` from `github_events` `ge` where `repo_id` in ( select `repo_id` from `repos` ) and type = ? and action = ? a...", "digest": "53dd5e9c4f6376b9aadbd5e4633a942d5ae60cee4c7e85cb64da9471a5250f70", "exec_count": 24, "sum_latency": 1509381144, "max_latency": 93906534, "min_latency": 55317062, "avg_latency": 62890881, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 2870.995052083333, "avg_ru": 119.62479383680555 }, { "summary_begin_time": 1731382320, "summary_end_time": 1731383591, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "6c392545d1efbaeed9bde22bf43c89d57f81654cf937a8b1572adb4a15719ffe", "exec_count": 24, "sum_latency": 1456644316, "max_latency": 1037234554, "min_latency": 12779487, "avg_latency": 60693513, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 23037.91386718754, "avg_ru": 959.9130777994809 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `group_by_countries` as ( select case when trim ( `gu` . `country_code` ) in ( ... ) or `gu` . `country_code` is ? then ? else `gu` . `country_code` end as `country_code` , count ( ? ) as `stargazers` from `github_events` `ge` left join `github_users` `gu` on `ge` . `actor_login` = `gu` . `login` where `repo_id` = ( select `repo_id` from `github_repos` where `repo_name` = `concat` ( ... ) limit ? ) and `ge` . `type` = ? and `ge` . `action` = ? and `ge` . `created_at` \u003e= ? and `ge` . `created_at` \u003c= ? and if ( ? = true , `gu` . `country_code` not in ( ... ) , true ) group by 1 ) , `summary` as ( select sum ( `stargazers` ) as `total` from `group_by_countries` ) select `country_code` , `stargazers` , `stargazers` / `total` as `percentage` from `group_by_countries` , `summary` order by `stargazers` desc ;", "digest": "e033077dd0a595f1c0f9818bb9678a8c2dcc0699b56c70937b6363bb844b660b", "exec_count": 41, "sum_latency": 1313140075, "max_latency": 194851089, "min_latency": 3030051, "avg_latency": 32027806, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 8417.493408203052, "avg_ru": 205.3047172732452 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `repos` as ( select `gr` . `repo_id` , `gr` . `repo_name` from `github_repos` `gr` where `gr` . `owner_id` = ? ) , `repos_with_stars` as ( select `repo_id` , count ( ? ) as `stars` from `github_events` `ge` where `ge` . `repo_id` in ( select `repo_id` from `repos` ) and `ge` . `type` = ? and `ge` . `action` = ? and `ge` . `actor_login` not like ? and `created_at` \u003e ( now ( ) - interval ? month ) group by `repo_id` order by `stars` desc limit ? ) select `gr` . `repo_id` , `gr` . `repo_name` , `rws` . `stars` from `repos_with_stars` `rws` join `github_repos` `gr` using ( `repo_id` ) order by `stars` desc limit ?", "digest": "544f8fc71f07c278592c339a9fa0be5b20076c6d1fe64d23d63826caca8bc477", "exec_count": 24, "sum_latency": 1262086824, "max_latency": 73459416, "min_latency": 8896236, "avg_latency": 52586951, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 2315.768294270832, "avg_ru": 96.490345594618 }, { "summary_begin_time": 1731382199, "summary_end_time": 1731383711, "digest_text": "with `stars` as ( select `repo_id` , `created_at` , `row_number` ( ) `over` ( partition by `repo_id` , `actor_login` ) as `row_num` from `github_events` where type = ? and `repo_id` in ( ? ) ) , `stars_per_month` as ( select `repo_id` , `date_format` ( `created_at` , ? ) as `t_month` , count ( ? ) as `total` from `stars` where `row_num` = ? group by `repo_id` , `t_month` ) , `acc` as ( select `repo_id` , `t_month` , sum ( `total` ) `over` ( partition by `repo_id` order by `t_month` ) as `total` , `row_number` ( ) `over` ( partition by `repo_id` , `t_month` ) as `row_num` from `stars_per_month` ) select `repo_id` , `t_month` as `event_month` , `total` from `acc` where `row_num` = ? order by `t_month` ;", "digest": "261349e506e11aebca564132f9343a765a42f2799fe0a40e5ef34b7134839f61", "exec_count": 10, "sum_latency": 1248891884, "max_latency": 464036186, "min_latency": 3269270, "avg_latency": 124889188, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 972.5673889160157, "avg_ru": 97.25673889160157 }, { "summary_begin_time": 1731382320, "summary_end_time": 1731383651, "digest_text": "with recursive `seq` ( `idx` , `current_period_day` , `last_period_day` ) as ( select ? as `idx` , current_date ( ) as `current_period_day` , date_sub ( current_date ( ) , interval ? day ) as `last_period_day` union all select `idx` + ? as `idx` , date_sub ( current_date ( ) , interval `idx` day ) as `current_period_day` , date_sub ( current_date ( ) , interval `idx` + ? day ) as `last_period_day` from `seq` where `idx` \u003c ? ) , `group_by_day` as ( select `day_offset` % ? + ? as `idx` , `day_offset` div ? as `period` , day , `contributors` from ( select ( `datediff` ( current_date ( ) , day ) ) as `day_offset` , day , `contributors` from ( select `date_format` ( `created_at` , ? ) as day , count ( distinct `actor_id` ) as `contributors` from `github_events` `ge` where `repo_id` = ? and ( ( type = ? and action = ? ) or ( type = ? and action = ? ) or ( type = ? and action = ? ) or ( type = ? and action = ? ) or ( type = ? and action = ? ) or ( type = ? and action = ? ) ) and `created_a...", "digest": "1334130de9e7a3df7b13b40f87d2e653e93a0ce59d1be81cc711459f8bfaf8f3", "exec_count": 5, "sum_latency": 1101457190, "max_latency": 965876538, "min_latency": 13541729, "avg_latency": 220291438, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 3637.7133768717463, "avg_ru": 727.5426753743493 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "ed97b012822bd72ba7415b3dd81de502f5b9de37d3be1a714f2e71e6cd557b67", "exec_count": 46, "sum_latency": 1073595351, "max_latency": 419024368, "min_latency": 1412409, "avg_latency": 23339029, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1150.2638916015628, "avg_ru": 25.005736773947017 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `group_by_org` as ( select case when ( trim ( `gu` . `organization` ) in ( ... ) or `gu` . `organization` is ? ) then ? else `lower` ( replace ( `gu` . `organization` , ... ) ) end as `org_name` , count ( distinct `ge` . `actor_login` ) as `pull_request_creators` from `github_events` `ge` left join `github_users` `gu` on `ge` . `actor_login` = `gu` . `login` where `ge` . `repo_id` = ( select `repo_id` from `github_repos` where `repo_name` = `concat` ( ... ) limit ? ) and `ge` . `type` = ? and `ge` . `action` = ? and `ge` . `created_at` \u003e= ? and `ge` . `created_at` \u003c= ? and if ( ? = true , `gu` . `organization` != ? , true ) group by `org_name` ) , `summary` as ( select sum ( `pull_request_creators` ) as `total` from `group_by_org` ) select `org_name` , `pull_request_creators` , `pull_request_creators` / `total` as `percentage` from `group_by_org` , `summary` order by `pull_request_creators` desc ;", "digest": "5ee52db5067308c2055c2e9a315c9b52b4f252adc74af5c8c6d954d575d763bc", "exec_count": 41, "sum_latency": 1041289299, "max_latency": 455243070, "min_latency": 3187253, "avg_latency": 25397299, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1497.2536804199224, "avg_ru": 36.5183824492664 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select * from ( select `id` , type , action , `actor_id` , `actor_login` , `repo_id` , `repo_name` , `number` , `pr_merged` , `created_at` from `github_events` where `created_at` between ( utc_timestamp - interval ? minute - interval `unix_timestamp` ( utc_timestamp - interval ? minute ) % ? second ) and ( utc_timestamp - interval ? minute - interval `unix_timestamp` ( utc_timestamp - interval ? minute ) % ? second ) and `actor_login` not like ? and `actor_login` not like ? and type in ( ... ) limit ? ) `sub` order by `created_at` desc ;", "digest": "6130f94bf789102647203af567bfed857c0fbd8205ca89f930c33f74d6160df9", "exec_count": 111, "sum_latency": 1025730817, "max_latency": 43800121, "min_latency": 4215869, "avg_latency": 9240818, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1044.7612101236978, "avg_ru": 9.412263154267547 }, { "summary_begin_time": 1731382199, "summary_end_time": 1731383711, "digest_text": "with recursive `seq` ( `idx` , `current_period_day` , `last_period_day` ) as ( select ? as `idx` , current_date ( ) as `current_period_day` , date_sub ( current_date ( ) , interval ? day ) as `last_period_day` union all select `idx` + ? as `idx` , date_sub ( current_date ( ) , interval `idx` day ) as `current_period_day` , date_sub ( current_date ( ) , interval `idx` + ? day ) as `last_period_day` from `seq` where `idx` \u003c ? ) , `group_by_day` as ( select `day_offset` % ? + ? as `idx` , `day_offset` div ? as `period` , day , `stars` from ( select ( `datediff` ( current_date ( ) , day ) ) as `day_offset` , day , `stars` from ( select `date_format` ( `created_at` , ? ) as day , count ( ? ) as `stars` from `github_events` `ge` where type = ? and `repo_id` = ? and `created_at` \u003e date_sub ( current_date ( ) , interval ? day ) group by day order by day ) `sub` ) `sub2` ) , `last_28_days` as ( select `idx` , day , `stars` from `group_by_day` where `period` = ? ) , `last_2nd_28_days` as ( se...", "digest": "c14f9770ffc525fe5a3b083a3baa2d06786fa8ed444dc5bb487aed4ba2621b08", "exec_count": 15, "sum_latency": 1004317221, "max_latency": 552761658, "min_latency": 4901052, "avg_latency": 66954481, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1878.8455454508464, "avg_ru": 125.2563696967231 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `stats_query_summary` ( `query_name` , `digest_text` , `executed_at` ) values ( ... ) ;", "digest": "db2684d569a499b5a91d12abc419988153f2c3abccf80584717c7ab29eafac46", "exec_count": 154, "sum_latency": 963053637, "max_latency": 14405505, "min_latency": 3251030, "avg_latency": 6253595, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 2793.921875, "avg_ru": 18.142349837662337 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select `hackernews` . `users` . * from `hackernews` . `users` where `hackernews` . `users` . `id` = ? order by `hackernews` . `users` . `id` asc limit ?", "digest": "297a56834acc252f7e0fbb8c4d1820bb4f2cf1fb9907ab5ad2f19883cb4edb45", "exec_count": 1136, "sum_latency": 947391617, "max_latency": 21853770, "min_latency": 576826, "avg_latency": 833971, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 542.692946370443, "avg_ru": 0.47772266405848857 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "with `group_by_org` as ( select case when ( trim ( `gu` . `organization` ) = ? or `gu` . `organization` is ? ) then ? else `lower` ( replace ( `gu` . `organization` , ... ) ) end as `org_name` , count ( distinct `ge` . `actor_login` ) as `issue_creators` from `github_events` `ge` left join `github_users` `gu` on `ge` . `actor_login` = `gu` . `login` where `ge` . `repo_id` = ( select `repo_id` from `github_repos` where `repo_name` = `concat` ( ... ) limit ? ) and `ge` . `type` = ? and `ge` . `action` = ? and `ge` . `created_at` \u003e= ? and `ge` . `created_at` \u003c= ? and if ( ? = true , `gu` . `organization` != ? , true ) group by `org_name` ) , `summary` as ( select sum ( `issue_creators` ) as `total` from `group_by_org` ) select `org_name` , `issue_creators` , `issue_creators` / `total` as `percentage` from `group_by_org` , `summary` order by `issue_creators` desc ;", "digest": "a45cef35e7c1a830a64438920470cfdb0ca2a1aeba125619a74664fac0726c8b", "exec_count": 44, "sum_latency": 920473488, "max_latency": 199480754, "min_latency": 3111569, "avg_latency": 20919852, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 2952.3678904215476, "avg_ru": 67.09927023685336 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "74ea47789ccb49e1223fff49e5a235a6682c187c1d730f58bcb8394834031c1d", "exec_count": 53, "sum_latency": 840881453, "max_latency": 451761629, "min_latency": 1370554, "avg_latency": 15865687, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1398.5346191406252, "avg_ru": 26.38744564416274 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "b653d71f35b4c383bc75b1e37758069dc24b9cbe49ebd0b600fc58b67d0b72aa", "exec_count": 59, "sum_latency": 824670270, "max_latency": 431918979, "min_latency": 1447396, "avg_latency": 13977462, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1415.9999969482424, "avg_ru": 23.999999948275295 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "1288a26c5196a52a854ef7e29009bdff403c3b80482adfe428681a9064f3128c", "exec_count": 38, "sum_latency": 817616437, "max_latency": 243835786, "min_latency": 10511410, "avg_latency": 21516222, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 28770.45371093756, "avg_ru": 757.1172029194095 }, { "summary_begin_time": 1731382199, "summary_end_time": 1731383711, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "9e97aff0e4cbc4d845f39683f95f21f8e58c104670c2b6e109cf89f620df6840", "exec_count": 41, "sum_latency": 808675540, "max_latency": 38805032, "min_latency": 13343127, "avg_latency": 19723793, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 39493.11796875007, "avg_ru": 963.2467797256114 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "d55960a69cdb138f874f4f0a7f713b2e79a343f25e4dd9fd845adf677d4a5d18", "exec_count": 51, "sum_latency": 778018853, "max_latency": 437054877, "min_latency": 1468573, "avg_latency": 15255271, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1363.7450164794925, "avg_ru": 26.74009836234299 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "dec90e4c5c2feb4bf9dcb6a9afee6c6924e2d1be319a6a691674edb138c895e6", "exec_count": 52, "sum_latency": 776868288, "max_latency": 439421467, "min_latency": 1271757, "avg_latency": 14939774, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1210.2992309570313, "avg_ru": 23.274985210712142 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "select max ( `stackoverflow` . `questions` . `last_activity_date` ) from `stackoverflow` . `questions`", "digest": "e24f6aabf2a3e11712a8e85240556f4568b4b27100cba9b2344a9371d0fd8914", "exec_count": 75, "sum_latency": 772006748, "max_latency": 16403725, "min_latency": 8685480, "avg_latency": 10293423, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 243.96473185221353, "avg_ru": 3.252863091362847 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "with `group_by_countries` as ( select case when trim ( `gu` . `country_code` ) in ( ... ) or `gu` . `country_code` is ? then ? else `gu` . `country_code` end as `country_code` , count ( distinct `actor_login` ) as `issue_creators` from `github_events` `ge` left join `github_users` `gu` on `ge` . `actor_login` = `gu` . `login` where `repo_id` = ( select `repo_id` from `github_repos` where `repo_name` = `concat` ( ... ) limit ? ) and `ge` . `type` = ? and `ge` . `action` = ? and `ge` . `created_at` \u003e= ? and `ge` . `created_at` \u003c= ? and if ( ? = true , `gu` . `country_code` not in ( ... ) , true ) group by 1 ) , `summary` as ( select sum ( `issue_creators` ) as `total` from `group_by_countries` ) select `country_code` , `issue_creators` , `issue_creators` / `total` as `percentage` from `group_by_countries` , `summary` order by `issue_creators` desc ;", "digest": "1021af16579c112d553fc838b50d38000149abe820866a35a36d5f0237b43cee", "exec_count": 39, "sum_latency": 711147084, "max_latency": 133383170, "min_latency": 5874168, "avg_latency": 18234540, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 2012.7801208496226, "avg_ru": 51.609746688451864 }, { "summary_begin_time": 1731382501, "summary_end_time": 1731383772, "digest_text": "select `table_name` from ( select * from `information_schema` . `tables` where `table_schema` = database ( ) ) `_subquery`", "digest": "11d1f137ebaedbc58ec1b3af5af1db986edca323b2468ee76e5fb8d436dc5ff2", "exec_count": 4, "sum_latency": 680785508, "max_latency": 265491809, "min_latency": 106820430, "avg_latency": 170196377, "schema_name": "gharchive_dev", "plan_count": 1 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "5691fd1a9990db5a3a281408b66425d109559cfb310b1294aec3b4b62f727463", "exec_count": 39, "sum_latency": 676958032, "max_latency": 25051963, "min_latency": 11276691, "avg_latency": 17357898, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 34907.15742187507, "avg_ru": 895.0553185096171 }, { "summary_begin_time": 1731383410, "summary_end_time": 1731383470, "digest_text": "insert into `user_selected_stocks` ( `symbol` , time , high , low , open , close , `volume` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ...", "digest": "28d0bbdd695d979f5f3ca250eb3a5fd2aa4da49756f4560cabd1e784d943be3c", "exec_count": 1, "sum_latency": 675751462, "max_latency": 675751462, "min_latency": 675751462, "avg_latency": 675751462, "schema_name": "sp500insight", "plan_count": 1, "sum_ru": 83.95759989420573, "avg_ru": 83.95759989420573 }, { "summary_begin_time": 1731382199, "summary_end_time": 1731383772, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "03ac2dab2f7857321f73bcaa5a63e4ffce264a44122a13715f6ee920bd74dd75", "exec_count": 55, "sum_latency": 673594410, "max_latency": 329768071, "min_latency": 1371432, "avg_latency": 12247171, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1244.2515136718753, "avg_ru": 22.622754794034098 }, { "summary_begin_time": 1731383711, "summary_end_time": 1731383772, "digest_text": "insert into `user_selected_stocks` ( `symbol` , time , high , low , open , close , `volume` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ...", "digest": "403b2fb3af215559f5de35afd158d6a00fc097d803259d8e137723bfa4e89667", "exec_count": 1, "sum_latency": 665152498, "max_latency": 665152498, "min_latency": 665152498, "avg_latency": 665152498, "schema_name": "sp500insight", "plan_count": 1, "sum_ru": 81.39517110188801, "avg_ru": 81.39517110188801 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "6ee72b12462872c788b9debcc8a3de80f52714b4a14dae52278672ba84b2faf3", "exec_count": 35, "sum_latency": 662265823, "max_latency": 26716443, "min_latency": 12770355, "avg_latency": 18921880, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 34745.083398437564, "avg_ru": 992.7166685267875 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "7e2e65f36669580042af9ffbc0685f1263f569b73d1089ce824efd596fe696d2", "exec_count": 32, "sum_latency": 659880103, "max_latency": 35671531, "min_latency": 13681563, "avg_latency": 20621253, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 31788.161914062548, "avg_ru": 993.3800598144546 }, { "summary_begin_time": 1731383349, "summary_end_time": 1731383410, "digest_text": "insert into `user_selected_stocks` ( `symbol` , time , high , low , open , close , `volume` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ...", "digest": "e5516bddd5acb7e61caf2f3bfeeefac63fe52ef4af00252a2445b667b6ce41ec", "exec_count": 1, "sum_latency": 650773372, "max_latency": 650773372, "min_latency": 650773372, "avg_latency": 650773372, "schema_name": "sp500insight", "plan_count": 1, "sum_ru": 77.02444864908854, "avg_ru": 77.02444864908854 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `repos` as ( select `gr` . `repo_id` from `github_repos` `gr` where `gr` . `owner_id` = ? ) , `opened_issues` as ( select `sub` . `repo_id` , `sub` . `number` , `sub` . `opened_by` , `sub` . `opened_at` , timestampdiff ( month , `opened_at` , now ( ) ) div ? as `period` from ( select `ge` . `repo_id` , `ge` . `number` , `ge` . `actor_login` as `opened_by` , `ge` . `created_at` as `opened_at` , `row_number` ( ) `over` ( partition by `ge` . `repo_id` , `ge` . `number` order by `ge` . `created_at` ) as `times` from `github_events` `ge` where `ge` . `repo_id` in ( select `repo_id` from `repos` ) and `ge` . `type` = ? and `ge` . `action` = ? and `ge` . `actor_login` not like ? and `created_at` \u003e ( now ( ) - interval ? month ) ) `sub` where `sub` . `times` = ? ) , `closed_issues` as ( select `sub` . `repo_id` , `sub` . `number` , `sub` . `closed_by` , `sub` . `closed_at` , if ( `sub` . `closed_by` = `oi` . `opened_by` , ... ) as type , timestampdiff ( month , `closed_at` , now ( ) ) ...", "digest": "5ce933429ea39c10882e6b1c03b544bf14cba788f343764d3ae387d68a07f300", "exec_count": 25, "sum_latency": 649524768, "max_latency": 43541222, "min_latency": 8889050, "avg_latency": 25980990, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1023.7721557617188, "avg_ru": 40.95088623046875 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "with `group_by_countries` as ( select case when trim ( `gu` . `country_code` ) in ( ... ) or `gu` . `country_code` is ? then ? else `gu` . `country_code` end as `country_code` , count ( distinct `actor_login` ) as `pull_request_creators` from `github_events` `ge` left join `github_users` `gu` on `ge` . `actor_login` = `gu` . `login` where `repo_id` = ( select `repo_id` from `github_repos` where `repo_name` = `concat` ( ... ) limit ? ) and `ge` . `type` = ? and `ge` . `action` = ? and `ge` . `created_at` \u003e= ? and `ge` . `created_at` \u003c= ? and if ( ? = true , `gu` . `country_code` not in ( ... ) , true ) group by 1 ) , `summary` as ( select sum ( `pull_request_creators` ) as `total` from `group_by_countries` ) select `country_code` , `pull_request_creators` , `pull_request_creators` / `total` as `percentage` from `group_by_countries` , `summary` order by `pull_request_creators` desc ;", "digest": "73a63750840e80dd32470565af2391380ad047b695bc34464102b617418f47ab", "exec_count": 41, "sum_latency": 638925138, "max_latency": 143988576, "min_latency": 2854315, "avg_latency": 15583539, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1423.0299194336133, "avg_ru": 34.70804681545398 }, { "summary_begin_time": 1731382199, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) on duplicate key update `repo_id` = `repo_id`", "digest": "72dd244c69f070da35d814162e43a9954c30bb024322947c360da503ebb99fae", "exec_count": 46, "sum_latency": 622690816, "max_latency": 25715050, "min_latency": 8841393, "avg_latency": 13536756, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 27895.82441406255, "avg_ru": 606.430965523099 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "e280a493a87b4378d6b1fe54376fa3bac12e4cb826100ef240e3195f68927fe7", "exec_count": 45, "sum_latency": 618974616, "max_latency": 321782376, "min_latency": 1232043, "avg_latency": 13754991, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 940.5014648437502, "avg_ru": 20.90003255208334 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383651, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , true , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , fa...", "digest": "23827b80d5d7ce1b1c5093336788caa1b2e82eaf6cb7a9bd475eec22459ae8a9", "exec_count": 32, "sum_latency": 613243411, "max_latency": 34989642, "min_latency": 13569842, "avg_latency": 19163856, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 31012.989843750056, "avg_ru": 969.1559326171893 }, { "summary_begin_time": 1731382320, "summary_end_time": 1731383711, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "76a88a37a22698d98fd3430d73f4193b05554dd58fb2e20447bcfdbce973d8b9", "exec_count": 44, "sum_latency": 608993211, "max_latency": 322896418, "min_latency": 1334624, "avg_latency": 13840754, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1033.7263854980472, "avg_ru": 23.49378148859198 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) on duplicate key update `repo_id` =...", "digest": "7260abf4bcf52f42b81c694c810ff89baf3460166124c2ecac69667ba7e45f9f", "exec_count": 44, "sum_latency": 602588507, "max_latency": 24112511, "min_latency": 9648709, "avg_latency": 13695193, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 28618.640625000055, "avg_ru": 650.423650568183 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "666ef0230b9e5f335e0796a4ef5a1bd60e49a4888fd2f8f8b7aa478bbeb060d2", "exec_count": 29, "sum_latency": 591206423, "max_latency": 32135656, "min_latency": 13203150, "avg_latency": 20386428, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 29734.044140625047, "avg_ru": 1025.3118669181051 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , true , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , fa...", "digest": "5ee302301fc6a0cbd47ad0e5fd712c8112c6a241d12f7b664503b6863c275337", "exec_count": 29, "sum_latency": 588285815, "max_latency": 37018786, "min_latency": 12393782, "avg_latency": 20285717, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 28254.164648437545, "avg_ru": 974.2815396012946 }, { "summary_begin_time": 1731382199, "summary_end_time": 1731383711, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , t...", "digest": "33ab890189ec0d13d3584b416ef91ca83fd8ff6575280cefdddd704343ea6ab7", "exec_count": 32, "sum_latency": 585414371, "max_latency": 31358650, "min_latency": 12147916, "avg_latency": 18294199, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 31361.679492187555, "avg_ru": 980.0524841308611 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , true , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , fa...", "digest": "3fe678f57478b0d00cd0474ab610e300b7108401a5752e883d497e6fec0455a8", "exec_count": 26, "sum_latency": 571731248, "max_latency": 61101402, "min_latency": 11581232, "avg_latency": 21989663, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 26078.089257812535, "avg_ru": 1003.0034329927898 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "549508b9ffd6f3967fca4903e488e53687788f17a63a53e5f6414bf67939de0f", "exec_count": 33, "sum_latency": 565226423, "max_latency": 30943200, "min_latency": 10632057, "avg_latency": 17128073, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 27965.964648437555, "avg_ru": 847.4534741950774 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , true , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , fa...", "digest": "e0512fdfea3f6ea04669b371c22ff85bff0f553dbb2aa7f4e752b7769e8711da", "exec_count": 34, "sum_latency": 556787924, "max_latency": 24783240, "min_latency": 11182661, "avg_latency": 16376115, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 31707.213281250057, "avg_ru": 932.5650965073546 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "fae06a702ea678dc76c3e678635ce99857e8babd7d84774beff5ae57054d4d1b", "exec_count": 32, "sum_latency": 552981680, "max_latency": 29924133, "min_latency": 12782852, "avg_latency": 17280677, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 31657.813867187553, "avg_ru": 989.306683349611 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "f67fb80d4fb941ffeb34313f10e0b6e3e42ddb925d60f88ebf70908c519e2779", "exec_count": 29, "sum_latency": 552313097, "max_latency": 35951441, "min_latency": 12958538, "avg_latency": 19045279, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 28005.19492187505, "avg_ru": 965.696376616381 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "f7988c0be94bac0f27e693e28923ac9cfd2e3c6e855934b4b33432c44cb8b293", "exec_count": 25, "sum_latency": 546585826, "max_latency": 35870135, "min_latency": 12771118, "avg_latency": 21863433, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 24851.326562500035, "avg_ru": 994.0530625000014 }, { "summary_begin_time": 1731382259, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "97d1153ba080a7e7d34cc5d1318df10c86b7e5e9a955e4b7dded37fc853df35d", "exec_count": 31, "sum_latency": 544385805, "max_latency": 29593928, "min_latency": 12313174, "avg_latency": 17560832, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 27984.556250000056, "avg_ru": 902.7276209677437 }, { "summary_begin_time": 1731382320, "summary_end_time": 1731383711, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "81fbe0383de24431ff55007851fcfa06e149e156ff7afd5ac021ef640428b7e9", "exec_count": 34, "sum_latency": 535964376, "max_latency": 25651613, "min_latency": 9635154, "avg_latency": 15763658, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 25622.10078125005, "avg_ru": 753.5911994485309 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "fdc2797c1cc12a82f27ddd78a1191dd372fff64c90dcce94a9ac1cc2c2d8553f", "exec_count": 34, "sum_latency": 528420374, "max_latency": 26504169, "min_latency": 10221376, "avg_latency": 15541775, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 27887.14023437506, "avg_ru": 820.2100068933842 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) on duplicate key update `repo_id` = `repo_id`", "digest": "c4fbafaa2a8738267641de2efab102a1271de41ba35b380878817534e3990bb7", "exec_count": 54, "sum_latency": 521182484, "max_latency": 24719165, "min_latency": 5416051, "avg_latency": 9651527, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 11947.558789062496, "avg_ru": 221.2510886863425 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383711, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , true , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , fa...", "digest": "c1dedf3fe7c518e9bdf1838710959139b6ffecd3e2dbafef8ebd0ae990cedb8e", "exec_count": 25, "sum_latency": 519629636, "max_latency": 33692766, "min_latency": 12817903, "avg_latency": 20785185, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 25254.545312500042, "avg_ru": 1010.1818125000017 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "594f84614a0cc70fc88c7162dc3888f08f5051a3468bc25f9031a517f17422be", "exec_count": 35, "sum_latency": 519093034, "max_latency": 33371741, "min_latency": 9878871, "avg_latency": 14831229, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 24664.884375000052, "avg_ru": 704.7109821428586 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383651, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , f...", "digest": "8828b349fb62fad122792fb83b7b50f2ccfa98d517996a8633351543b051418e", "exec_count": 31, "sum_latency": 517608676, "max_latency": 26018229, "min_latency": 11901188, "avg_latency": 16697054, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 27787.69570312506, "avg_ru": 896.3772807459696 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `event_logs` ( `id` , `created_at` ) values ( ... ) , ( ... ) , ( ... ) , ( ... ) , ( ... ) on duplicate key update `id` = `id`", "digest": "30c40f6a8dd91874ead39aa378f944e8ef401b55d0884c967c8cb813b91ab1fd", "exec_count": 60, "sum_latency": 507610013, "max_latency": 94767855, "min_latency": 1026368, "avg_latency": 8460166, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 1034.554217529297, "avg_ru": 17.242570292154948 }, { "summary_begin_time": 1731382199, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , true , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , fa...", "digest": "aca179c8ac10ad90b2d2eb06546990bf7ec57d40f514f198c330154e266e04bd", "exec_count": 26, "sum_latency": 506894936, "max_latency": 35421934, "min_latency": 13233366, "avg_latency": 19495959, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 25179.052343750045, "avg_ru": 968.4250901442325 }, { "summary_begin_time": 1731382501, "summary_end_time": 1731383651, "digest_text": "with `group_by_area` as ( select `gu` . `country_code` as `country_or_area` , count ( ? ) as `cnt` from `github_events` `ge` left join `github_users` `gu` on `ge` . `actor_login` = `gu` . `login` where `repo_id` in ( ? ) and `ge` . `type` = ? and `ge` . `action` = ? and `gu` . `country_code` not in ( ... ) group by `country_or_area` ) , `summary` as ( select sum ( `cnt` ) as `total` from `group_by_area` ) select `country_or_area` , `cnt` as `count` , `cnt` / `summary` . `total` as `percentage` from `group_by_area` , `summary` order by `cnt` desc ;", "digest": "b4d08c7d6845f9df9d89f57d88d4a9bfe0dd4068d1771a08051a2983d0e77ef1", "exec_count": 4, "sum_latency": 505267035, "max_latency": 269694489, "min_latency": 6887430, "avg_latency": 126316758, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 4468.563988240472, "avg_ru": 1117.140997060118 }, { "summary_begin_time": 1731382380, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) on duplicat...", "digest": "beac496de5be37a8d87f52aedc7280b0982b305313c549f1f18c5d83cf03a26c", "exec_count": 34, "sum_latency": 502846488, "max_latency": 23674978, "min_latency": 9650809, "avg_latency": 14789602, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 23553.674023437547, "avg_ru": 692.7551183363985 }, { "summary_begin_time": 1731382139, "summary_end_time": 1731383772, "digest_text": "insert into `github_events` ( `repo_id` , `repo_name` , `language` , `actor_id` , `actor_login` , `additions` , `deletions` , `action` , `commit_id` , `number` , `org_id` , `org_login` , `pr_merged` , `state` , `pr_merged_at` , `closed_at` , `comments` , `pr_or_issue_id` , `pr_changed_files` , `pr_review_comments` , `event_day` , `event_month` , `event_year` , `push_size` , `push_distinct_size` , `id` , `type` , `created_at` , `pr_or_issue_created_at` , `creator_user_id` , `creator_user_login` ) values ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , true , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , false , ... ) , ( ... , fa...", "digest": "a30fb47209c738ec037bae041bee36a3f916588f9b8706275baa1a083a79e65d", "exec_count": 27, "sum_latency": 501862451, "max_latency": 31312582, "min_latency": 13809735, "avg_latency": 18587498, "schema_name": "gharchive_dev", "plan_count": 1, "sum_ru": 26914.987304687544, "avg_ru": 996.8513816550942 } ], "nextPageToken": "", "totalSize": 10 } ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/statement-plans-detail.json ================================================ { "summary_begin_time": 1731395228, "summary_end_time": 1731396928, "digest_text": "select count ( ? ) as `cnt` , max ( `created_at` ) as `latest_created_at` , `unix_timestamp` ( max ( `created_at` ) ) as `latest_timestamp` from `github_events` where `created_at` between `from_unixtime` ( ? ) and ( utc_timestamp - interval ? minute ) and `from_unixtime` ( ? ) \u003e ( utc_timestamp - interval ? hour ) ;", "digest": "85310c3dcab6bcdd1a6c04a6f254ab47b9ce73165d54fb9d1a75931d6fb7f15e", "exec_count": 875, "stmt_type": "Select", "sum_latency": 5148485571, "max_latency": 164537484, "min_latency": 619447, "avg_latency": 5883983, "avg_compile_latency": 968229, "max_compile_latency": 2824436, "sum_cop_task_num": 18249, "max_cop_process_time": 162000000, "max_cop_wait_time": 5000000, "avg_process_time": 4080000, "max_process_time": 305000000, "avg_wait_time": 901714, "max_wait_time": 28000000, "avg_backoff_time": 2285, "max_backoff_time": 2000000, "avg_total_keys": 11801, "max_total_keys": 592472, "avg_processed_keys": 11780, "max_processed_keys": 592451, "avg_mem": 104312, "max_mem": 129833, "first_seen": 1731395230, "last_seen": 1731396926, "sample_user": "3EDFHZJX5iSzvfr.gh_api", "query_sample_text": "SELECT\n /*+ MAX_EXECUTION_TIME(15000) */\n COUNT(1) AS cnt,\n MAX(created_at) AS latest_created_at,\n UNIX_TIMESTAMP(MAX(created_at)) AS latest_timestamp\nFROM github_events\nWHERE\n created_at BETWEEN FROM_UNIXTIME(1731395039) AND (UTC_TIMESTAMP - INTERVAL 5 MINUTE)\n AND FROM_UNIXTIME(1731395039) \u003e (UTC_TIMESTAMP - INTERVAL 4 HOUR)", "schema_name": "gharchive_dev", "table_names": "gharchive_dev.github_events", "index_names": "github_events:index_github_events_on_created_at", "plan_count": 2, "plan": "\tid \ttask \testRows \toperator info \tactRows\texecution info \tmemory \tdisk\n\tProjection_5 \troot \t1 \tColumn#35, Column#36, unix_timestamp(Column#36)-\u003eColumn#37 \t1 \ttime:3.02ms, loops:2, Concurrency:OFF \t17.7 KB \tN/A\n\t└─HashAgg_15 \troot \t1 \tfuncs:count(Column#39)-\u003eColumn#35, funcs:max(Column#40)-\u003eColumn#36 \t1 \ttime:3ms, loops:2, partial_worker:{wall_time:2.986984ms, concurrency:5, task_num:1, tot_wait:14.759342ms, tot_exec:7.86µs, tot_time:14.77375ms, max:2.957061ms, p95:2.957061ms}, final_worker:{wall_time:3.010556ms, concurrency:5, task_num:1, tot_wait:14.851254ms, tot_exec:11.675µs, tot_time:14.86594ms, max:2.982471ms, p95:2.982471ms}\t99.8 KB \tN/A\n\t └─IndexReader_16 \troot \t1 \tindex:HashAgg_7 \t15 \ttime:2.94ms, loops:2, cop_task: {num: 21, max: 2.89ms, min: 392.8µs, avg: 951.8µs, p95: 1.38ms, max_proc_keys: 4362, p95_proc_keys: 1028, tot_proc: 2ms, rpc_num: 21, rpc_time: 19.6ms, copr_cache_hit_ratio: 0.00, build_task_duration: 115.7µs, max_distsql_concurrency: 15} \t600 Bytes\tN/A\n\t └─HashAgg_7 \tcop[tikv]\t1 \tfuncs:count(1)-\u003eColumn#39, funcs:max(gharchive_dev.github_events.created_at)-\u003eColumn#40 \t15 \ttikv_task:{proc max:4ms, min:0s, avg: 952.4µs, p80:4ms, p95:4ms, iters:26, tasks:21}, scan_detail: {total_process_keys: 7568, total_process_keys_size: 317856, total_keys: 7589, get_snapshot_time: 202µs, rocksdb: {block: {}}} \tN/A \tN/A\n\t └─IndexRangeScan_14\tcop[tikv]\t210539763.07\ttable:github_events, index:index_github_events_on_created_at(created_at), range:[2024-11-12 07:03:59,2024-11-12 07:06:14], keep order:false\t7568 \ttikv_task:{proc max:4ms, min:0s, avg: 952.4µs, p80:4ms, p95:4ms, iters:26, tasks:21} \tN/A \tN/A", "plan_digest": "458c98bf3604ae89a16e932e38e3c9cadb7500646f7fa88795dcb5e0fbf4e5be", "binary_plan": "{\"discardedDueToTooLong\":false,\"main\":{\"accessObjects\":[],\"actRows\":1,\"children\":[{\"accessObjects\":[],\"actRows\":1,\"children\":[{\"accessObjects\":[{\"dynamicPartitionObjects\":{\"objects\":[{\"allPartitions\":true,\"database\":\"gharchive_dev\",\"table\":\"github_events\"}]}}],\"actRows\":15,\"children\":[{\"accessObjects\":[],\"actRows\":15,\"children\":[{\"accessObjects\":[{\"scanObject\":{\"database\":\"gharchive_dev\",\"indexes\":[{\"cols\":[\"created_at\"],\"name\":\"index_github_events_on_created_at\"}],\"table\":\"github_events\"}}],\"actRows\":7568,\"copExecInfo\":{\"tikv_task\":{\"avg\":\"952.4µs\",\"iters\":\"26\",\"min\":\"0s\",\"p80\":\"4ms\",\"p95\":\"4ms\",\"proc max\":\"4ms\",\"tasks\":\"21\"}},\"cost\":34275873428.61,\"diagnosis\":[\"high_est_error\"],\"diskBytes\":\"N/A\",\"duration\":\"20.0004ms\",\"estRows\":210539763.075,\"labels\":[],\"memoryBytes\":\"N/A\",\"name\":\"IndexRangeScan_14\",\"operatorInfo\":\"range:[2024-11-12 07:03:59,2024-11-12 07:06:14], keep order:false\",\"rootBasicExecInfo\":{},\"rootGroupExecInfo\":[],\"storeType\":\"tikv\",\"taskType\":\"cop\"}],\"copExecInfo\":{\"scan_detail\":{\"get_snapshot_time\":\"202µs\",\"rocksdb\":{\"block\":{}},\"total_keys\":\"7589\",\"total_process_keys\":\"7568\",\"total_process_keys_size\":\"317856\"},\"tikv_task\":{\"avg\":\"952.4µs\",\"iters\":\"26\",\"min\":\"0s\",\"p80\":\"4ms\",\"p95\":\"4ms\",\"proc max\":\"4ms\",\"tasks\":\"21\"}},\"cost\":40579435442.6955,\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"20.0004ms\",\"estRows\":1,\"labels\":[],\"memoryBytes\":\"N/A\",\"name\":\"HashAgg_7\",\"operatorInfo\":\"funcs:count(1)-\\u003eColumn#39, funcs:max(gharchive_dev.github_events.created_at)-\\u003eColumn#40\",\"rootBasicExecInfo\":{},\"rootGroupExecInfo\":[],\"storeType\":\"tikv\",\"taskType\":\"cop\"}],\"copExecInfo\":{},\"cost\":2705295700.4037004,\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"20.0004ms\",\"estRows\":1,\"labels\":[],\"memoryBytes\":\"600\",\"name\":\"IndexReader_16\",\"operatorInfo\":\"index:HashAgg_7\",\"rootBasicExecInfo\":{\"loops\":\"2\",\"time\":\"2.94ms\"},\"rootGroupExecInfo\":[{\"cop_task\":{\"avg\":\"951.8µs\",\"build_task_duration\":\"115.7µs\",\"copr_cache_hit_ratio\":\"0.00\",\"max\":\"2.89ms\",\"max_distsql_concurrency\":\"15\",\"max_proc_keys\":\"4362\",\"min\":\"392.8µs\",\"num\":\"21\",\"p95\":\"1.38ms\",\"p95_proc_keys\":\"1028\",\"rpc_num\":\"21\",\"rpc_time\":\"19.6ms\",\"tot_proc\":\"2ms\"}}],\"storeType\":\"tidb\",\"taskType\":\"root\"}],\"copExecInfo\":{},\"cost\":2705297237.9637003,\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"20.0004ms\",\"estRows\":1,\"labels\":[],\"memoryBytes\":\"102180\",\"name\":\"HashAgg_15\",\"operatorInfo\":\"funcs:count(Column#39)-\\u003eColumn#35, funcs:max(Column#40)-\\u003eColumn#36\",\"rootBasicExecInfo\":{\"loops\":\"2\",\"time\":\"3ms\"},\"rootGroupExecInfo\":[{\"final_worker\":{\"concurrency\":\"5\",\"max\":\"2.982471ms\",\"p95\":\"2.982471ms\",\"task_num\":\"1\",\"tot_exec\":\"11.675µs\",\"tot_time\":\"14.86594ms\",\"tot_wait\":\"14.851254ms\",\"wall_time\":\"3.010556ms\"},\"partial_worker\":{\"concurrency\":\"5\",\"max\":\"2.957061ms\",\"p95\":\"2.957061ms\",\"task_num\":\"1\",\"tot_exec\":\"7.86µs\",\"tot_time\":\"14.77375ms\",\"tot_wait\":\"14.759342ms\",\"wall_time\":\"2.986984ms\"}}],\"storeType\":\"tidb\",\"taskType\":\"root\"}],\"copExecInfo\":{},\"cost\":2705297248.1433,\"diagnosis\":[],\"diskBytes\":\"N/A\",\"duration\":\"3.02ms\",\"estRows\":1,\"labels\":[],\"memoryBytes\":\"18160\",\"name\":\"Projection_5\",\"operatorInfo\":\"Column#35, Column#36, unix_timestamp(Column#36)-\\u003eColumn#37\",\"rootBasicExecInfo\":{\"loops\":\"2\",\"time\":\"3.02ms\"},\"rootGroupExecInfo\":[{\"Concurrency\":\"OFF\"}],\"storeType\":\"tidb\",\"taskType\":\"root\"},\"withRuntimeStats\":true}", "plan_hint": "hash_agg(@`sel_1`), use_index(@`sel_1` `gharchive_dev`.`github_events` `index_github_events_on_created_at`), no_order_index(@`sel_1` `gharchive_dev`.`github_events` `index_github_events_on_created_at`), agg_to_cop(@`sel_1`), max_execution_time(15000)", "sum_ru": 16599.290484619145, "sum_rru": 16599.290484619145, "avg_ru": 18.970617696707595, "avg_rru": 18.970617696707595 } ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/statement-plans-list.json ================================================ { "data": [ { "summary_begin_time": 1731395228, "summary_end_time": 1731396928, "digest_text": "select count ( ? ) as `cnt` , max ( `created_at` ) as `latest_created_at` , `unix_timestamp` ( max ( `created_at` ) ) as `latest_timestamp` from `github_events` where `created_at` between `from_unixtime` ( ? ) and ( utc_timestamp - interval ? minute ) and `from_unixtime` ( ? ) \u003e ( utc_timestamp - interval ? hour ) ;", "digest": "85310c3dcab6bcdd1a6c04a6f254ab47b9ce73165d54fb9d1a75931d6fb7f15e", "exec_count": 869, "stmt_type": "Select", "sum_latency": 5144430781, "max_latency": 164537484, "min_latency": 2662266, "avg_latency": 5919943, "avg_mem": 104850, "max_mem": 129833, "schema_name": "gharchive_dev", "plan_digest": "458c98bf3604ae89a16e932e38e3c9cadb7500646f7fa88795dcb5e0fbf4e5be", "plan_hint": "hash_agg(@`sel_1`), use_index(@`sel_1` `gharchive_dev`.`github_events` `index_github_events_on_created_at`), no_order_index(@`sel_1` `gharchive_dev`.`github_events` `index_github_events_on_created_at`), agg_to_cop(@`sel_1`), max_execution_time(15000)" }, { "summary_begin_time": 1731395712, "summary_end_time": 1731395894, "digest_text": "select count ( ? ) as `cnt` , max ( `created_at` ) as `latest_created_at` , `unix_timestamp` ( max ( `created_at` ) ) as `latest_timestamp` from `github_events` where `created_at` between `from_unixtime` ( ? ) and ( utc_timestamp - interval ? minute ) and `from_unixtime` ( ? ) \u003e ( utc_timestamp - interval ? hour ) ;", "digest": "85310c3dcab6bcdd1a6c04a6f254ab47b9ce73165d54fb9d1a75931d6fb7f15e", "exec_count": 6, "stmt_type": "Select", "sum_latency": 4054790, "max_latency": 797059, "min_latency": 619447, "avg_latency": 675798, "avg_mem": 26418, "max_mem": 27912, "schema_name": "gharchive_dev", "plan_digest": "8c0a7b1bc1a0894b4dab93844ea1fd5d6a10f5515a4ebd66ca56d3f71bd7b27f", "plan_hint": "stream_agg(@`sel_1`), max_execution_time(15000)" } ] } ================================================ FILE: ui-v2/packages/api/server/src/azores/sample-res/statement-slow-query-list.json ================================================ [ { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724674023", "success": 0, "timestamp": 1731484007.364735, "query_time": 17.356576616, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 3415, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724674025", "success": 0, "timestamp": 1731483875.213136, "query_time": 5.20261797, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 3403, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724623537", "success": 0, "timestamp": 1731483761.945083, "query_time": 11.936930494, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 3413, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724623531", "success": 0, "timestamp": 1731483636.588591, "query_time": 6.580224996, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1862, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724623537", "success": 0, "timestamp": 1731483510.391377, "query_time": 0.382754965, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1853, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724674023", "success": 0, "timestamp": 1731483407.392716, "query_time": 17.384262229, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1864, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724623531", "success": 0, "timestamp": 1731483278.527449, "query_time": 8.519016228, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1862, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724623537", "success": 0, "timestamp": 1731483171.406427, "query_time": 21.397817264, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1842, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724674025", "success": 0, "timestamp": 1731483048.661767, "query_time": 18.65083934, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 3415, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724623531", "success": 0, "timestamp": 1731482913.415495, "query_time": 3.40567882, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1859, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724674025", "success": 0, "timestamp": 1731482800.178462, "query_time": 10.167798402, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1864, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724623537", "success": 0, "timestamp": 1731482680.645222, "query_time": 10.636440703, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1864, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724674023", "success": 0, "timestamp": 1731482556.314372, "query_time": 6.305582706, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1861, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724674025", "success": 0, "timestamp": 1731482442.929289, "query_time": 12.918626472, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1864, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" }, { "digest": "cb0ce9437949f181c6f405bcdfe92e9b179e95f307e5b0817aa7c773430511d5", "query": "SELECT COUNT(DISTINCT actor_id) AS developers, COUNT(DISTINCT repo_id) AS repos, SUM(IF(action = 'closed' AND pr_merged = true, additions, 0)) AS additions, SUM(IF(action = 'closed' AND pr_merged = true, deletions, 0)) AS deletions, COUNT(DISTINCT IF(action = 'opened', pr_or_issue_id, NULL)) AS opened_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = false, pr_or_issue_id, NULL)) AS closed_prs, COUNT(DISTINCT IF(action = 'closed' AND pr_merged = true, pr_or_issue_id, NULL)) AS merged_prs FROM github_events ge WHERE type = 'PullRequestEvent' AND created_at \u003e DATE_SUB(NOW(), INTERVAL 24 HOUR) ;", "instance": "", "db": "", "connection_id": "3483529913724623537", "success": 0, "timestamp": 1731482317.57419, "query_time": 7.565186919, "parse_time": 0, "compile_time": 0, "rewrite_time": 0, "preproc_subqueries_time": 0, "optimize_time": 0, "wait_ts": 0, "cop_time": 0, "lock_keys_time": 0, "write_sql_response_total": 0, "exec_retry_time": 0, "memory_max": 1861, "disk_max": 0, "txn_start_ts": "", "prev_stmt": "", "plan": "", "is_internal": 0, "index_names": "", "stats": "", "backoff_types": "", "prepared": 0, "plan_from_cache": 0, "plan_from_binding": 0, "user": "", "host": "", "process_time": 0, "wait_time": 0, "backoff_time": 0, "get_commit_ts_time": 0, "local_latch_wait_time": 0, "resolve_lock_time": 0, "prewrite_time": 0, "wait_prewrite_binlog_time": 0, "commit_time": 0, "commit_backoff_time": 0, "cop_proc_avg": 0, "cop_proc_p90": 0, "cop_proc_max": 0, "cop_wait_avg": 0, "cop_wait_p90": 0, "cop_wait_max": 0, "write_keys": 0, "write_size": 0, "prewrite_region": 0, "txn_retry": 0, "request_count": 0, "process_keys": 0, "total_keys": 0, "cop_proc_addr": "", "cop_wait_addr": "", "rocksdb_delete_skipped_count": 0, "rocksdb_key_skipped_count": 0, "rocksdb_block_cache_hit_count": 0, "rocksdb_block_read_count": 0, "rocksdb_block_read_byte": 0, "ru": 0, "time_queued_by_rc": 0, "resource_group": "" } ] ================================================ FILE: ui-v2/packages/api/server/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true, "lib": ["ESNext"], "types": ["@cloudflare/workers-types/2023-07-01"], "jsx": "react-jsx", "jsxImportSource": "hono/jsx" } } ================================================ FILE: ui-v2/packages/api/server/wrangler.toml ================================================ name = "tidb-dashboard-lib-api-server" main = "src/azores/index.ts" compatibility_date = "2024-12-14" # compatibility_flags = [ "nodejs_compat" ] # [vars] # MY_VAR = "my-variable" # [[kv_namespaces]] # binding = "MY_KV_NAMESPACE" # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # [[r2_buckets]] # binding = "MY_BUCKET" # bucket_name = "my-bucket" # [[d1_databases]] # binding = "DB" # database_name = "my-database" # database_id = "" # [ai] # binding = "AI" # [observability] # enabled = true # head_sampling_rate = 1 ================================================ FILE: ui-v2/packages/libs/1-charts/CHANGELOG.md ================================================ # @pingcap-incubator/tidb-dashboard-lib-charts ## 0.16.0 ### Minor Changes - bump version ## 0.15.0 ### Minor Changes - refactor: re-exports lib-uitls/lib-charts/lib-primitive-ui/lib-biz-ui from lib-apps ## 0.14.1 ### Patch Changes - upgrade @elastic/charts to latest version ## 0.14.0 ### Minor Changes - upgrade uikit, refine table empty status, refine chart empty legend name case ## 0.13.1 ### Patch Changes - increase data format decimal ## 0.13.0 ### Minor Changes - suport brush to change time range ## 0.12.0 ### Minor Changes - update metrics charts ## 0.11.0 ### Minor Changes - fix time-range-picker, refine cols-multi-select ## 0.10.0 ### Minor Changes - i18n for slow query and statement app ## 0.9.0 ### Minor Changes - upgrade uikit ## 0.8.0 ### Minor Changes - support advanced filters for diagnosis ## 0.7.0 ### Minor Changes - refine pagination and sort url state ## 0.6.0 ### Minor Changes - refine ## 0.5.0 ### Minor Changes - refine slow-query and statement apps ## 0.4.0 ### Minor Changes - update uikit ## 0.3.0 ### Minor Changes - add azores cluster metrics page ## 0.2.0 ### Minor Changes - add metrics azores host page ## 0.1.0 ### Minor Changes - update i18n, format utils ## 0.0.9 ### Patch Changes - add i18n ## 0.0.8 ### Patch Changes - set time formatter ## 0.0.7 ### Patch Changes - support dark mode for lib-charts ## 0.0.6 ### Patch Changes - refine ## 0.0.5 ### Patch Changes - refactor metric ## 0.0.4 ### Patch Changes - upgrade uikit ## 0.0.3 ### Patch Changes - add prom utils functions ## 0.0.2 ### Patch Changes - first release ================================================ FILE: ui-v2/packages/libs/1-charts/README.md ================================================ # @pingcap-incubator/tidb-dashboard-lib-charts ## Usage ### Step 1 Import style css ```ts import "@pingcap-incubator/tidb-dashboard-lib-charts/dist/style.css" ``` ### Step 2 Use `ChartThemeSwitch` component in the top level (for example `App` component) ```tsx function Routes() { const theme = useComputedColorScheme() return ( {/* ... */} ) } ``` ### Step 3 Render series data by `SeriesChart` component ```tsx ``` ================================================ FILE: ui-v2/packages/libs/1-charts/package.json ================================================ { "name": "@pingcap-incubator/tidb-dashboard-lib-charts", "version": "0.16.0", "description": "", "type": "module", "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist", "README.md", "CHANGELOG.md" ], "scripts": { "tsc:watch": "tsc --watch", "rollup:watch": "rollup -c --watch", "css:watch": "sass src/style.scss dist/style.css --watch", "dev": "concurrently --kill-others \"pnpm tsc:watch\" \"pnpm rollup:watch\" \"pnpm css:watch\"", "css:build": "sass src/style.scss dist/style.css", "build": "tsc && rollup -c && pnpm css:build" }, "keywords": [], "author": "", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^12.1.1", "@types/react": "^18.3.12", "react": "^18.3.1", "rollup": "^4.24.0", "sass": "^1.81.0", "tslib": "^2.8.0" }, "peerDependencies": { "react": "^18.3.1" }, "dependencies": { "@baurine/grafana-value-formats": "^1.0.5", "@elastic/charts": "^69.1.1" } } ================================================ FILE: ui-v2/packages/libs/1-charts/rollup.config.js ================================================ import typescript from "@rollup/plugin-typescript" export default { input: "src/index.ts", output: { dir: "dist", format: "es", }, plugins: [typescript()], } ================================================ FILE: ui-v2/packages/libs/1-charts/src/chart-theme-switch.tsx ================================================ import { useEffect } from "react" const LIGHT_TOKEN = "charts-light-theme" const DARK_TOKEN = "charts-dark-theme" export function useChartTheme(theme: "light" | "dark") { useEffect(() => { if (theme === "light") { window.document.body.classList.remove(DARK_TOKEN) window.document.body.classList.add(LIGHT_TOKEN) } else { window.document.body.classList.remove(LIGHT_TOKEN) window.document.body.classList.add(DARK_TOKEN) } }, [theme]) } export function ChartThemeSwitch({ value }: { value: "light" | "dark" }) { useChartTheme(value) return null } ================================================ FILE: ui-v2/packages/libs/1-charts/src/index.ts ================================================ export * from "./series-chart" export * from "./chart-theme-switch" export * from "./type" ================================================ FILE: ui-v2/packages/libs/1-charts/src/sample-data.ts ================================================ // see https://github.com/elastic/elastic-charts/blob/main/packages/charts/src/utils/data_samples/test_dataset_kibana.ts ================================================ FILE: ui-v2/packages/libs/1-charts/src/series-chart.tsx ================================================ import { getValueFormat } from "@baurine/grafana-value-formats" import { Axis, BrushEvent, Chart, DARK_THEME, LIGHT_THEME, LegendValue, LineSeries, Position, ScaleType, SeriesIdentifier, Settings, SettingsProps, timeFormatter, } from "@elastic/charts" import { useCallback, useMemo } from "react" import { renderSeriesData } from "./series-render" import { SeriesData } from "./type" function formatNumByUnit(value: number, unit: string) { const formatFn = getValueFormat(unit) if (!formatFn) { return value + "" } return formatFn(value, 2) } function niceTimeFormat(seconds: number) { // if (max time - min time > 5 days) if (seconds > 5 * 24 * 60 * 60) return "MM-DD" // if (max time - min time > 1 day) if (seconds > 1 * 24 * 60 * 60) return "MM-DD HH:mm" // if (max time - min time > 5 minutes) if (seconds > 5 * 60) return "HH:mm" return "HH:mm:ss" } // align the time range according to the minimal interval and minimal range size. export function alignRange( range: TimeRangeValue, minIntervalSec = 30, minRangeSec = 60, ): TimeRangeValue { let [min, max] = range if (max - min < minRangeSec) { min = max - minRangeSec } min = Math.floor(min / minIntervalSec) * minIntervalSec max = Math.ceil(max / minIntervalSec) * minIntervalSec return [min, max] } const tooltipHeaderFormatter = timeFormatter("YYYY-MM-DD HH:mm:ss") type TimeRangeValue = [number, number] type SeriesChartProps = { theme?: "light" | "dark" data: SeriesData[] unit: string timeRange: TimeRangeValue charSetting?: SettingsProps onBrush?: (range: TimeRangeValue) => void } export function SeriesChart({ theme = "light", data, unit, timeRange, charSetting, onBrush, }: SeriesChartProps) { const xAxisFormatter = useMemo( () => timeFormatter(niceTimeFormat(timeRange[1] - timeRange[0])), [timeRange], ) // note: this doesn't work with StrictMode in debug mode (it's fine in production) const handleBrushEnd = useCallback( (ev: BrushEvent) => { if (!ev.x) { return } const timeRange: TimeRangeValue = [ Math.floor((ev.x[0] as number) / 1000), Math.floor((ev.x[1] as number) / 1000), ] onBrush?.(alignRange(timeRange)) }, [onBrush], ) // @todo: it seems doesn't work, try it later const specIds = useMemo(() => data.map((d) => d.id), [data]) const handleLegendSort = useCallback( (a: SeriesIdentifier, b: SeriesIdentifier) => { return specIds.indexOf(a.specId) - specIds.indexOf(b.specId) }, [specIds], ) return ( formatNumByUnit(v, unit)} /> {data.map(renderSeriesData)} {/* for avoid chart to show "no data" when data is empty */} {data.length === 0 && ( )} ) } ================================================ FILE: ui-v2/packages/libs/1-charts/src/series-render.tsx ================================================ import { AreaSeries, BarSeries, LineSeries, ScaleType } from "@elastic/charts" import { SeriesData } from "./type" export function renderSeriesData(sd: SeriesData) { if (sd.type === "line") { return renderLine(sd) } else if (sd.type === "bar_stacked") { return renderStackedBar(sd) } else if (sd.type === "area_stack") { return renderAreaStack(sd) } else if (sd.type === "area") { return renderArea(sd) } return renderLine(sd) } function renderLine(sd: SeriesData) { return ( ) } function renderStackedBar(sd: SeriesData) { return ( ) } function renderAreaStack(sd: SeriesData) { return ( ) } function renderArea(sd: SeriesData) { return ( ) } ================================================ FILE: ui-v2/packages/libs/1-charts/src/style.scss ================================================ .charts-light-theme { @import '../node_modules/@elastic/charts/dist/theme_only_light'; } .charts-dark-theme { @import '../node_modules/@elastic/charts/dist/theme_only_dark'; } ================================================ FILE: ui-v2/packages/libs/1-charts/src/type.ts ================================================ import { LineSeriesStyle, RecursivePartial } from "@elastic/charts" export type SeriesDataType = "line" | "area" | "bar_stacked" | "area_stack" export type DataPoint = [msTimestamp: number, value: number | null] export type SeriesData = { id: string name: string data: DataPoint[] type?: SeriesDataType color?: string | ((seriesName: string) => string | undefined) lineSeriesStyle?: RecursivePartial } ================================================ FILE: ui-v2/packages/libs/1-charts/tsconfig.json ================================================ { "extends": "../../tsconfig.app.json", "compilerOptions": { "outDir": "./dist" }, "include": ["./src"] } ================================================ FILE: ui-v2/packages/libs/1-icons/CHANGELOG.md ================================================ # @pingcap-incubator/tidb-dashboard-lib-icons ## 0.14.0 ### Minor Changes - bump version ## 0.13.0 ### Minor Changes - refactor: re-exports lib-uitls/lib-charts/lib-primitive-ui/lib-biz-ui from lib-apps ## 0.12.0 ### Minor Changes - upgrade uikit, refine table empty status, refine chart empty legend name case ## 0.11.0 ### Minor Changes - fix time-range-picker, refine cols-multi-select ## 0.10.0 ### Minor Changes - i18n for slow query and statement app ## 0.9.0 ### Minor Changes - upgrade uikit ## 0.8.0 ### Minor Changes - support advanced filters for diagnosis ## 0.7.0 ### Minor Changes - refine pagination and sort url state ## 0.6.0 ### Minor Changes - refine ## 0.5.0 ### Minor Changes - refine slow-query and statement apps ## 0.4.0 ### Minor Changes - update uikit ## 0.3.0 ### Minor Changes - add azores cluster metrics page ## 0.2.0 ### Minor Changes - add metrics azores host page ## 0.1.0 ### Minor Changes - update i18n, format utils ## 0.0.7 ### Patch Changes - add i18n ## 0.0.6 ### Patch Changes - support dark mode for lib-charts ## 0.0.5 ### Patch Changes - refine ## 0.0.4 ### Patch Changes - refactor metric ## 0.0.3 ### Patch Changes - upgrade uikit ## 0.0.2 ### Patch Changes - first release ================================================ FILE: ui-v2/packages/libs/1-icons/package.json ================================================ { "name": "@pingcap-incubator/tidb-dashboard-lib-icons", "version": "0.14.0", "description": "", "type": "module", "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist", "README.md", "CHANGELOG.md" ], "scripts": { "tsc:watch": "tsc --watch", "rollup:watch": "rollup -c --watch", "dev": "concurrently --kill-others \"pnpm tsc:watch\" \"pnpm rollup:watch\"", "build": "tsc && rollup -c" }, "keywords": [], "author": "", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^12.1.1", "@tidbcloud/uikit": "catalog:", "@types/react": "^18.3.12", "react": "^18.3.1", "rollup": "^4.24.0", "tslib": "^2.8.0" }, "peerDependencies": { "@tidbcloud/uikit": "catalog:", "react": "^18.3.1" } } ================================================ FILE: ui-v2/packages/libs/1-icons/rollup.config.js ================================================ import typescript from "@rollup/plugin-typescript" export default { input: "src/index.ts", output: { dir: "dist", format: "es", }, plugins: [typescript()], } ================================================ FILE: ui-v2/packages/libs/1-icons/src/index.ts ================================================ // decided not to export // export * from "@tidbcloud/uikit/icons" export function icons() { console.log("placeholder") } ================================================ FILE: ui-v2/packages/libs/1-icons/tsconfig.json ================================================ { "extends": "../../tsconfig.app.json", "compilerOptions": { "outDir": "./dist" }, "include": ["./src"] } ================================================ FILE: ui-v2/packages/libs/1-utils/CHANGELOG.md ================================================ # @pingcap-incubator/tidb-dashboard-lib-utils ## 0.15.0 ### Minor Changes - bump version ## 0.14.0 ### Minor Changes - refactor i18n and fitlers state ## 0.13.0 ### Minor Changes - refactor: re-exports lib-uitls/lib-charts/lib-primitive-ui/lib-biz-ui from lib-apps ## 0.12.1 ### Patch Changes - support now-to-future type relative time range ## 0.12.0 ### Minor Changes - upgrade uikit, refine table empty status, refine chart empty legend name case ## 0.11.2 ### Patch Changes - refine chart same as grafana ## 0.11.1 ### Patch Changes - refine chart step ## 0.11.0 ### Minor Changes - fix time-range-picker, refine cols-multi-select ## 0.10.0 ### Minor Changes - i18n for slow query and statement app ## 0.9.0 ### Minor Changes - upgrade uikit ## 0.8.1 ### Patch Changes - renaming fields ## 0.8.0 ### Minor Changes - support advanced filters for diagnosis ## 0.7.0 ### Minor Changes - refine pagination and sort url state ## 0.6.0 ### Minor Changes - refine ## 0.5.0 ### Minor Changes - refine slow-query and statement apps ## 0.4.0 ### Minor Changes - update uikit ## 0.3.0 ### Minor Changes - add azores cluster metrics page ## 0.2.0 ### Minor Changes - add metrics azores host page ## 0.1.0 ### Minor Changes - update i18n, format utils ## 0.0.10 ### Patch Changes - refine i18n ## 0.0.9 ### Patch Changes - test i18n ## 0.0.8 ### Patch Changes - debug i18n ## 0.0.7 ### Patch Changes - add i18n ## 0.0.6 ### Patch Changes - support dark mode for lib-charts ## 0.0.5 ### Patch Changes - refine ## 0.0.4 ### Patch Changes - refactor metric ## 0.0.3 ### Patch Changes - upgrade uikit ## 0.0.2 ### Patch Changes - first release ================================================ FILE: ui-v2/packages/libs/1-utils/package.json ================================================ { "name": "@pingcap-incubator/tidb-dashboard-lib-utils", "version": "0.15.0", "description": "", "type": "module", "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist", "README.md", "CHANGELOG.md" ], "scripts": { "tsc:watch": "tsc --watch", "rollup:watch": "rollup -c --watch", "dev": "concurrently --kill-others \"pnpm tsc:watch\" \"pnpm rollup:watch\"", "build": "tsc && rollup -c" }, "keywords": [], "author": "", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^12.1.1", "@tidbcloud/uikit": "catalog:", "@types/react": "^18.3.12", "i18next": "^23.16.2", "i18next-browser-languagedetector": "^8.0.0", "react": "^18.3.1", "react-i18next": "^15.1.0", "rollup": "^4.24.0", "tslib": "^2.8.0", "zustand": "^5.0.2" }, "peerDependencies": { "@tidbcloud/uikit": "catalog:", "i18next": "^23.16.2", "i18next-browser-languagedetector": "^8.0.0", "react": "^18.3.1", "react-i18next": "^15.1.0", "zustand": "^5.0.2" }, "dependencies": { "@baurine/grafana-value-formats": "^1.0.5", "@baurine/sql-formatter-plus": "^1.5.3", "pretty-ms": "^9.2.0", "string-template": "^1.0.0" } } ================================================ FILE: ui-v2/packages/libs/1-utils/rollup.config.js ================================================ import typescript from "@rollup/plugin-typescript" export default { input: "src/index.ts", output: { dir: "dist", format: "es", }, plugins: [typescript()], } ================================================ FILE: ui-v2/packages/libs/1-utils/src/delay.ts ================================================ export function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } ================================================ FILE: ui-v2/packages/libs/1-utils/src/env.d.ts ================================================ declare module "@baurine/sql-formatter-plus" declare module "string-template" ================================================ FILE: ui-v2/packages/libs/1-utils/src/format.ts ================================================ import { getValueFormat } from "@baurine/grafana-value-formats" import { format } from "@baurine/sql-formatter-plus" import { dayjs } from "@tidbcloud/uikit/utils" import prettyMs from "pretty-ms" export function formatTime( value: number | Date, // number is unix timestamp, unit is milliseconds format: string = "YYYY-MM-DD HH:mm:ss", ) { return dayjs(value).format(format) } export function formatDuration(seconds: number, short = false) { if (short) { return prettyMs(seconds * 1000, { compact: true }) } else { return prettyMs(seconds * 1000, { verbose: true }) } } export function formatNumByUnit( value: number, unit: string, precision: number = 1, ) { if (isNaN(value)) { return "" } const formatFn = getValueFormat(unit) if (!formatFn) { return value + "" } if (unit === "short") { return formatFn(value, 0, precision) } return formatFn(value, precision) } export function formatSql(sql: string, compact: boolean = false): string { let formattedSQL = sql try { formattedSQL = format(sql, { uppercase: true, language: "tidb" }) } catch (_e) { console.log(sql) } if (compact) { formattedSQL = simpleMinifySql(formattedSQL) } return formattedSQL } // remove extra spaces to make sql more compact export function simpleMinifySql(str: string) { return str .replace(/\s{1,}/g, " ") .replace(/\{\s{1,}/g, "{") .replace(/\}\s{1,}/g, "}") .replace(/;\s{1,}/g, ";") .replace(/\/\*\s{1,}/g, "/*") .replace(/\*\/\s{1,}/g, "*/") } ================================================ FILE: ui-v2/packages/libs/1-utils/src/i18n.ts ================================================ import { useHotkeys } from "@tidbcloud/uikit/hooks" import i18next, { Resource, TOptions } from "i18next" import LanguageDetector from "i18next-browser-languagedetector" import { useCallback, useMemo } from "react" import { initReactI18next, useTranslation } from "react-i18next" export { Trans } from "react-i18next" const DEF_DISTRO = { pd: "PD", tidb: "TiDB", tikv: "TiKV", tiflash: "TiFlash", ticdc: "TiCDC", } export function initI18n() { i18next .use(initReactI18next) .use(LanguageDetector) .init({ resources: {}, fallbackLng: "en", // fallbackLng won't change the detected language supportedLngs: ["zh", "en"], // supportedLngs will change the detected language interpolation: { escapeValue: false, defaultVariables: { distro: DEF_DISTRO }, }, }) return i18next } export function changeLang(lang: string) { i18next.changeLanguage(lang) } function addResourceBundles(langsLocales: Resource) { Object.keys(langsLocales).forEach((lang) => { const locales = langsLocales[lang] const ns = locales["__namespace__"] as string if (!ns) { throw new Error(`__namespace__ not found in locales`) } i18next.addResourceBundle(lang, ns, locales, true, true) }) } export function addLangsLocales(langsLocales: Resource) { if (i18next.isInitialized) { addResourceBundles(langsLocales) } else { i18next.on("initialized", function (_options) { addResourceBundles(langsLocales) }) } } export function useTn(ns: string) { const { t, i18n } = useTranslation() // translate by key // example: // tk('panels.instance_top.title', 'Top 5 Node Utilization') // tk(`panels.${category}.title`) // tk("time_range.hour", "{{count}} hr", { count: 1 }) // tk("time_range.hour", "{{count}} hrs", { count: 24 }) // tk("time_range.hour", "", {count: n}) const tk = useCallback( (i18nKey: string, defVal?: string, options?: TOptions) => { return t(i18nKey, defVal ?? i18nKey, { ns, ...options }) }, [t, ns], ) // translate by text // example: // tt("how are you?") // tt("Hello.World") // tt("Clear Filters") // tt("hello {{name}}", { name: "world" }) const tt = useCallback( (text: string, options?: TOptions) => { return t(text, text, { ns, ...options }) }, [t, ns], ) const ret = useMemo(() => { return { tk, tt, i18n, t } }, [tk, tt, i18n, t]) return ret } export function useHotkeyChangeLang(hotkey: string = "mod+L") { const { i18n } = useTranslation() useHotkeys([ [hotkey, () => i18n.changeLanguage(i18n.language === "en" ? "zh" : "en")], ]) } ================================================ FILE: ui-v2/packages/libs/1-utils/src/index.ts ================================================ // decided not to export // export * from "@tidbcloud/uikit/hooks" // export * from "@tidbcloud/uikit/utils" export * from "./format" export * from "./delay" export * from "./prom" export * from "./time-range" export * from "./i18n" export * from "./url-state" export * from "./memory-state" ================================================ FILE: ui-v2/packages/libs/1-utils/src/memory-state/index.ts ================================================ export * from "./reset-filters-state" ================================================ FILE: ui-v2/packages/libs/1-utils/src/memory-state/reset-filters-state.ts ================================================ import { create } from "zustand" interface ResetFiltersState { resetVal: number reset: () => void } export const useResetFiltersState = create((set) => ({ resetVal: 0, reset: () => set({ resetVal: Date.now() }), })) ================================================ FILE: ui-v2/packages/libs/1-utils/src/prom.ts ================================================ import interpolate from "string-template" export enum TransformNullValue { NULL = "null", AS_ZERO = "as_zero", } export type PromResultItem = { metric: Record values: ([number, string] | { timestamp: number; value: string })[] } export type PromSeriesItem = { name: string data: [number, number | null][] } //////////////////////////////// const POSITIVE_INFINITY_SAMPLE_VALUE = "+Inf" const NEGATIVE_INFINITY_SAMPLE_VALUE = "-Inf" function parseStrVal(value: string): number { switch (value) { case POSITIVE_INFINITY_SAMPLE_VALUE: return Number.POSITIVE_INFINITY case NEGATIVE_INFINITY_SAMPLE_VALUE: return Number.NEGATIVE_INFINITY default: return parseFloat(value) } } function transformStrVal(value: string, nullValue?: TransformNullValue) { let v: number | null = parseStrVal(value) if (isNaN(v)) { if (nullValue === TransformNullValue.AS_ZERO) { v = 0 } else { v = null } } return v } export function transformPromResultItem( item: PromResultItem, nameTemplate: string, nullValue?: TransformNullValue, ): PromSeriesItem { let name = interpolate(nameTemplate, item.metric) if (name === "") { name = Object.entries(item.metric) .filter(([, v]) => v !== "") .map(([k, v]) => `${k}=${v}`) .join(",") if (name !== "") { name = "{" + name + "}" } } if (name === "") { name = "value" } return { name, data: item.values.map((v) => { const [ts, val] = Array.isArray(v) ? v : [v.timestamp, v.value] return [ts * 1000, transformStrVal(val, nullValue)] }), } } //////////////////////////////// export const DEF_SCRAPE_INTERVAL = 30 export function resolvePromQLTemplate( promql: string, step: number, scrapeInterval: number = DEF_SCRAPE_INTERVAL, ): string { return promql.replace( /\$__rate_interval/g, `${Math.max(step + scrapeInterval, 4 * scrapeInterval)}s`, ) } export function calcPromQueryStep( tr: [number, number], width: number, scrapeInterval: number = DEF_SCRAPE_INTERVAL, minBinWidth: number = 4, ) { if (width <= 0) { return scrapeInterval } const points = width / minBinWidth const step = (tr[1] - tr[0]) / points const fixedStep = Math.ceil(step / scrapeInterval) * scrapeInterval return fixedStep } ================================================ FILE: ui-v2/packages/libs/1-utils/src/time-range.ts ================================================ import { dayjs } from "@tidbcloud/uikit/utils" //////////////////////////////////////////////////////////// export type TimeRangeValue = [from: number, to: number] export interface RelativeTimeRange { // to be compatible, keep "relative", and "before-now" has same meaning with "relative" type: "relative" | "before-to-now" | "now-to-future" value: number // unit: seconds } export interface AbsoluteTimeRange { type: "absolute" value: TimeRangeValue // unit: seconds } export type TimeRange = RelativeTimeRange | AbsoluteTimeRange //////////////////////////////////////////////////////////// export const toTimeRangeValue = ( timeRange: TimeRange, offset = 0, ): TimeRangeValue => { if (timeRange.type === "absolute") { return timeRange.value.map((t) => t + offset) as TimeRangeValue } else { const now = dayjs().unix() if (timeRange.type === "now-to-future") { return [now + offset, now + timeRange.value + offset] } return [now - timeRange.value + offset, now + offset] } } export function fromTimeRangeValue(v: TimeRangeValue): AbsoluteTimeRange { return { type: "absolute", value: [...v], } } //////////////////////////////////////////////////////////// export type URLTimeRange = { from: string; to: string } export const toURLTimeRange = (timeRange: TimeRange): URLTimeRange => { if (timeRange.type === "absolute") { return { from: timeRange.value[0] + "", to: timeRange.value[1] + "" } } if (timeRange.type === "now-to-future") { return { from: "now", to: timeRange.value + "" } } return { from: `${timeRange.value}`, to: "now" } } export const urlToTimeRange = (urlTimeRange: URLTimeRange): TimeRange => { if (urlTimeRange.from === "now") { return { type: "now-to-future", value: Number(urlTimeRange.to) } } if (urlTimeRange.to === "now") { return { type: "relative", value: Number(urlTimeRange.from) } } return { type: "absolute", value: [Number(urlTimeRange.from), Number(urlTimeRange.to)], } } export const urlToTimeRangeValue = ( urlTimeRange: URLTimeRange, ): TimeRangeValue => { return toTimeRangeValue(urlToTimeRange(urlTimeRange)) } ================================================ FILE: ui-v2/packages/libs/1-utils/src/url-state/advanced-filters-url-state.ts ================================================ import { useCallback, useMemo } from "react" import { PaginationUrlState } from "./pagination-url-state" import { useUrlState } from "./use-url-state" export type AdvancedFilterItem = { name: string operator: string // =, !=, >, >=, <, <=, in, not in values: string[] } export type AdvancedFiltersUrlState = Partial> export function useAdvancedFiltersUrlState() { const [queryParams, setQueryParams] = useUrlState< AdvancedFiltersUrlState & PaginationUrlState >() const advancedFilters = useMemo(() => { const filtersObjArr: AdvancedFilterItem[] = [] if (queryParams.af) { const filtersArr = queryParams.af.split(";") filtersArr.forEach((filter) => { const [filterName, filterOperator, ...filterValues] = filter.split(",") if (filterName && filterOperator) { filtersObjArr.push({ name: decodeURIComponent(filterName), operator: decodeURIComponent(filterOperator), values: filterValues.map((v) => decodeURIComponent(v)), }) } }) } return filtersObjArr }, [queryParams.af]) const setAdvancedFilters = useCallback( (newAdvancedFilters: AdvancedFilterItem[]) => { const afStr = newAdvancedFilters .map( (f) => `${encodeURIComponent(f.name)},${encodeURIComponent( f.operator, )},${f.values.map((v) => encodeURIComponent(v)).join(",")}`, ) .join(";") setQueryParams({ af: afStr, pageIndex: undefined, }) }, [setQueryParams], ) return { advancedFilters, setAdvancedFilters, } } ================================================ FILE: ui-v2/packages/libs/1-utils/src/url-state/index.ts ================================================ export * from "./use-url-state" export * from "./pagination-url-state" export * from "./sort-url-state" export * from "./timerange-url-state" export * from "./search-url-state" export * from "./advanced-filters-url-state" export * from "./pro-table-sort-state" export * from "./pro-table-pagination-state" ================================================ FILE: ui-v2/packages/libs/1-utils/src/url-state/pagination-url-state.ts ================================================ import { useCallback, useMemo } from "react" import { useUrlState } from "./use-url-state" export type Pagination = { pageIndex: number pageSize: number } export type PaginationUrlState = Partial< Record<"pageIndex" | "pageSize", string> > export function usePaginationUrlState(defPageSize: number = 15) { const [queryParams, setQueryParams] = useUrlState() const pagination = useMemo(() => { return { pageIndex: Number(queryParams.pageIndex) || 0, pageSize: Number(queryParams.pageSize) || defPageSize, } }, [queryParams.pageIndex, queryParams.pageSize, defPageSize]) const setPagination = useCallback( (newPagination: Pagination) => { setQueryParams({ pageIndex: newPagination.pageIndex === 0 ? undefined : newPagination.pageIndex.toString(), pageSize: newPagination.pageSize === defPageSize ? undefined : newPagination.pageSize.toString(), }) }, [setQueryParams], ) return { pagination, setPagination } } ================================================ FILE: ui-v2/packages/libs/1-utils/src/url-state/pro-table-pagination-state.ts ================================================ import { MRT_PaginationState, ProTableOptions } from "@tidbcloud/uikit/biz" import { useCallback } from "react" import { Pagination } from "./pagination-url-state" type onPaginationChangeFn = Required["onPaginationChange"] export function useProTablePaginationState( pagination: Pagination, setPagination: (v: Pagination) => void, ): { paginationState: MRT_PaginationState setPaginationState: onPaginationChangeFn } { const paginationState = pagination const setPaginationState = useCallback( (updater) => { const newPagination = typeof updater === "function" ? updater(paginationState) : updater setPagination(newPagination) }, [setPagination, paginationState.pageIndex, paginationState.pageSize], ) return { paginationState, setPaginationState } } ================================================ FILE: ui-v2/packages/libs/1-utils/src/url-state/pro-table-sort-state.ts ================================================ import { MRT_SortingState, ProTableOptions } from "@tidbcloud/uikit/biz" import { useCallback, useMemo } from "react" import { SortRule } from "./sort-url-state" type onSortChangeFn = Required["onSortingChange"] export function useProTableSortState( sortRule: SortRule, setSortRule: (v: SortRule) => void, ): { sortingState: MRT_SortingState setSortingState: onSortChangeFn } { const sortingState = useMemo(() => { if (sortRule.orderBy) { return [{ id: sortRule.orderBy, desc: sortRule.desc }] } return [] }, [sortRule.orderBy, sortRule.desc]) const setSortingState = useCallback( (updater) => { const newSort = typeof updater === "function" ? updater(sortingState) : updater if (newSort === sortingState) { return } if (newSort.length > 0) { setSortRule({ orderBy: newSort[0].id, desc: newSort[0].desc }) } else { setSortRule({ orderBy: "", desc: true }) } }, [setSortRule, sortingState], ) return { sortingState, setSortingState } } ================================================ FILE: ui-v2/packages/libs/1-utils/src/url-state/search-url-state.ts ================================================ import { useCallback } from "react" import { PaginationUrlState } from "./pagination-url-state" import { useUrlState } from "./use-url-state" export type SearchUrlState = Partial> export function useSearchUrlState() { const [queryParams, setQueryParams] = useUrlState< SearchUrlState & PaginationUrlState >() const term = decodeURIComponent(queryParams.term ?? "") const setTerm = useCallback( (v?: string) => { setQueryParams({ term: v ? encodeURIComponent(v) : v, pageIndex: undefined, }) }, [setQueryParams], ) return { term, setTerm } } ================================================ FILE: ui-v2/packages/libs/1-utils/src/url-state/sort-url-state.ts ================================================ import { useCallback, useMemo } from "react" import { PaginationUrlState } from "./pagination-url-state" import { useUrlState } from "./use-url-state" export type SortRule = { orderBy: string desc: boolean } export type SortUrlState = Partial> export function useSortUrlState(defOrderBy: string = "") { const [queryParams, setQueryParams] = useUrlState< SortUrlState & PaginationUrlState >() const sortRule = useMemo(() => { return { orderBy: queryParams.orderBy ?? defOrderBy, desc: queryParams.desc !== "false", } }, [queryParams.orderBy, queryParams.desc, defOrderBy]) const setSortRule = useCallback( (newSortRule: SortRule) => { setQueryParams({ orderBy: newSortRule.orderBy || undefined, desc: newSortRule.desc ? undefined : "false", pageIndex: undefined, }) }, [setQueryParams], ) return { sortRule, setSortRule } } ================================================ FILE: ui-v2/packages/libs/1-utils/src/url-state/timerange-url-state.ts ================================================ import { useCallback, useMemo } from "react" import { TimeRange, toURLTimeRange, urlToTimeRange } from "../time-range" import { PaginationUrlState } from "./pagination-url-state" import { useUrlState } from "./use-url-state" export type TimeRangeUrlState = Partial> const DEF_TIME_RANGE: TimeRange = { type: "relative", value: 30 * 60, } export function useTimeRangeUrlState(defTimeRange?: TimeRange) { const [queryParams, setQueryParams] = useUrlState< TimeRangeUrlState & PaginationUrlState >() const timeRange = useMemo(() => { const { from, to } = queryParams if (from && to) { return urlToTimeRange({ from, to }) } return defTimeRange || DEF_TIME_RANGE }, [queryParams.from, queryParams.to, defTimeRange]) const setTimeRange = useCallback( (newTimeRange: TimeRange) => { setQueryParams({ ...toURLTimeRange(newTimeRange), pageIndex: undefined, }) }, [setQueryParams], ) return { timeRange, setTimeRange } } ================================================ FILE: ui-v2/packages/libs/1-utils/src/url-state/use-url-state.tsx ================================================ import { createContext, useCallback, useContext, useMemo, useState, } from "react" type UrlStateCtxValue = { urlQuery: string setUrlQuery: (v: string) => void } const UrlStateContext = createContext(null) const useUrlStateContext = () => { const context = useContext(UrlStateContext) if (!context) { throw new Error("useUrlStateContext must be used within a UrlStateProvider") } return context } function defCtxVal(): UrlStateCtxValue { return { urlQuery: new URL(window.location.href).search, setUrlQuery(p) { const url = new URL(window.location.href) window.history.replaceState({}, "", `${url.pathname}?${p}`) }, } } export function UrlStateProvider(props: { children: React.ReactNode value?: UrlStateCtxValue }) { const val: UrlStateCtxValue = props.value || defCtxVal() const [urlQuery, _setUrlQuery] = useState(val.urlQuery) // UrlStateProvider is designed to each page has its own provider instance, // won't share between pages // so we don't need to sync urlQuery from props // ------------------- // sync urlQuery from props changes // useEffect(() => { // _setUrlQuery(val.urlQuery) // }, [val.urlQuery]) const ctxValue = useMemo( () => ({ urlQuery, setUrlQuery: (v: string) => { val.setUrlQuery(v) // trigger re-render _setUrlQuery(v) }, }), [urlQuery, val], ) return ( {props.children} ) } //---------------------- type UrlState = Partial> type UrlStateObj = { [key in keyof T]: string } export function useUrlState(): [ UrlStateObj, (s: UrlStateObj) => void, ] { const { urlQuery, setUrlQuery } = useUrlStateContext() const queryParams = useMemo(() => { console.log("1 ---- urlQuery:", urlQuery) const searchParams = new URLSearchParams(urlQuery) const paramsObj: Record = {} searchParams.forEach((v, k) => { paramsObj[k] = v }) return paramsObj as UrlStateObj }, [urlQuery]) const setQueryParams = useCallback( (s: UrlStateObj) => { console.log("2 ---- urlQuery:", urlQuery) console.log("3 ---- s:", s) const searchParams = new URLSearchParams(urlQuery) Object.keys(s).forEach((k) => { if (s[k]) { searchParams.set(k, s[k]) } else { searchParams.delete(k) } }) // searchParams.toString() will do encodeURIComponent internally setUrlQuery(searchParams.toString()) }, [setUrlQuery, urlQuery], ) return [queryParams, setQueryParams] as const } ================================================ FILE: ui-v2/packages/libs/1-utils/tsconfig.json ================================================ { "extends": "../../tsconfig.app.json", "compilerOptions": { "outDir": "./dist" }, "include": ["./src"] } ================================================ FILE: ui-v2/packages/libs/2-primitive-ui/CHANGELOG.md ================================================ # @pingcap-incubator/tidb-dashboard-lib-primitive-ui ## 0.14.0 ### Minor Changes - bump version ## 0.13.0 ### Minor Changes - refactor: re-exports lib-uitls/lib-charts/lib-primitive-ui/lib-biz-ui from lib-apps ## 0.12.0 ### Minor Changes - upgrade uikit, refine table empty status, refine chart empty legend name case ## 0.11.0 ### Minor Changes - fix time-range-picker, refine cols-multi-select ## 0.10.0 ### Minor Changes - i18n for slow query and statement app ## 0.9.0 ### Minor Changes - upgrade uikit ## 0.8.0 ### Minor Changes - support advanced filters for diagnosis ## 0.7.0 ### Minor Changes - refine pagination and sort url state ## 0.6.0 ### Minor Changes - refine ## 0.5.0 ### Minor Changes - refine slow-query and statement apps ## 0.4.0 ### Minor Changes - update uikit ## 0.3.0 ### Minor Changes - add azores cluster metrics page ## 0.2.0 ### Minor Changes - add metrics azores host page ## 0.1.0 ### Minor Changes - update i18n, format utils ## 0.0.7 ### Patch Changes - add i18n ## 0.0.6 ### Patch Changes - support dark mode for lib-charts ## 0.0.5 ### Patch Changes - refine ## 0.0.4 ### Patch Changes - refactor metric ## 0.0.3 ### Patch Changes - upgrade uikit ## 0.0.2 ### Patch Changes - first release ================================================ FILE: ui-v2/packages/libs/2-primitive-ui/package.json ================================================ { "name": "@pingcap-incubator/tidb-dashboard-lib-primitive-ui", "version": "0.14.0", "description": "", "type": "module", "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist", "README.md", "CHANGELOG.md" ], "scripts": { "tsc:watch": "tsc --watch", "rollup:watch": "rollup -c --watch", "dev": "concurrently --kill-others \"pnpm tsc:watch\" \"pnpm rollup:watch\"", "build": "tsc && rollup -c" }, "keywords": [], "author": "", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^12.1.1", "@tidbcloud/uikit": "catalog:", "@types/react": "^18.3.12", "react": "^18.3.1", "rollup": "^4.24.0", "tslib": "^2.8.0" }, "peerDependencies": { "@tidbcloud/uikit": "catalog:", "react": "^18.3.1" } } ================================================ FILE: ui-v2/packages/libs/2-primitive-ui/rollup.config.js ================================================ import typescript from "@rollup/plugin-typescript" export default { input: "src/index.ts", output: { dir: "dist", format: "es", }, plugins: [typescript()], } ================================================ FILE: ui-v2/packages/libs/2-primitive-ui/src/index.ts ================================================ // decided not to export // export * from "@tidbcloud/uikit" export * from "./uikit-theme-provider" ================================================ FILE: ui-v2/packages/libs/2-primitive-ui/src/uikit-theme-provider.tsx ================================================ import { ColorScheme, useColorScheme } from "@tidbcloud/uikit" import { useHotkeys } from "@tidbcloud/uikit/hooks" import { ThemeProvider } from "@tidbcloud/uikit/theme" export const UiKitThemeProvider = ({ children, }: React.PropsWithChildren) => { const { colorScheme, setColorScheme } = useColorScheme("auto", { getInitialValueInEffect: false, key: "mantine-color-scheme", }) const toggleColorScheme = (value?: ColorScheme) => { setColorScheme(value || (colorScheme === "dark" ? "light" : "dark")) } useHotkeys([["mod+J", () => toggleColorScheme()]]) return ( {children} ) } ================================================ FILE: ui-v2/packages/libs/2-primitive-ui/tsconfig.json ================================================ { "extends": "../../tsconfig.app.json", "compilerOptions": { "outDir": "./dist" }, "include": ["./src"] } ================================================ FILE: ui-v2/packages/libs/3-biz-ui/CHANGELOG.md ================================================ # @pingcap-incubator/tidb-dashboard-lib-biz-ui ## 0.17.0 ### Minor Changes - bump version ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.15.0 ## 0.16.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.14.0 ## 0.16.0 ### Minor Changes - refactor: re-exports lib-uitls/lib-charts/lib-primitive-ui/lib-biz-ui from lib-apps ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.13.0 ## 0.15.1 ### Patch Changes - support now-to-future type relative time range - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.12.1 ## 0.15.0 ### Minor Changes - upgrade uikit, refine table empty status, refine chart empty legend name case ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.12.0 ## 0.14.3 ### Patch Changes - adjust monitoring panel names ## 0.14.2 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.11.2 ## 0.14.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.11.1 ## 0.14.0 ### Minor Changes - refine ## 0.13.2 ### Patch Changes - enable plan-table columns resize ## 0.13.1 ### Patch Changes - update json view style ## 0.13.0 ### Minor Changes - update metrics charts ## 0.12.0 ### Minor Changes - refine plan table and fix highlight-sql components ## 0.11.3 ### Patch Changes - refine advanced fitlers ## 0.11.2 ### Patch Changes - fix NumberInput cannot input 0 bug ## 0.11.1 ### Patch Changes - fix cols-multi-select style ## 0.11.0 ### Minor Changes - fix time-range-picker, refine cols-multi-select ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.11.0 ## 0.10.0 ### Minor Changes - i18n for slow query and statement app ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.10.0 ## 0.9.1 ### Patch Changes - refine advanced filters component ## 0.9.0 ### Minor Changes - upgrade uikit ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.9.0 ## 0.8.3 ### Patch Changes - show raw json info for slow query and statement detail ## 0.8.2 ### Patch Changes - add columns select, time range alert ## 0.8.1 ### Patch Changes - renaming fields - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.8.1 ## 0.8.0 ### Minor Changes - support advanced filters for diagnosis ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.8.0 ## 0.7.0 ### Minor Changes - refine pagination and sort url state ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.7.0 ## 0.6.1 ### Patch Changes - refine ## 0.6.0 ### Minor Changes - refine ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.6.0 ## 0.5.0 ### Minor Changes - refine slow-query and statement apps ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.5.0 ## 0.4.0 ### Minor Changes - update uikit ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.4.0 ## 0.3.0 ### Minor Changes - add azores cluster metrics page ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.3.0 ## 0.2.0 ### Minor Changes - add metrics azores host page ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.2.0 ## 0.1.0 ### Minor Changes - update i18n, format utils ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.1.0 ## 0.0.10 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.10 ## 0.0.9 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.9 ## 0.0.8 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.8 ## 0.0.7 ### Patch Changes - add i18n - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.7 ## 0.0.6 ### Patch Changes - support dark mode for lib-charts - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.6 ## 0.0.5 ### Patch Changes - refine - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.5 ## 0.0.4 ### Patch Changes - refactor metric - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.4 ## 0.0.3 ### Patch Changes - upgrade uikit - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.3 ## 0.0.2 ### Patch Changes - first release - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.2 ================================================ FILE: ui-v2/packages/libs/3-biz-ui/package.json ================================================ { "name": "@pingcap-incubator/tidb-dashboard-lib-biz-ui", "version": "0.17.0", "description": "", "type": "module", "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist", "README.md", "CHANGELOG.md" ], "scripts": { "tsc:watch": "tsc --watch", "rollup:watch": "rollup -c --watch", "dev": "concurrently --kill-others \"pnpm tsc:watch\" \"pnpm rollup:watch\"", "build": "tsc && rollup -c" }, "keywords": [], "author": "", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^12.1.1", "@tidbcloud/uikit": "catalog:", "@types/react": "^18.3.12", "react": "^18.3.1", "rollup": "^4.24.0", "tslib": "^2.8.0" }, "peerDependencies": { "@tidbcloud/uikit": "catalog:", "react": "^18.3.1" }, "dependencies": { "@pingcap-incubator/tidb-dashboard-lib-utils": "workspace:^", "ahooks": "^3.8.1", "react-json-view-lite": "^2.0.1" } } ================================================ FILE: ui-v2/packages/libs/3-biz-ui/rollup.config.js ================================================ import typescript from "@rollup/plugin-typescript" export default { input: "src/index.ts", output: { dir: "dist", format: "es", }, plugins: [typescript()], } ================================================ FILE: ui-v2/packages/libs/3-biz-ui/src/action-drawer.tsx ================================================ import { Box, BoxProps, Drawer, DrawerProps, Group, GroupProps, } from "@tidbcloud/uikit" export const ActionDrawer = (props: DrawerProps) => { return ( ({ content: { display: "flex", flexDirection: "column", transitionProperty: "flex-basis, transform, opacity !important", }, header: { paddingLeft: 24, backgroundColor: theme.colors.carbon[0], borderBottom: `1px solid ${theme.colors.carbon[4]}`, }, title: { fontWeight: 700, fontSize: 16, lineHeight: 1.5, color: theme.colors.carbon[9], }, body: { flex: 1, display: "flex", flexDirection: "column", padding: 0, overflowY: "hidden", }, })} {...props} /> ) } const ActionDrawerBody = (props: React.PropsWithChildren) => { return } const ActionDrawerFooter = (props: GroupProps) => { return ( ({ borderTop: `1px solid ${theme.colors.carbon[4]}`, backgroundColor: theme.colors.carbon[0], position: "sticky", bottom: 0, })} {...props} /> ) } ActionDrawer.Body = ActionDrawerBody ActionDrawer.Footer = ActionDrawerFooter ================================================ FILE: ui-v2/packages/libs/3-biz-ui/src/advanced-filters/filter-setting.tsx ================================================ import { AdvancedFilterItem, useTn, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { ActionIcon, Box, Group, NumberInput, Select, TextInput, Typography, } from "@tidbcloud/uikit" import { IconTrash01 } from "@tidbcloud/uikit/icons" import { useEffect } from "react" import { FilterMultiSelect } from "../filter-multi-select" // AdvancedFilterItem represent the filter from url export type AdvancedFilterSettingItem = AdvancedFilterItem & { createdAt: number deleted: boolean } // AdvancedFilterInfo represent the info from backend export type AdvancedFilterInfo = { name: string type: string // string | number | bool unit: string values: string[] } export function AdvancedFilterSetting({ availableFilters, filtersInfo, onReqFilterInfo, filter, onUpdate, showDelete = true, conditionLabel = "AND", }: { availableFilters: Array filtersInfo?: AdvancedFilterInfo[] onReqFilterInfo?: (filterName: string) => void filter: AdvancedFilterSettingItem onUpdate?: (item: AdvancedFilterSettingItem) => void showDelete?: boolean conditionLabel?: string }) { const { tt } = useTn("advanced-filters") const filterInfo = filtersInfo?.find((f) => f.name === filter.name) useEffect(() => { if (filter.name) { const fInfo = filtersInfo?.find((f) => f.name === filter.name) if (!fInfo) { onReqFilterInfo?.(filter.name) } } }, [filter.name]) return ( {conditionLabel} onUpdate?.({ ...filter, operator: v || "in" })} /> )} {filterInfo && filterInfo.values.length === 0 && ( {}} error={beyondMin || startAfterEnd || beyondDuration} /> updateTime(d.currentTarget.value, "start")} error={beyondMin || startAfterEnd || beyondDuration} /> {tt("End")} {}} error={beyondMax || startAfterEnd || beyondDuration} /> updateTime(d.currentTarget.value, "end")} error={beyondMax || startAfterEnd || beyondDuration} /> {(startAfterEnd || beyondMin || beyondMax || beyondDuration) && ( } color="red" pt={8}> {startAfterEnd && ( {tt("Please select an end time after the start time.")} )} {beyondMin && ( {tt("Please select a start time after {{time}}.", { time: formatTime(minDateTime!, "MMM D, YYYY HH:mm:ss"), })} )} {beyondMax && ( {tt("Please select an end time before {{time}}.", { time: formatTime(maxDateTime!, "MMM D, YYYY HH:mm:ss"), })} )} {beyondDuration && ( {tt( "The selection exceeds the {{duration}} limit, please select a shorter time range.", { duration: formatDuration(maxDuration!) }, )} )} )} ) } export default CustomTimeRangePicker ================================================ FILE: ui-v2/packages/libs/3-biz-ui/src/time-range-picker/index.tsx ================================================ import { RelativeTimeRange, TimeRange, addLangsLocales, formatDuration, formatTime, toTimeRangeValue, useTn, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Box, Button, ButtonProps, Group, Menu, Text, Tooltip, Typography, } from "@tidbcloud/uikit" import { IconChevronRight } from "@tidbcloud/uikit/icons" import { useMemo, useState } from "react" import CustomTimeRangePicker from "./custom" const DEFAULT_QUICK_RANGES = [ 5 * 60, 15 * 60, 30 * 60, 60 * 60, 3 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60, 2 * 24 * 60 * 60, 3 * 24 * 60 * 60, ] export interface TimeRangePickerProps extends ButtonProps { value: TimeRange onChange?: (value: TimeRange) => void loading?: boolean minDateTime?: () => Date maxDateTime?: () => Date maxDuration?: number // unit: seconds // quick range selection items, Last x mins, Last x hours... // unit: seconds. quickRanges?: number[] quickRangesType?: RelativeTimeRange["type"] disableAbsoluteRanges?: boolean } export const TimeRangePicker = ({ value, minDateTime, maxDateTime, maxDuration, disableAbsoluteRanges = false, onChange, quickRanges = DEFAULT_QUICK_RANGES, quickRangesType = "relative", loading, sx, }: React.PropsWithChildren) => { const { tt } = useTn("time-range-picker") const [opened, setOpened] = useState(false) const [customMode, setCustomMode] = useState(false) const isRelativeRange = value.type !== "absolute" const relativeTimePrefix = value.type === "now-to-future" ? tt("Next") : tt("Past") const timeRangeValue = toTimeRangeValue(value) const duration = timeRangeValue[1] - timeRangeValue[0] const selectedRelativeItem = useMemo(() => { if (value.type === "absolute") { return } return quickRanges.find( (it) => it === value.value && value.type === quickRangesType, ) }, [quickRanges, value, quickRangesType]) const formattedAbsDateTime = useMemo(() => { return `${formatTime(timeRangeValue[0] * 1000, "MMM D, YYYY HH:mm")} - ${formatTime( timeRangeValue[1] * 1000, "MMM D, YYYY HH:mm", )}` }, [timeRangeValue]) return ( { setOpened(true) setCustomMode(false) }} onClose={() => setOpened(false)} > {customMode ? ( { onChange?.(v) setOpened(false) }} onCancel={() => setOpened(false)} onReturnClick={() => setCustomMode(false)} /> ) : ( <> {!disableAbsoluteRanges && ( <> } closeMenuOnClick={false} onClick={() => setCustomMode(true)} > {tt("Custom")} )} <> {quickRanges.map((seconds) => ( ({ background: seconds === selectedRelativeItem ? theme.colors.carbon[3] : "", })} onClick={() => onChange?.({ type: quickRangesType, value: seconds }) } > {relativeTimePrefix} {formatDuration(seconds)} ))} )} ) } const DurationBadge = ({ children }: { children: React.ReactNode }) => { return ( {children} ) } //------------------------ // i18n // auto updated by running `pnpm gen:locales` const I18nNamespace = "time-range-picker" type I18nLocaleKeys = | "Apply" | "Back" | "Cancel" | "Custom" | "End" | "Next" | "Past" | "Please select a start time after {{time}}." | "Please select an end time after the start time." | "Please select an end time before {{time}}." | "Start" | "The selection exceeds the {{duration}} limit, please select a shorter time range." type I18nLocale = { [K in I18nLocaleKeys]?: string } const en: I18nLocale = {} const zh: I18nLocale = { Apply: "应用", Back: "返回", Cancel: "取消", Custom: "自定义", End: "结束", Next: "未来", Past: "过去", "Please select a start time after {{time}}.": "请选择 {{time}} 之后的开始时间。", "Please select an end time after the start time.": "请确保结束时间在开始时间之后。", "Please select an end time before {{time}}.": "请选择 {{time}} 之前的结束时间。", Start: "开始", "The selection exceeds the {{duration}} limit, please select a shorter time range.": "选择超出了 {{duration}} 的限制,请选择更短的时间范围。", } function updateI18nLocales(locales: { [ln: string]: I18nLocale }) { for (const [ln, locale] of Object.entries(locales)) { addLangsLocales({ [ln]: { __namespace__: I18nNamespace, ...locale, }, }) } } updateI18nLocales({ en, zh }) TimeRangePicker.i18nNamespace = I18nNamespace TimeRangePicker.updateI18nLocales = updateI18nLocales ================================================ FILE: ui-v2/packages/libs/3-biz-ui/tsconfig.json ================================================ { "extends": "../../tsconfig.app.json", "compilerOptions": { "outDir": "./dist" }, "include": ["./src"] } ================================================ FILE: ui-v2/packages/libs/4-apps/CHANGELOG.md ================================================ # @pingcap-incubator/tidb-dashboard-lib-apps ## 0.20.2 ### Patch Changes - fix diagnosis url state ## 0.20.1 ### Patch Changes - update monitoring ui ## 0.20.0 ### Minor Changes - bump version ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.16.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.14.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.15.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.14.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.17.0 ## 0.19.7 ### Patch Changes - fix get prom addr when metric data is empty ## 0.19.2 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.14.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.16.1 ## 0.19.1 ### Patch Changes - temporary disable sql-statement setting entry ## 0.19.0 ### Minor Changes - refactor: re-exports lib-uitls/lib-charts/lib-primitive-ui/lib-biz-ui from lib-apps ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.15.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.13.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.13.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.13.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.16.0 ## 0.18.2 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.14.1 ## 0.18.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.12.1 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.15.1 ## 0.18.0 ### Minor Changes - upgrade uikit, refine table empty status, refine chart empty legend name case ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.14.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.12.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.12.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.12.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.15.0 ## 0.17.6 ### Patch Changes - show prometheus address for monitoring chart ## 0.17.5 ### Patch Changes - add troubleshooting links and prom addr for charts ## 0.17.4 ### Patch Changes - adjust monitoring panel names - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.14.3 ## 0.17.3 ### Patch Changes - refine chart same as grafana - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.11.2 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.14.2 ## 0.17.2 ### Patch Changes - refine chart step - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.11.1 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.14.1 ## 0.17.1 ### Patch Changes - refine related statement button ## 0.17.0 ### Minor Changes - refine ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.14.0 ## 0.16.2 ### Patch Changes - address feedback ## 0.16.1 ### Patch Changes - add statement setting ## 0.16.0 ### Minor Changes - add sql-history feature ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.13.2 ## 0.15.1 ### Patch Changes - fix i18n issue - Updated dependencies - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.13.1 - @pingcap-incubator/tidb-dashboard-lib-charts@0.13.1 ## 0.15.0 ### Minor Changes - update sql-limit ux ## 0.14.0 ### Minor Changes - add drill down drawer for metrics ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.13.0 ## 0.13.0 ### Minor Changes - update metrics charts ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.12.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.13.0 ## 0.12.0 ### Minor Changes - fix related slow queries link for statement detail page ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.12.0 ## 0.11.4 ### Patch Changes - 1. add refresh button for monitoring single chart 2. add tip for open detail page in new tab for slow-query and statement 3. highlight selected slow-query and statement row in list table for slow-query and statement - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.11.3 ## 0.11.3 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.11.2 ## 0.11.2 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.11.1 ## 0.11.1 ### Patch Changes - refine monitoring ## 0.11.0 ### Minor Changes - fix time-range-picker, refine cols-multi-select ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.11.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.11.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.11.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.11.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.11.0 ## 0.10.0 ### Minor Changes - i18n for slow query and statement app ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.10.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.10.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.10.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.10.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.10.0 ## 0.9.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.9.1 ## 0.9.0 ### Minor Changes - upgrade uikit ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.9.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.9.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.9.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.9.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.9.0 ## 0.8.5 ### Patch Changes - show raw json info for slow query and statement detail - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.8.3 ## 0.8.4 ### Patch Changes - support open statement detail page from slow query detail page ## 0.8.3 ### Patch Changes - refine plans list ## 0.8.2 ### Patch Changes - add columns select, time range alert - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.8.2 ## 0.8.1 ### Patch Changes - renaming fields - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.8.1 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.8.1 ## 0.8.0 ### Minor Changes - support advanced filters for diagnosis ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.8.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.8.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.8.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.8.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.8.0 ## 0.7.1 ### Patch Changes - add showDetailBack config for diagnosis apps ## 0.7.0 ### Minor Changes - refine pagination and sort url state ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.7.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.7.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.7.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.7.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.7.0 ## 0.6.1 ### Patch Changes - refine - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.6.1 ## 0.6.0 ### Minor Changes - refine ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.6.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.6.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.6.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.6.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.6.0 ## 0.5.4 ### Patch Changes - add sql limit for slow query ## 0.5.3 ### Patch Changes - add sql limit feature ## 0.5.2 ### Patch Changes - add sql plan bind feature ## 0.5.1 ### Patch Changes - refine slow-query and statement detail page back button ## 0.5.0 ### Minor Changes - refine slow-query and statement apps ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.5.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.5.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.5.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.5.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.5.0 ## 0.4.2 ### Patch Changes - make metric config kind as union type ## 0.4.1 ### Patch Changes - add drill down modal ## 0.4.0 ### Minor Changes - update uikit ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.4.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.4.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.4.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.4.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.4.0 ## 0.3.3 ### Patch Changes - fix build ## 0.3.2 ### Patch Changes - update wording ## 0.3.1 ### Patch Changes - update i18n ## 0.3.0 ### Minor Changes - add azores cluster metrics page ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.3.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.3.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.3.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.3.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.3.0 ## 0.2.1 ### Patch Changes - refine metrics ## 0.2.0 ### Minor Changes - add metrics azores host page ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.2.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.2.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.2.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.2.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.2.0 ## 0.1.0 ### Minor Changes - update i18n, format utils ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.1.0 - @pingcap-incubator/tidb-dashboard-lib-icons@0.1.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.1.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.1.0 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.1.0 ## 0.0.13 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.10 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.0.10 ## 0.0.12 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.9 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.0.9 ## 0.0.11 ### Patch Changes - debug i18n - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.8 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.0.8 ## 0.0.10 ### Patch Changes - add i18n - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.7 - @pingcap-incubator/tidb-dashboard-lib-icons@0.0.7 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.7 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.0.7 - @pingcap-incubator/tidb-dashboard-lib-charts@0.0.9 ## 0.0.9 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.0.8 ## 0.0.8 ### Patch Changes - support dark mode for lib-charts - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.6 - @pingcap-incubator/tidb-dashboard-lib-icons@0.0.6 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.6 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.0.6 - @pingcap-incubator/tidb-dashboard-lib-charts@0.0.7 ## 0.0.7 ### Patch Changes - add loading for panel, remove segement control items border ## 0.0.6 ### Patch Changes - refine - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.5 - @pingcap-incubator/tidb-dashboard-lib-icons@0.0.5 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.5 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.0.5 - @pingcap-incubator/tidb-dashboard-lib-charts@0.0.6 ## 0.0.5 ### Patch Changes - refactor metric - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.4 - @pingcap-incubator/tidb-dashboard-lib-icons@0.0.4 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.4 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.0.4 - @pingcap-incubator/tidb-dashboard-lib-charts@0.0.5 ## 0.0.4 ### Patch Changes - upgrade uikit - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.3 - @pingcap-incubator/tidb-dashboard-lib-icons@0.0.3 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.3 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.0.3 - @pingcap-incubator/tidb-dashboard-lib-charts@0.0.4 ## 0.0.3 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.0.3 ## 0.0.2 ### Patch Changes - first release - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.2 - @pingcap-incubator/tidb-dashboard-lib-icons@0.0.2 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.2 - @pingcap-incubator/tidb-dashboard-lib-biz-ui@0.0.2 - @pingcap-incubator/tidb-dashboard-lib-charts@0.0.2 ================================================ FILE: ui-v2/packages/libs/4-apps/package.json ================================================ { "name": "@pingcap-incubator/tidb-dashboard-lib-apps", "version": "0.20.2", "description": "", "type": "module", "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js" }, "./slow-query": { "import": "./dist/slow-query/index.js" }, "./statement": { "import": "./dist/statement/index.js" }, "./metric": { "import": "./dist/metric/index.js" }, "./utils": { "import": "./dist/_re-exports/utils.js" }, "./charts": { "import": "./dist/_re-exports/charts.js" }, "./charts-css": { "import": "./dist/_re-exports/charts-css.js" }, "./primitive-ui": { "import": "./dist/_re-exports/primitive-ui.js" }, "./biz-ui": { "import": "./dist/_re-exports/biz-ui.js" } }, "typesVersions": { "*": { ".": [ "./dist/index.d.ts" ], "slow-query": [ "./dist/slow-query/index.d.ts" ], "statement": [ "./dist/statement/index.d.ts" ], "metric": [ "./dist/metric/index.d.ts" ], "utils": [ "./dist/_re-exports/utils.d.ts" ], "charts": [ "./dist/_re-exports/charts.d.ts" ], "charts-css": [ "./dist/_re-exports/charts-css.d.ts" ], "primitive-ui": [ "./dist/_re-exports/primitive-ui.d.ts" ], "biz-ui": [ "./dist/_re-exports/biz-ui.d.ts" ] } }, "files": [ "dist", "README.md", "CHANGELOG.md" ], "scripts": { "tsc:watch": "tsc --watch", "rollup:watch": "rollup -c --watch", "dev": "concurrently --kill-others \"pnpm tsc:watch\" \"pnpm rollup:watch\"", "build": "tsc && rollup -c" }, "keywords": [], "author": "", "license": "MIT", "devDependencies": { "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-typescript": "^12.1.1", "@tanstack/react-query": "^5.59.16", "@tidbcloud/uikit": "catalog:", "@types/lodash-es": "^4.17.12", "@types/react": "^18.3.12", "react": "^18.3.1", "rollup": "^4.24.0", "tslib": "^2.8.0", "zustand": "^5.0.2" }, "peerDependencies": { "@tanstack/react-query": "^5.59.16", "@tidbcloud/uikit": "catalog:", "react": "^18.3.1", "zustand": "^5.0.2" }, "dependencies": { "@pingcap-incubator/tidb-dashboard-lib-biz-ui": "workspace:^", "@pingcap-incubator/tidb-dashboard-lib-charts": "workspace:^", "@pingcap-incubator/tidb-dashboard-lib-icons": "workspace:^", "@pingcap-incubator/tidb-dashboard-lib-primitive-ui": "workspace:^", "@pingcap-incubator/tidb-dashboard-lib-utils": "workspace:^", "lodash-es": "^4.17.21" } } ================================================ FILE: ui-v2/packages/libs/4-apps/rollup.config.js ================================================ import typescript from "@rollup/plugin-typescript" import json from "@rollup/plugin-json" export default { input: { index: "src/index.ts", "slow-query/index": "src/slow-query/index.ts", "statement/index": "src/statement/index.ts", "metric/index": "src/metric/index.ts", // _re-exports "_re-exports/utils": "src/_re-exports/utils.ts", "_re-exports/charts": "src/_re-exports/charts.ts", "_re-exports/charts-css": "src/_re-exports/charts-css.ts", "_re-exports/primitive-ui": "src/_re-exports/primitive-ui.ts", "_re-exports/biz-ui": "src/_re-exports/biz-ui.ts", }, output: { dir: "dist", format: "es", }, plugins: [typescript(), json()], } ================================================ FILE: ui-v2/packages/libs/4-apps/src/_re-exports/biz-ui.ts ================================================ export * from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" ================================================ FILE: ui-v2/packages/libs/4-apps/src/_re-exports/charts-css.ts ================================================ import "@pingcap-incubator/tidb-dashboard-lib-charts/dist/style.css" ================================================ FILE: ui-v2/packages/libs/4-apps/src/_re-exports/charts.ts ================================================ export * from "@pingcap-incubator/tidb-dashboard-lib-charts" ================================================ FILE: ui-v2/packages/libs/4-apps/src/_re-exports/primitive-ui.ts ================================================ export * from "@pingcap-incubator/tidb-dashboard-lib-primitive-ui" ================================================ FILE: ui-v2/packages/libs/4-apps/src/_re-exports/utils.ts ================================================ export * from "@pingcap-incubator/tidb-dashboard-lib-utils" ================================================ FILE: ui-v2/packages/libs/4-apps/src/_shared/cols-factory.tsx ================================================ import { formatNumByUnit, formatTime, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Tooltip, Typography } from "@tidbcloud/uikit" import { MRT_ColumnDef, MRT_RowData } from "@tidbcloud/uikit/biz" export class ColConfig { constructor(private col: MRT_ColumnDef) {} getConfig(): MRT_ColumnDef { return this.col } setConfig(config: MRT_ColumnDef) { this.col = config return this } patchConfig(config: Partial>) { this.col = { ...this.col, ...config } return this } } export class TableColsFactory { constructor(private tk: (key: string) => string) {} columns(colConfigs: ColConfig[]): MRT_ColumnDef[] { return colConfigs.map((c) => c.getConfig()) } defCol(filedName: keyof T): ColConfig { return new ColConfig({ id: String(filedName), header: this.tk(`fields.${String(filedName)}`), enableSorting: true, enableResizing: false, accessorFn: (row) => row[filedName], }) } number(filedName: keyof T, unit: string): ColConfig { return this.defCol(filedName).patchConfig({ accessorFn: (row) => formatNumByUnit(row[filedName]!, unit) || "-", }) } timestamp(filedName: keyof T): ColConfig { return this.defCol(filedName).patchConfig({ accessorFn: (row) => formatTime(row[filedName]! * 1000), }) } text(filedName: keyof T): ColConfig { return this.defCol(filedName).patchConfig({ enableSorting: false, enableResizing: true, accessorFn: (row) => ( {row[filedName] || "-"} ), }) } textWithTooltip(filedName: keyof T): ColConfig { return this.defCol(filedName).patchConfig({ enableSorting: false, enableResizing: true, accessorFn: (row) => ( {row[filedName] || "-"} ), }) } } ================================================ FILE: ui-v2/packages/libs/4-apps/src/_shared/sql-history/components/chart.tsx ================================================ import { SeriesChart, SeriesData, } from "@pingcap-incubator/tidb-dashboard-lib-charts" import { TimeRangeValue, toTimeRangeValue, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Box, Flex, Loader, useComputedColorScheme } from "@tidbcloud/uikit" import { useMemo } from "react" import { useSqlHistoryState } from "../shared-state/memory-state" import { useSqlHistoryMetricData } from "../utils/use-data" export function SqlHistoryChart() { const colorScheme = useComputedColorScheme() const { data: metricData, isLoading } = useSqlHistoryMetricData() const metric = useSqlHistoryState((state) => state.metric) const timeRange = useSqlHistoryState((state) => state.timeRange) const setTimeRange = useSqlHistoryState((state) => state.setTimeRange) const tr = useMemo( () => (timeRange ? toTimeRangeValue(timeRange) : [0, 0]), [timeRange], ) const seriesData = useMemo(() => { if (!metricData) return [] return [ { id: metric?.name || "", name: metric?.name || "", data: metricData || [], }, ] }, [metricData, metric]) function handleTimeRangeChange(v: TimeRangeValue) { setTimeRange({ type: "absolute", value: v }) } return ( {seriesData.length > 0 || !isLoading ? ( ) : ( )} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/_shared/sql-history/components/filters.tsx ================================================ import { TimeRangePicker } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Group, Select } from "@tidbcloud/uikit" import { dayjs } from "@tidbcloud/uikit/utils" import { useEffect, useMemo } from "react" import { useAppContext } from "../ctx" import { useSqlHistoryState } from "../shared-state/memory-state" import { useSqlHistoryMetricNamesData } from "../utils/use-data" const QUICK_RANGES: number[] = [ 5 * 60, // 5 mins 15 * 60, 30 * 60, 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60, 2 * 24 * 60 * 60, 3 * 24 * 60 * 60, // 3 days 7 * 24 * 60 * 60, // 7 days ] function MetricSelect() { const ctx = useAppContext() const { t } = useTn("sql-history") const { data: metrics } = useSqlHistoryMetricNamesData() const metric = useSqlHistoryState((state) => state.metric) const setMetric = useSqlHistoryState((state) => state.setMetric) useEffect(() => { if (metrics && !metric) { setMetric(metrics[0]) } }, [metrics]) const selectData = useMemo( () => metrics?.map((m) => ({ label: t(`fields.${m.name}`, m.name, { ns: ctx.cfg.parentAppName }), value: m.name, })), [metrics, t], ) function handleMetricChange(v: string | null) { const metric = metrics?.find((m) => m.name === v) setMetric(metric) } return ( setResourceGroup(v || "")} /> { setSelectedInstance(v ? v : undefined) }} /> ) const timeRangePicker = ( { setTimeRange(v) }} quickRanges={QUICK_RANGES} minDateTime={() => dayjs() .subtract(QUICK_RANGES[QUICK_RANGES.length - 1], "seconds") .startOf("d") .toDate() } maxDateTime={() => dayjs().endOf("d").toDate()} /> ) return ( {instanceSelect} {timeRangePicker} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/pages/azores-metric-detail/modal.tsx ================================================ import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Modal } from "@tidbcloud/uikit" import { useChartState } from "../../shared-state/memory-state" import { AzoresMetricDetailBody } from "./body" export function AzoresMetricDetailModal() { const selectedChart = useChartState((state) => state.selectedChart) const reset = useChartState((state) => state.reset) const { tt } = useTn("metric") if (!selectedChart) { return null } return ( ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/pages/azores-overview/index.tsx ================================================ import { Stack } from "@tidbcloud/uikit" import { useMetricQueriesConfigData } from "../../utils/use-data" import { AzoresOverviewMetricsPanel } from "./panel" import { LoadingCard } from "../../components/loading-card" export function AzoresOverviewMetricsPage() { const { data: panelConfigData, isLoading } = useMetricQueriesConfigData("azores-overview") if (isLoading) { return ( ) } return ( {panelConfigData ?.filter((p) => p.charts.length > 0) .map((panel) => { return ( ) })} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/pages/azores-overview/panel.tsx ================================================ import { RelativeTimeRange, useTn, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Box, Card, Group, SegmentedControl, Stack, Tooltip, Typography, } from "@tidbcloud/uikit" import { IconInfoCircle } from "@tidbcloud/uikit/icons" import { useMemo, useState } from "react" import { ChartBody } from "../../components/chart-body" import { ChartHeader } from "../../components/chart-header" import { SinglePanelConfig } from "../../utils/type" import { useMetricConfigData } from "../../utils/use-data" // @ts-expect-error @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars function useLocales() { const { tk } = useTn("metric") // used for gogocode to scan and generate en.json before build tk("panels.instance_top", "Top 5 Node Utilization") tk("panels.host_top", "Top 5 Host Performance") tk("panels.cluster_top", "Top 5 SQL Performance") } export function AzoresOverviewMetricsPanel({ config, }: { config: SinglePanelConfig }) { const { tk, tt } = useTn("metric") const timeRangeOptions = useMemo(() => { return [ { label: tk("time_range.hour", "{{count}} hr", { count: 1 }), value: 60 * 60 + "", }, { label: tk("time_range.hour", "{{count}} hrs", { count: 24 }), value: 24 * 60 * 60 + "", }, { label: tk("time_range.day", "{{count}} days", { count: 7 }), value: 7 * 24 * 60 * 60 + "", }, ] }, [tk]) const [timeRange, setTimeRange] = useState({ type: "relative", value: parseInt(timeRangeOptions[0].value), }) const { data: metricConfigData } = useMetricConfigData() return ( {tk(`panels.${config.category}`)} {!!metricConfigData?.delaySec && ( )} { setTimeRange({ type: "relative", value: parseInt(v) }) }} /> {config.charts.map((c, idx) => ( ))} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/pages/normal/chart-card.tsx ================================================ import { SeriesChart, SeriesData, } from "@pingcap-incubator/tidb-dashboard-lib-charts" import { PromResultItem, TransformNullValue, calcPromQueryStep, toTimeRangeValue, transformPromResultItem, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Box, Card, Flex, Group, Loader, Typography, useComputedColorScheme, } from "@tidbcloud/uikit" import { useEffect, useMemo, useRef } from "react" import { useAppContext } from "../../ctx" import { useMetricsUrlState } from "../../shared-state/url-state" import { SingleChartConfig, SingleQueryConfig } from "../../utils/type" import { useMetricDataByPromQLs } from "../../utils/use-data" export function transformData( items: PromResultItem[], qIdx: number, query: SingleQueryConfig, nullValue?: TransformNullValue, ): SeriesData[] { return items.map((d, dIdx) => ({ ...transformPromResultItem(d, query.legendName, nullValue), id: `${qIdx}-${dIdx}`, type: query.type, color: query.color, // lineSeriesStyle: query.lineSeriesStyle, })) } export function ChartCard({ config }: { config: SingleChartConfig }) { const colorScheme = useComputedColorScheme() const ctx = useAppContext() const { timeRange, refresh } = useMetricsUrlState() const tr = useMemo(() => toTimeRangeValue(timeRange), [timeRange, refresh]) const chartRef = useRef(null) // a function can always get the latest value function getStep() { if (!chartRef.current) return 0 return calcPromQueryStep( tr, chartRef.current.offsetWidth - 140, ctx.cfg.scrapeInterval, ) } const { data: metricData, isLoading, refetch, } = useMetricDataByPromQLs( config.queries.map((q) => q.promql), timeRange, getStep, ) const seriesData = useMemo( () => (metricData ?? []) .map((d, idx) => transformData(d, idx, config.queries[idx], config.nullValue), ) .flat(), [metricData], ) useEffect(() => { refetch() }, [timeRange, refresh]) return ( {config.title} {seriesData.length > 0 || !isLoading ? ( ) : ( )} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/pages/normal/filters.tsx ================================================ import { AutoRefreshButton, AutoRefreshButtonRef, DEFAULT_AUTO_REFRESH_SECONDS, TimeRangePicker, } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { Group, SegmentedControl } from "@tidbcloud/uikit" import { dayjs } from "@tidbcloud/uikit/utils" import { useRef, useState } from "react" import { useMetricsUrlState } from "../../shared-state/url-state" import { QUICK_RANGES } from "../../utils/constants" import { useMetricQueriesConfigData } from "../../utils/use-data" export function Filters() { const { panel, timeRange, setTimeRange, setRefresh, setQueryParams } = useMetricsUrlState() const { data: panelConfigData } = useMetricQueriesConfigData("normal") const tabs = panelConfigData?.map((p) => ({ label: p.displayName, value: p.category, })) const [autoRefreshValue, setAutoRefreshValue] = useState( DEFAULT_AUTO_REFRESH_SECONDS, ) const autoRefreshRef = useRef(null) const [loading, setLoading] = useState(false) function handlePanelChange(newPanel: string) { autoRefreshRef.current?.refresh() setQueryParams({ panel: newPanel || undefined, refresh: new Date().valueOf().toString(), }) } function handleRefresh() { setLoading(true) setTimeout(() => { setLoading(false) }, 1000) setRefresh() } const panelSwitch = tabs && tabs.length > 0 && ( ) const timeRangePicker = ( { setTimeRange(v) }} quickRanges={QUICK_RANGES} minDateTime={() => dayjs() .subtract(QUICK_RANGES[QUICK_RANGES.length - 1], "seconds") .startOf("d") .toDate() } maxDateTime={() => dayjs().endOf("d").toDate()} /> ) const autoRefreshButton = ( ) return ( {panelSwitch} {timeRangePicker} {autoRefreshButton} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/pages/normal/index.tsx ================================================ import { Stack } from "@tidbcloud/uikit" import { Filters } from "./filters" import { Panel } from "./panel" export function NormalMetricsPage() { return ( ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/pages/normal/panel.tsx ================================================ import { LoadingSkeleton } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { SimpleGrid } from "@tidbcloud/uikit" import { useMetricsUrlState } from "../../shared-state/url-state" import { useMetricQueriesConfigData } from "../../utils/use-data" import { ChartCard } from "./chart-card" export function Panel() { const { panel } = useMetricsUrlState() const { data: panelConfigData, isLoading } = useMetricQueriesConfigData("normal") const panelConfig = panelConfigData?.find((p) => p.category === panel) || panelConfigData?.[0] if (isLoading) { return } return ( {panelConfig?.charts.map((c) => )} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/shared-state/memory-state.ts ================================================ import { TimeRange } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { create } from "zustand" import { SingleChartConfig } from "../utils/type" const DEF_TIME_RANGE: TimeRange = { type: "relative", value: 30 * 60 } interface ChartState { selectedChart: SingleChartConfig | undefined setSelectedChart: (newChart: SingleChartConfig | undefined) => void timeRange: TimeRange setTimeRange: (newTimeRange: TimeRange) => void selectedInstance: string | undefined setSelectedInstance: (newValue: string | undefined) => void metricPromAddrs: { [key: string]: string } setMetricPromAddrs: (metricName: string, promAddr: string) => void reset: () => void } export const useChartState = create((set, get) => ({ selectedChart: undefined, setSelectedChart: (newChart) => set({ selectedChart: newChart }), timeRange: DEF_TIME_RANGE, setTimeRange: (newTimeRange) => set({ timeRange: newTimeRange }), selectedInstance: undefined, setSelectedInstance: (newValue) => set({ selectedInstance: newValue }), metricPromAddrs: {}, setMetricPromAddrs: (metricName: string, promAddr: string) => set({ metricPromAddrs: { ...get().metricPromAddrs, [metricName]: promAddr }, }), reset: () => set({ selectedChart: undefined, timeRange: DEF_TIME_RANGE, selectedInstance: undefined, metricPromAddrs: {}, }), })) //--------------------------------- const LOCAL_STORAGE_KEY = "metrics.hide.charts" interface ChartsSelectState { hiddenCharts: string[] setHiddenCharts: (newHiddenCharts: string[]) => void reset: () => void } export const useChartsSelectState = create((set) => ({ hiddenCharts: localStorage.getItem(LOCAL_STORAGE_KEY)?.split(",") || [], setHiddenCharts: (newHiddenCharts) => { localStorage.setItem(LOCAL_STORAGE_KEY, newHiddenCharts.join(",")) set({ hiddenCharts: newHiddenCharts }) }, reset: () => { localStorage.removeItem(LOCAL_STORAGE_KEY) set({ hiddenCharts: [] }) }, })) ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/shared-state/url-state.ts ================================================ import { TimeRangeUrlState, useTimeRangeUrlState, useUrlState, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { useCallback } from "react" type MetricsUrlState = Partial> & TimeRangeUrlState export function useMetricsUrlState() { const [queryParams, setQueryParams] = useUrlState() const { timeRange, setTimeRange } = useTimeRangeUrlState() const panel = queryParams.panel ?? "" const setPanel = useCallback( (newPanel: string) => { setQueryParams({ panel: newPanel || undefined }) }, [setQueryParams], ) const refresh = queryParams.refresh ?? "" const setRefresh = useCallback( (v?: string) => { const now = new Date().valueOf().toString() setQueryParams({ refresh: `${v || ""}${now}` }) }, [setQueryParams], ) return { panel, setPanel, timeRange, setTimeRange, refresh, setRefresh, queryParams, setQueryParams, } } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/utils/common.ts ================================================ import { TimeRange, toTimeRangeValue, } from "@pingcap-incubator/tidb-dashboard-lib-utils" export function fixTimeRange( timeRange: TimeRange, step: number = 30, ): [number, number] { const tr = toTimeRangeValue(timeRange) const end = tr[1] - (tr[1] % step) const begin = tr[0] - (tr[0] % step) return [begin, end] } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/utils/constants.ts ================================================ export const QUICK_RANGES: number[] = [ 5 * 60, // 5 mins 15 * 60, 30 * 60, 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60, 2 * 24 * 60 * 60, 3 * 24 * 60 * 60, // 3 days 7 * 24 * 60 * 60, // 7 days ] ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/utils/type.ts ================================================ import { SeriesDataType } from "@pingcap-incubator/tidb-dashboard-lib-charts" import { TransformNullValue } from "@pingcap-incubator/tidb-dashboard-lib-utils" export type MetricConfigKind = | "normal" | "azores-overview" | "azores-host" | "azores-cluster-overview" | "azores-cluster" export interface SingleQueryConfig { promql: string legendName: string type: SeriesDataType color?: string | ((seriesName: string) => string | undefined) } export interface SingleChartConfig { metricName: string title: string label?: string queries: SingleQueryConfig[] nullValue?: TransformNullValue unit: string } // one group has many categories // one category has many charts // one chart has many queries (aka promqls) export interface SinglePanelConfig { group: string category: string displayName?: string charts: SingleChartConfig[] } ================================================ FILE: ui-v2/packages/libs/4-apps/src/metric/utils/use-data.ts ================================================ import { TimeRange, resolvePromQLTemplate, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { keepPreviousData, useQuery } from "@tanstack/react-query" import { useAppContext } from "../ctx" import { useMetricsUrlState } from "../shared-state/url-state" import { MetricConfigKind } from "../utils/type" import { fixTimeRange } from "./common" export function useMetricQueriesConfigData(kind: MetricConfigKind) { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "metric-queries-config", kind], queryFn: () => ctx.api.getMetricQueriesConfig(kind), }) } export function useMetricConfigData() { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "metric-config"], queryFn: () => ctx.api.getMetricConfig(), }) } export function useMetricLabelValuesData( metricName: string, labelName: string, timeRange: TimeRange, ) { const ctx = useAppContext() return useQuery({ queryKey: [ ctx.ctxId, "metric-label-values", metricName, labelName, timeRange, ], queryFn: () => { const tr = fixTimeRange(timeRange) return ctx.api.getMetricLabelValues({ metricName, labelName, beginTime: tr[0], endTime: tr[1], }) }, enabled: !!metricName, }) } export function useMetricDataByMetricName( metricName: string, timeRange: TimeRange, stepFn: () => number, labelValue?: string, ) { const ctx = useAppContext() return useQuery({ queryKey: [ ctx.ctxId, "metric-data-by-metric-name", metricName, timeRange, // step is not the query key, it is expected labelValue, ], queryFn: () => { const step = stepFn() const tr = fixTimeRange(timeRange, step) return ctx.api .getMetricDataByMetricName({ metricName, beginTime: tr[0], endTime: tr[1], step, label: labelValue, }) .then((d) => ({ metrics: d, tr, promAddr: d[0]?.promAddr })) }, placeholderData: keepPreviousData, // set `enabled: false`, so queryFn can only be manually triggered by calling `refetch()` enabled: false, retry: 1, }) } // @todo: return tr export function useMetricDataByPromQLs( promQLs: string[], timeRange: TimeRange, stepFn: () => number, ) { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "metric-data-by-promqls", promQLs, timeRange], queryFn: () => { const step = stepFn() const tr = fixTimeRange(timeRange, step) return Promise.all( promQLs.map((pq) => ctx.api.getMetricDataByPromQL({ promQL: resolvePromQLTemplate(pq, step, ctx.cfg.scrapeInterval), beginTime: tr[0], endTime: tr[1], step, }), ), ) }, placeholderData: keepPreviousData, // set `enabled: false`, so queryFn can only be manually triggered by calling `refetch()` enabled: false, }) } //--------------------------- export function useCurPanelConfigsData() { const { panel } = useMetricsUrlState() const { data: panelConfigData, isLoading } = useMetricQueriesConfigData("azores-cluster") const filteredPanelConfigData = panelConfigData?.filter( (p) => p.group === (panel || "basic"), ) return { panelConfigData: filteredPanelConfigData, isLoading, } } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/ctx/index.tsx ================================================ import { AdvancedFilterItem } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { createContext, useContext } from "react" import { AppApi as SqlHistoryAppApi } from "../../_shared/sql-history" import { AppApi as SqlLimitAppApi } from "../../_shared/sql-limit" import { AdvancedFilterInfoModel, SlowqueryModel } from "../models" //////////////////////////////// type AppApi = SqlLimitAppApi & SqlHistoryAppApi & { // filters getDbs(): Promise // advanced filters getAdvancedFilterNames(): Promise getAdvancedFilterInfo(params: { name: string }): Promise // available fields getAvailableFields(): Promise // list & detail getSlowQueries(params: { beginTime: number endTime: number dbs: string[] ruGroups: string[] sqlDigest: string term: string limit: number advancedFilters: AdvancedFilterItem[] fields: string[] orderBy: string desc: boolean pageIndex: number pageSize: number }): Promise<{ total: number; items: SlowqueryModel[] }> getSlowQuery(params: { id: string }): Promise } type AppConfig = { title?: string // whether to show back to list page button in the detail page // if set to false, the back button will be hidden // and you need to handle the back action outside of the app by yourself // default is true showDetailBack?: boolean } type AppActions = { openDetail(id: string, newTab: boolean): void backToList(): void openStatement(id: string): void } export type AppCtxValue = { // we use ctxId to be a part of queryKey for react-query, // to differ same requests from different clusters, so this value can be clusterId, or other unique value ctxId: string api: AppApi cfg: AppConfig actions: AppActions } export const AppContext = createContext(null) export const useAppContext = () => { const context = useContext(AppContext) if (!context) { throw new Error("SlowQuery AppContext must be used within a provider") } return context } //////////////////////////////// export function AppProvider(props: { children: React.ReactNode ctxValue: AppCtxValue }) { return ( {props.children} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/index.ts ================================================ export * from "./ctx" export * from "./pages/list" export * from "./pages/detail" // i18n import "./locales" ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/locales/en.json ================================================ { "__namespace__": "slow-query", "__comment__": "this file can be updated by running `pnpm gen:locales` command", "fields.backoff_detail": "Backoff Detail", "fields.backoff_time": "Execution Backoff Time", "fields.backoff_time.desc": "The total backoff waiting time before retry when a query encounters errors (note: there may be multiple backoffs in parallel so that this may not be a wall time)", "fields.backoff_total": "Backoff Total", "fields.backoff_types": "Backoff Types", "fields.binary_plan": "Binary Plan", "fields.commit_backoff_time": "Commit Backoff Time", "fields.commit_backoff_time.desc": "The total backoff waiting time when 2PC commit encounters errors (note: there may be multiple backoffs in parallel so that this may not be a wall time)", "fields.commit_time": "Commit Time", "fields.commit_time.desc": "Time consumed in 2PC commit phase when transaction commits", "fields.compile_time": "Generate Plan Time", "fields.compile_time.desc": "Time consumed when generating the plan", "fields.connection_id": "Connection ID", "fields.connection_id.desc": "Unique connection ID of the query", "fields.cop_proc_addr": "Copr Address (Process)", "fields.cop_proc_addr.desc": "The address of the {{distro.tikv}} that takes most time process the Coprocessor request", "fields.cop_proc_avg": "Mean Cop Proc", "fields.cop_proc_max": "Max Cop Proc", "fields.cop_proc_p90": "P90 Cop Proc", "fields.cop_time": "Coprocessor Executor Time", "fields.cop_time.desc": "The elapsed wall time when {{distro.tidb}} Coprocessor executor waiting all Coprocessor requests to finish (note: when there are JOIN in SQL statement, multiple {{distro.tidb}} Coprocessor executors may be running in parallel, which may cause this time not being a wall time)", "fields.cop_wait_addr": "Copr Address (Wait)", "fields.cop_wait_addr.desc": "The address of the {{distro.tikv}} that takes most time wait the Coprocessor request", "fields.cop_wait_avg": "Mean Cop Wait", "fields.cop_wait_max": "Max Cop Wait", "fields.cop_wait_p90": "P90 Cop Wait", "fields.db": "Execution Database", "fields.db.desc": "The database used to execute the query", "fields.digest": "Query Template ID", "fields.digest.desc": "a.k.a. Query digest", "fields.disk_max": "Max Disk", "fields.disk_max.desc": "Maximum disk usage of the query", "fields.exec_retry_count": "Retried Execution Count", "fields.exec_retry_time": "Retried Execution Time", "fields.exec_retry_time.desc": "Wall time consumed when SQL statement is retried and executed again, except for the last execution", "fields.get_commit_ts_time": "Get Commit Ts Time", "fields.get_commit_ts_time.desc": "Time consumed when getting a commit timestamp for 2PC commit phase when transaction commits", "fields.has_more_results": "Has More Results?", "fields.host": "Client Address", "fields.host.desc": "The address of the client that sends the query", "fields.index_names": "Index Names", "fields.index_names.desc": "The name of the used index", "fields.instance": "{{distro.tidb}} Instance", "fields.instance.desc": "The {{distro.tidb}} address that handles the query", "fields.is_explicit_txn": "Is Explicit Transaction?", "fields.is_internal": "Is Internal?", "fields.is_internal.desc": "Whether this is an internal query", "fields.kv_total": "KV Total", "fields.local_latch_wait_time": "Local Latch Wait Time", "fields.local_latch_wait_time.desc": "Time consumed when {{distro.tidb}} waits for the lock in the current {{distro.tidb}} instance before 2PC commit phase when transaction commits", "fields.lock_keys_time": "Lock Keys Time", "fields.lock_keys_time.desc": "Time consumed when locking keys in pessimistic transaction", "fields.memory_max": "Max Memory", "fields.memory_max.desc": "Maximum memory usage of the query", "fields.optimize_time": "Optimize Plan Time", "fields.optimize_time.desc": "Time consumed when optimizing the plan", "fields.parse_time": "Parse Time", "fields.parse_time.desc": "Time consumed when parsing the query", "fields.pd_total": "PD Total", "fields.plan": "Execution Plan", "fields.plan_digest": "Plan Digest", "fields.plan_from_binding": "Is Plan from Binding?", "fields.plan_from_cache": "Is Plan from Cache?", "fields.prepared": "Is Prepared?", "fields.prepared.desc": "Is Generated by the prepare statement", "fields.preproc_subqueries": "Preprocess Sub-Query", "fields.preproc_subqueries_time": "Preprocess Sub-Query Time", "fields.preproc_subqueries_time.desc": "Time consumed when pre-processing the subquery during the rewrite plan phase", "fields.prev_stmt": "Previous Query", "fields.prewrite_region": "Prewrite Regions", "fields.prewrite_time": "Prewrite Time", "fields.prewrite_time.desc": "Time consumed in 2PC prewrite phase when transaction commits", "fields.process_keys": "Process Keys", "fields.process_time": "Coprocessor Process Time", "fields.process_time.desc": "The total time of Coprocessor request being executed in {{distro.tikv}} (note: {{distro.tikv}} executes requests in parallel so that this is not a wall time)", "fields.query": "Query", "fields.query_time": "Latency", "fields.query_time.desc": "Execution time of the query", "fields.query_time_2": "Query Time", "fields.query_time_2.desc": "The elapsed wall time when execution the query", "fields.request_count": "Request Count", "fields.request_unit_read": "Request Unit Read", "fields.request_unit_write": "Request Unit Write", "fields.resolve_lock_time": "Resolve Lock Time", "fields.resolve_lock_time.desc": "Time consumed when {{distro.tidb}} resolves locks from other transactions in 2PC prewrite phase when transaction commits", "fields.resource_group": "Resource Group", "fields.resource_group.desc": "The resource group that the query belongs to", "fields.result": "Result", "fields.result.desc": "Whether query is executed successfully", "fields.result_rows": "Result Rows", "fields.rewrite_time": "Rewrite Plan Time", "fields.rewrite_time.desc": "Time consumed when rewriting the plan", "fields.rocksdb_block_cache_hit_count": "RocksDB Block Cache Hits", "fields.rocksdb_block_cache_hit_count.desc": "Total number of hits from the block cache (RocksDB block_cache_hit_count)", "fields.rocksdb_block_read_byte": "RocksDB Read Size", "fields.rocksdb_block_read_byte.desc": "Total number of bytes RocksDB read from file (RocksDB block_read_byte)", "fields.rocksdb_block_read_count": "RocksDB Block Reads", "fields.rocksdb_block_read_count.desc": "Total number of blocks RocksDB read from file (RocksDB block_read_count)", "fields.rocksdb_delete_skipped_count": "RocksDB Skipped Deletions", "fields.rocksdb_delete_skipped_count.desc": "Total number of deleted (a.k.a. tombstone) key versions that are skipped during iteration (RocksDB delete_skipped_count)", "fields.rocksdb_key_skipped_count": "RocksDB Skipped Keys", "fields.rocksdb_key_skipped_count.desc": "Total number of keys skipped during iteration (RocksDB key_skipped_count)", "fields.ru": "RU", "fields.ru.desc": "Request units", "fields.session_alias": "Session Alias", "fields.sql": "Query", "fields.stats": "Used Statistics", "fields.success": "Is Success?", "fields.success.desc": "Whether query is executed successfully", "fields.tidb_cpu_time": "{{distro.tidb}} CPU Time", "fields.tikv_cpu_time": "{{distro.tikv}} CPU Time", "fields.time_queued_by_rc": "Total Time Queued by RC", "fields.time_queued_by_rc.desc": "The total wait time spent in the resource queue (note: {{distro.tikv}} executes requests in parallel so that this is not a wall time)", "fields.timestamp": "Finish Time", "fields.timestamp.desc": "The time this query finished execution", "fields.total_keys": "Total Keys", "fields.total_keys.desc": "Total keys of the query", "fields.txn_retry": "Transaction Retries", "fields.txn_start_ts": "Start Timestamp", "fields.txn_start_ts.desc": "Transaction start timestamp, a.k.a. Transaction ID", "fields.user": "Execution User", "fields.user.desc": "The user that executes the query", "fields.wait_prewrite_binlog_time": "Wait Binlog Prewrite Time", "fields.wait_prewrite_binlog_time.desc": "Time consumed when waiting Binlog prewrite to finish", "fields.wait_time": "Coprocessor Wait Time", "fields.wait_time.desc": "The total time of Coprocessor request is prepared and wait to execute in {{distro.tikv}}, which may happen when retrieving a snapshot though Raft concensus protocol (note: {{distro.tikv}} waits requests in parallel so that this is not a wall time)", "fields.wait_ts": "Get Start Ts Time", "fields.wait_ts.desc": "Time consumed when getting a start timestamp when transaction begins", "fields.warnings": "Warnings", "fields.write_keys": "Write Keys", "fields.write_size": "Write Size", "fields.write_sql_response_total": "Send Response Time", "fields.write_sql_response_total.desc": "Time consumed when sending response to the SQL client" } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/locales/index.ts ================================================ import { addLangsLocales } from "@pingcap-incubator/tidb-dashboard-lib-utils" import en from "./en.json" import zh from "./zh.json" addLangsLocales({ en, zh }) ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/locales/preset.ts ================================================ import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" // @ts-expect-error @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars function useLocales() { const { tk } = useTn("slow-query") // used for gogocode to scan and generate en.json before build tk("fields.instance", "{{distro.tidb}} Instance") tk( "fields.instance.desc", "The {{distro.tidb}} address that handles the query", ) tk("fields.connection_id", "Connection ID") tk("fields.connection_id.desc", "Unique connection ID of the query") tk("fields.sql", "Query") tk("fields.query", "Query") tk("fields.timestamp", "Finish Time") tk("fields.timestamp.desc", "The time this query finished execution") tk("fields.query_time", "Latency") tk("fields.query_time.desc", "Execution time of the query") tk("fields.memory_max", "Max Memory") tk("fields.memory_max.desc", "Maximum memory usage of the query") tk("fields.disk_max", "Max Disk") tk("fields.disk_max.desc", "Maximum disk usage of the query") tk("fields.digest", "Query Template ID") tk("fields.digest.desc", "a.k.a. Query digest") tk("fields.is_internal", "Is Internal?") tk("fields.is_internal.desc", "Whether this is an internal query") tk("fields.success", "Is Success?") tk("fields.success.desc", "Whether query is executed successfully") tk("fields.prepared", "Is Prepared?") tk("fields.prepared.desc", "Is Generated by the prepare statement") tk("fields.plan_from_cache", "Is Plan from Cache?") tk("fields.plan_from_binding", "Is Plan from Binding?") tk("fields.result", "Result") tk("fields.result.desc", "Whether query is executed successfully") tk("fields.index_names", "Index Names") tk("fields.index_names.desc", "The name of the used index") tk("fields.stats", "Used Statistics") tk("fields.backoff_types", "Backoff Types") tk("fields.user", "Execution User") tk("fields.user.desc", "The user that executes the query") tk("fields.host", "Client Address") tk("fields.host.desc", "The address of the client that sends the query") tk("fields.db", "Execution Database") tk("fields.db.desc", "The database used to execute the query") tk("fields.query_time_2", "Query Time") tk( "fields.query_time_2.desc", "The elapsed wall time when execution the query", ) tk("fields.parse_time", "Parse Time") tk("fields.parse_time.desc", "Time consumed when parsing the query") tk("fields.compile_time", "Generate Plan Time") tk("fields.compile_time.desc", "Time consumed when generating the plan") tk("fields.rewrite_time", "Rewrite Plan Time") tk("fields.rewrite_time.desc", "Time consumed when rewriting the plan") tk("fields.preproc_subqueries_time", "Preprocess Sub-Query Time") tk( "fields.preproc_subqueries_time.desc", "Time consumed when pre-processing the subquery during the rewrite plan phase", ) tk("fields.optimize_time", "Optimize Plan Time") tk("fields.optimize_time.desc", "Time consumed when optimizing the plan") tk("fields.wait_ts", "Get Start Ts Time") tk( "fields.wait_ts.desc", "Time consumed when getting a start timestamp when transaction begins", ) tk("fields.cop_time", "Coprocessor Executor Time") tk( "fields.cop_time.desc", "The elapsed wall time when {{distro.tidb}} Coprocessor executor waiting all Coprocessor requests to finish (note: when there are JOIN in SQL statement, multiple {{distro.tidb}} Coprocessor executors may be running in parallel, which may cause this time not being a wall time)", ) tk("fields.wait_time", "Coprocessor Wait Time") tk( "fields.wait_time.desc", "The total time of Coprocessor request is prepared and wait to execute in {{distro.tikv}}, which may happen when retrieving a snapshot though Raft concensus protocol (note: {{distro.tikv}} waits requests in parallel so that this is not a wall time)", ) tk("fields.process_time", "Coprocessor Process Time") tk( "fields.process_time.desc", "The total time of Coprocessor request being executed in {{distro.tikv}} (note: {{distro.tikv}} executes requests in parallel so that this is not a wall time)", ) tk("fields.backoff_time", "Execution Backoff Time") tk( "fields.backoff_time.desc", "The total backoff waiting time before retry when a query encounters errors (note: there may be multiple backoffs in parallel so that this may not be a wall time)", ) tk("fields.lock_keys_time", "Lock Keys Time") tk( "fields.lock_keys_time.desc", "Time consumed when locking keys in pessimistic transaction", ) tk("fields.get_commit_ts_time", "Get Commit Ts Time") tk( "fields.get_commit_ts_time.desc", "Time consumed when getting a commit timestamp for 2PC commit phase when transaction commits", ) tk("fields.local_latch_wait_time", "Local Latch Wait Time") tk( "fields.local_latch_wait_time.desc", "Time consumed when {{distro.tidb}} waits for the lock in the current {{distro.tidb}} instance before 2PC commit phase when transaction commits", ) tk("fields.resolve_lock_time", "Resolve Lock Time") tk( "fields.resolve_lock_time.desc", "Time consumed when {{distro.tidb}} resolves locks from other transactions in 2PC prewrite phase when transaction commits", ) tk("fields.prewrite_time", "Prewrite Time") tk( "fields.prewrite_time.desc", "Time consumed in 2PC prewrite phase when transaction commits", ) tk("fields.wait_prewrite_binlog_time", "Wait Binlog Prewrite Time") tk( "fields.wait_prewrite_binlog_time.desc", "Time consumed when waiting Binlog prewrite to finish", ) tk("fields.commit_time", "Commit Time") tk( "fields.commit_time.desc", "Time consumed in 2PC commit phase when transaction commits", ) tk("fields.commit_backoff_time", "Commit Backoff Time") tk( "fields.commit_backoff_time.desc", "The total backoff waiting time when 2PC commit encounters errors (note: there may be multiple backoffs in parallel so that this may not be a wall time)", ) tk("fields.write_sql_response_total", "Send Response Time") tk( "fields.write_sql_response_total.desc", "Time consumed when sending response to the SQL client", ) tk("fields.exec_retry_time", "Retried Execution Time") tk( "fields.exec_retry_time.desc", "Wall time consumed when SQL statement is retried and executed again, except for the last execution", ) tk("fields.request_count", "Request Count") tk("fields.process_keys", "Process Keys") tk("fields.total_keys", "Total Keys") tk("fields.total_keys.desc", "Total keys of the query") tk("fields.cop_proc_addr", "Copr Address (Process)") tk( "fields.cop_proc_addr.desc", "The address of the {{distro.tikv}} that takes most time process the Coprocessor request", ) tk("fields.cop_wait_addr", "Copr Address (Wait)") tk( "fields.cop_wait_addr.desc", "The address of the {{distro.tikv}} that takes most time wait the Coprocessor request", ) tk("fields.txn_start_ts", "Start Timestamp") tk( "fields.txn_start_ts.desc", "Transaction start timestamp, a.k.a. Transaction ID", ) tk("fields.write_keys", "Write Keys") tk("fields.write_size", "Write Size") tk("fields.prewrite_region", "Prewrite Regions") tk("fields.txn_retry", "Transaction Retries") tk("fields.prev_stmt", "Previous Query") tk("fields.plan", "Execution Plan") tk("fields.cop_proc_avg", "Mean Cop Proc") tk("fields.cop_wait_avg", "Mean Cop Wait") tk("fields.rocksdb_delete_skipped_count", "RocksDB Skipped Deletions") tk( "fields.rocksdb_delete_skipped_count.desc", "Total number of deleted (a.k.a. tombstone) key versions that are skipped during iteration (RocksDB delete_skipped_count)", ) tk("fields.rocksdb_key_skipped_count", "RocksDB Skipped Keys") tk( "fields.rocksdb_key_skipped_count.desc", "Total number of keys skipped during iteration (RocksDB key_skipped_count)", ) tk("fields.rocksdb_block_cache_hit_count", "RocksDB Block Cache Hits") tk( "fields.rocksdb_block_cache_hit_count.desc", "Total number of hits from the block cache (RocksDB block_cache_hit_count)", ) tk("fields.rocksdb_block_read_count", "RocksDB Block Reads") tk( "fields.rocksdb_block_read_count.desc", "Total number of blocks RocksDB read from file (RocksDB block_read_count)", ) tk("fields.rocksdb_block_read_byte", "RocksDB Read Size") tk( "fields.rocksdb_block_read_byte.desc", "Total number of bytes RocksDB read from file (RocksDB block_read_byte)", ) tk("fields.ru", "RU") tk("fields.ru.desc", "Request units") tk("fields.resource_group", "Resource Group") tk( "fields.resource_group.desc", "The resource group that the query belongs to", ) tk("fields.time_queued_by_rc", "Total Time Queued by RC") tk( "fields.time_queued_by_rc.desc", "The total wait time spent in the resource queue (note: {{distro.tikv}} executes requests in parallel so that this is not a wall time)", ) tk("fields.ia_remote_read_segment_size", "IA Remote Read Segment Size") tk( "fields.ia_remote_read_segment_size.desc", "Total number of bytes read from IA remote segments", ) tk("fields.ia_remote_read_segment_wait_time", "IA Remote Read Segment Wait Time") tk( "fields.ia_remote_read_segment_wait_time.desc", "The wait time spent reading IA remote segments", ) // additional fields tk("fields.backoff_detail", "Backoff Detail") tk("fields.backoff_total", "Backoff Total") tk("fields.binary_plan", "Binary Plan") tk("fields.cop_proc_max", "Max Cop Proc") tk("fields.cop_proc_p90", "P90 Cop Proc") tk("fields.cop_wait_max", "Max Cop Wait") tk("fields.cop_wait_p90", "P90 Cop Wait") tk("fields.exec_retry_count", "Retried Execution Count") tk("fields.has_more_results", "Has More Results?") tk("fields.is_explicit_txn", "Is Explicit Transaction?") tk("fields.pd_total", "PD Total") tk("fields.kv_total", "KV Total") tk("fields.preproc_subqueries", "Preprocess Sub-Query") tk("fields.request_unit_read", "Request Unit Read") tk("fields.request_unit_write", "Request Unit Write") tk("fields.session_alias", "Session Alias") tk("fields.result_rows", "Result Rows") tk("fields.warnings", "Warnings") tk("fields.plan_digest", "Plan Digest") tk("fields.tidb_cpu_time", "{{distro.tidb}} CPU Time") tk("fields.tikv_cpu_time", "{{distro.tikv}} CPU Time") } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/locales/zh.json ================================================ { "__namespace__": "slow-query", "__comment__": "this file can be updated by running `pnpm gen:locales` command", "fields.backoff_detail": "Backoff 详情", "fields.backoff_time": "执行阶段累计 Backoff 耗时", "fields.backoff_time.desc": "在执行失败时,Backoff 机制等待一段时间再重试时的 Backoff 累计耗时(注:可能同时存在多个 Backoff,因此该时间可能不是自然流逝时间)", "fields.backoff_total": "Backoff 总数", "fields.backoff_types": "重试类型", "fields.binary_plan": "Binary 执行计划", "fields.commit_backoff_time": "Commit 阶段累计 Backoff 耗时", "fields.commit_backoff_time.desc": "事务递交失败时,Backoff 机制等待一段时间再重试时的 Backoff 累计耗时(注:可能同时存在多个 Backoff,因此该时间可能不是自然流逝时间)", "fields.commit_time": "Commit 阶段耗时", "fields.commit_time.desc": "事务两阶段提交中第二阶段(commit 阶段)的耗时", "fields.compile_time": "生成执行计划耗时", "fields.compile_time.desc": "Time consumed when generating the plan", "fields.connection_id": "连接号", "fields.connection_id.desc": "SQL 查询客户端连接 ID", "fields.cop_proc_addr": "最长处理时间实例", "fields.cop_proc_addr.desc": "The address of the {{distro.tikv}} that takes most time process the Coprocessor request", "fields.cop_proc_avg": "平均处理时间", "fields.cop_proc_max": "最长处理时间", "fields.cop_proc_p90": "P90 处理时间", "fields.cop_time": "Coprocessor 执行耗时", "fields.cop_time.desc": "{{distro.tidb}} Coprocessor 算子等待所有任务在 {{distro.tikv}} 上并行执行完毕耗费的自然时间(注:当 SQL 语句中包含 JOIN 时,多个 {{distro.tidb}} Coprocessor 算子可能会并行执行,此时不再等同于自然时间)", "fields.cop_wait_addr": "最长等待时间实例", "fields.cop_wait_addr.desc": "The address of the {{distro.tikv}} that takes most time wait the Coprocessor request", "fields.cop_wait_avg": "平均等待时间", "fields.cop_wait_max": "最长等待时间", "fields.cop_wait_p90": "P90 等待时间", "fields.db": "执行数据库", "fields.db.desc": "执行该 SQL 查询时使用的数据库名称", "fields.digest": "SQL 模板 ID", "fields.digest.desc": "SQL 模板的唯一标识(SQL 指纹)", "fields.disk_max": "最大磁盘空间", "fields.disk_max.desc": "该 SQL 查询执行时占用的最大磁盘空间", "fields.exec_retry_count": "前序执行重试次数", "fields.exec_retry_time": "前序执行重试耗时", "fields.exec_retry_time.desc": "由于锁冲突或错误,计划可能会执行失败并重试执行多次,该时间是不包含最后一次执行的前序执行自然时间(注:执行计划中的时间不含该前序时间)", "fields.get_commit_ts_time": "取事务 Commit Ts 耗时", "fields.get_commit_ts_time.desc": "从 {{distro.pd}} 取提交时间戳(事务号)步骤的耗时", "fields.has_more_results": "是否有更多结果?", "fields.host": "客户端地址", "fields.host.desc": "发送 SQL 查询的客户端地址", "fields.index_names": "索引名", "fields.index_names.desc": "SQL 查询执行时使用的索引名称", "fields.instance": "{{distro.tidb}} 实例", "fields.instance.desc": "处理该 SQL 查询的 {{distro.tidb}} 实例地址", "fields.is_explicit_txn": "是否为显式事务?", "fields.is_internal": "是否为内部 SQL 查询", "fields.is_internal.desc": "Whether this is an internal query", "fields.kv_total": "KV 总数", "fields.local_latch_wait_time": "{{distro.tidb}} 本地等锁耗时", "fields.local_latch_wait_time.desc": "事务在 {{distro.tidb}} 本地与其他事务产生了锁冲突并等待的耗时", "fields.lock_keys_time": "上锁耗时", "fields.lock_keys_time.desc": "悲观事务中对相关行数据进行上锁的耗时", "fields.memory_max": "最大内存", "fields.memory_max.desc": "该 SQL 查询执行时占用的最大内存空间", "fields.optimize_time": "优化执行计划耗时", "fields.optimize_time.desc": "Time consumed when optimizing the plan", "fields.parse_time": "解析耗时", "fields.parse_time.desc": "解析该 SQL 查询的耗时", "fields.pd_total": "PD 总数", "fields.plan": "执行计划", "fields.plan_digest": "Plan ID", "fields.plan_from_binding": "查询计划是否来自绑定", "fields.plan_from_cache": "查询计划是否来自缓存", "fields.prepared": "是否由 prepare 语句生成", "fields.prepared.desc": "Is Generated by the prepare statement", "fields.preproc_subqueries": "子查询预处理", "fields.preproc_subqueries_time": "子查询预处理耗时", "fields.preproc_subqueries_time.desc": "Time consumed when pre-processing the subquery during the rewrite plan phase", "fields.prev_stmt": "前一条 SQL 查询", "fields.prewrite_region": "Prewrite 涉及 Regions 个数", "fields.prewrite_time": "Prewrite 阶段耗时", "fields.prewrite_time.desc": "事务两阶段提交中第一阶段(prewrite 阶段)的耗时", "fields.process_keys": "可见版本数", "fields.process_time": "Coprocessor 累计执行耗时", "fields.process_time.desc": "{{distro.tikv}} 执行 Coprocessor 任务的累计处理时间(注:{{distro.tikv}} 会并行处理任务,因此该时间不是自然流逝时间)", "fields.query": "SQL", "fields.query_time": "总执行时间", "fields.query_time.desc": "该 SQL 查询总的执行时间", "fields.query_time_2": "SQL 执行时间", "fields.query_time_2.desc": "执行 SQL 耗费的自然时间", "fields.request_count": "Coprocessor 请求数", "fields.request_unit_read": "读取资源单位", "fields.request_unit_write": "写入资源单位", "fields.resolve_lock_time": "解锁耗时", "fields.resolve_lock_time.desc": "事务在提交过程中与其他事务产生了锁冲突并处理锁冲突的耗时", "fields.resource_group": "资源组", "fields.resource_group.desc": "SQL 语句所属的资源组", "fields.result": "执行结果", "fields.result.desc": "SQL 查询是否执行成功", "fields.result_rows": "返回行数", "fields.rewrite_time": "重写执行计划耗时", "fields.rewrite_time.desc": "Time consumed when rewriting the plan", "fields.rocksdb_block_cache_hit_count": "RocksDB 缓存读次数", "fields.rocksdb_block_cache_hit_count.desc": "RocksDB 从 Block Cache 缓存中读数据的次数 (block_cache_hit_count)", "fields.rocksdb_block_read_byte": "RocksDB 文件系统读数据量", "fields.rocksdb_block_read_byte.desc": "RocksDB 从文件系统中读数据的数据量 (block_read_byte)", "fields.rocksdb_block_read_count": "RocksDB 文件系统读次数", "fields.rocksdb_block_read_count.desc": "RocksDB 从文件系统中读数据的次数 (block_read_count)", "fields.rocksdb_delete_skipped_count": "RocksDB 已删除 Key 扫描数", "fields.rocksdb_delete_skipped_count.desc": "RocksDB 扫数据时遇到的已删除 (tombstone) Key 数量 (delete_skipped_count)", "fields.rocksdb_key_skipped_count": "RocksDB Key 扫描数", "fields.rocksdb_key_skipped_count.desc": "RocksDB 扫数据时所有遇到的 Key 数量 (key_skipped_count)", "fields.ru": "RU", "fields.ru.desc": "资源单位(RU)", "fields.session_alias": "会话别名", "fields.sql": "SQL", "fields.stats": "使用的统计信息", "fields.success": "是否执行成功", "fields.success.desc": "SQL 查询是否执行成功", "fields.tidb_cpu_time": "{{distro.tidb}} CPU 时间", "fields.tikv_cpu_time": "{{distro.tikv}} CPU 时间", "fields.time_queued_by_rc": "RC 等待累积耗时", "fields.time_queued_by_rc.desc": "SQL 语句在资源组队列中等待的累积时间(注:{{distro.tikv}} 会并行等待任务,因此该时间不是自然流逝时间)", "fields.timestamp": "结束运行时间", "fields.timestamp.desc": "该 SQL 查询结束运行时的时间", "fields.total_keys": "遇到版本数", "fields.total_keys.desc": "Total keys of the query", "fields.txn_retry": "事务重试次数", "fields.txn_start_ts": "事务号", "fields.txn_start_ts.desc": "事务开始的时间戳,也即是事务号", "fields.user": "执行用户名", "fields.user.desc": "执行该 SQL 查询的用户名,可能存在多个执行用户,仅显示其中某一个", "fields.wait_prewrite_binlog_time": "Binlog Prewrite 等待耗时", "fields.wait_prewrite_binlog_time.desc": "等待 Binlog Prewrite 完成的耗时", "fields.wait_time": "Coprocessor 累计等待耗时", "fields.wait_time.desc": "{{distro.tikv}} 准备并等待 Coprocessor 任务执行的累计时间,等待过程中包括通过 Raft 一致性协议取快照等(注:{{distro.tikv}} 会并行等待任务,因此该时间不是自然流逝时间)", "fields.wait_ts": "取事务 Start Ts 耗时", "fields.wait_ts.desc": "从 {{distro.pd}} 取事务开始时间戳步骤的耗时", "fields.warnings": "警告信息", "fields.write_keys": "写入 Key 个数", "fields.write_size": "写入数据量", "fields.write_sql_response_total": "发送结果耗时", "fields.write_sql_response_total.desc": "发送 SQL 语句执行结果给客户端的耗时", "All (Raw JSON)": "全部 (原始 JSON)", "Basic": "基础信息", "Clear Filters": "清空筛选条件", "Coprocessor": "Coprocessor", "Detail": "详情", "Due to the limitation, currently only support to query max 24 hours data, so the actual time range is {{begin}} to {{end}}": "由于限制,目前仅支持查询最大 24 小时的数据,实际时间范围是 {{begin}} ~ {{end}}", "Find SQL text": "查找 SQL", "I got it": "我知道了", "No Data": "无结果", "Plan": "执行计划", "Query": "SQL 查询", "SQL digest": "SQL digest", "Slow Query Detail": "慢查询详情", "Table": "表格", "Tell me again next time": "下次还告诉我", "Text": "文本", "Time": "执行时间", "Tips": "提示", "Transaction": "事务", "View related statement in statement page (but it may be evicted already)": "在 SQL 语句页面查看相关的 SQL 语句(但有可能已经被淘汰)", "View related statement in statement page, but it may be evicted already, so the result maybe empty": "在 SQL 语句页面查看相关的 SQL 语句,但该 SQL 语句有可能已经被淘汰,所以结果可能为空", "View related statement": "查看相关的 SQL 语句", "Warnings": "警告", "When opening the detail page, you can press Ctrl or to view it in a new tab, or Shift to view it in a new window.": "在打开详情页时,你可以按住 Ctrl 在新标签页打开,或按住 Shift 在新窗口中打开。", "page": "页", "total": "总计" } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/models/advanced-filter-info-model.ts ================================================ import { AdvancedFilterInfo } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" export type AdvancedFilterInfoModel = AdvancedFilterInfo ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/models/index.ts ================================================ export * from "./slowquery-model" export * from "./advanced-filter-info-model" ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/models/slowquery-model.ts ================================================ export interface SlowqueryModel { backoff_time?: number backoff_types?: string binary_plan?: string binary_plan_json?: string binary_plan_text?: string commit_backoff_time?: number commit_time?: number compile_time?: number connection_id?: string cop_proc_addr?: string cop_proc_avg?: number cop_proc_max?: number cop_proc_p90?: number cop_time?: number cop_wait_addr?: string cop_wait_avg?: number cop_wait_max?: number cop_wait_p90?: number db?: string digest?: string disk_max?: number exec_retry_time?: number get_commit_ts_time?: number host?: string ia_remote_read_segment_size?: number ia_remote_read_segment_wait_time?: number index_names?: string instance?: string is_internal?: number local_latch_wait_time?: number lock_keys_time?: number memory_max?: number optimize_time?: number parse_time?: number plan?: string plan_from_binding?: number plan_from_cache?: number prepared?: number preproc_subqueries_time?: number prev_stmt?: string prewrite_region?: number prewrite_time?: number process_keys?: number process_time?: number query?: string query_time?: number request_count?: number resolve_lock_time?: number resource_group?: string rewrite_time?: number rocksdb_block_cache_hit_count?: number rocksdb_block_read_byte?: number rocksdb_block_read_count?: number rocksdb_delete_skipped_count?: number rocksdb_key_skipped_count?: number ru?: number stats?: string success?: number time_queued_by_rc?: number timestamp?: number total_keys?: number txn_retry?: number txn_start_ts?: string user?: string wait_prewrite_binlog_time?: number wait_time?: number wait_ts?: number warnings?: string | object[] write_keys?: number write_size?: number write_sql_response_total?: number } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/detail-basic.tsx ================================================ import { InfoModel, InfoTable, } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { formatNumByUnit, formatTime, useTn, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { useMemo } from "react" import { SlowqueryModel } from "../../models" function getData( data: SlowqueryModel, tk: (key: string) => string, ): InfoModel[] { return [ { name: tk("fields.timestamp"), value: formatTime(data.timestamp! * 1000), desc: tk("fields.timestamp.desc"), }, { name: tk("fields.digest"), value: data.digest!, desc: tk("fields.digest.desc"), }, { name: tk("fields.is_internal"), value: data.is_internal === 1 ? "Yes" : "No", desc: tk("fields.is_internal.desc"), }, { name: tk("fields.success"), value: data.success === 1 ? "Yes" : "No", desc: tk("fields.success.desc"), }, { name: tk("fields.prepared"), value: data.prepared === 1 ? "Yes" : "No", desc: tk("fields.prepared.desc"), }, { name: tk("fields.plan_from_cache"), value: data.plan_from_cache === 1 ? "Yes" : "No", }, { name: tk("fields.plan_from_binding"), value: data.plan_from_binding === 1 ? "Yes" : "No", }, { name: tk("fields.db"), value: data.db || "-", desc: tk("fields.db.desc"), }, { name: tk("fields.index_names"), value: data.index_names || "-", desc: tk("fields.index_names.desc"), }, { name: tk("fields.stats"), value: data.stats || "-", }, { name: tk("fields.backoff_types"), value: data.backoff_types || "-", }, { name: tk("fields.memory_max"), value: formatNumByUnit(data.memory_max || 0, "bytes"), desc: tk("fields.memory_max.desc"), }, { name: tk("fields.disk_max"), value: formatNumByUnit(data.disk_max || 0, "bytes"), desc: tk("fields.disk_max.desc"), }, { name: tk("fields.instance"), value: data.instance || "-", desc: tk("fields.instance.desc"), }, { name: tk("fields.connection_id"), value: data.connection_id || "-", desc: tk("fields.connection_id.desc"), }, { name: tk("fields.user"), value: data.user || "-", desc: tk("fields.user.desc"), }, { name: tk("fields.host"), value: data.host || "-", desc: tk("fields.host.desc"), }, ] } export function DetailBasic({ data }: { data: SlowqueryModel }) { const { tk } = useTn("slow-query") const infoData = useMemo(() => getData(data, tk), [data, tk]) return } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/detail-copr.tsx ================================================ import { InfoModel, InfoTable, } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { formatNumByUnit, useTn, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { useMemo } from "react" import { SlowqueryModel } from "../../models" function getData( data: SlowqueryModel, tk: (key: string) => string, ): InfoModel[] { return [ { name: tk("fields.request_count"), value: formatNumByUnit(data.request_count || 0, "short"), }, { name: tk("fields.process_keys"), value: formatNumByUnit(data.process_keys || 0, "short"), }, { name: tk("fields.total_keys"), value: formatNumByUnit(data.total_keys || 0, "short"), }, { name: tk("fields.cop_proc_addr"), value: data.cop_proc_addr || "-", desc: tk("fields.cop_proc_addr.desc"), }, { name: tk("fields.cop_wait_addr"), value: data.cop_wait_addr || "-", desc: tk("fields.cop_wait_addr.desc"), }, { name: tk("fields.rocksdb_block_cache_hit_count"), value: formatNumByUnit(data.rocksdb_block_cache_hit_count || 0, "short"), desc: tk("fields.rocksdb_block_cache_hit_count.desc"), }, { name: tk("fields.rocksdb_block_read_byte"), value: formatNumByUnit(data.rocksdb_block_read_byte || 0, "bytes"), desc: tk("fields.rocksdb_block_read_byte.desc"), }, { name: tk("fields.rocksdb_block_read_count"), value: formatNumByUnit(data.rocksdb_block_read_count || 0, "short"), desc: tk("fields.rocksdb_block_read_count.desc"), }, { name: tk("fields.rocksdb_delete_skipped_count"), value: formatNumByUnit(data.rocksdb_delete_skipped_count || 0, "short"), desc: tk("fields.rocksdb_delete_skipped_count.desc"), }, { name: tk("fields.rocksdb_key_skipped_count"), value: formatNumByUnit(data.rocksdb_key_skipped_count || 0, "short"), desc: tk("fields.rocksdb_key_skipped_count.desc"), }, { name: tk("fields.ia_remote_read_segment_size"), value: formatNumByUnit(data.ia_remote_read_segment_size || 0, "bytes"), desc: tk("fields.ia_remote_read_segment_size.desc"), }, { name: tk("fields.ia_remote_read_segment_wait_time"), value: formatNumByUnit( data.ia_remote_read_segment_wait_time || 0, "s", ), desc: tk("fields.ia_remote_read_segment_wait_time.desc"), }, ] } export function DetailCopr({ data }: { data: SlowqueryModel }) { const { tk } = useTn("slow-query") const infoData = useMemo(() => getData(data, tk), [data, tk]) return } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/detail-tabs.tsx ================================================ import { CustomJsonView } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { ActionIcon, Card, CopyButton, Stack, Tabs, Title, Tooltip, } from "@tidbcloud/uikit" import { IconCheck, IconCopy02 } from "@tidbcloud/uikit/icons" import { useMemo } from "react" import { SlowqueryModel } from "../../models" import { DetailBasic } from "./detail-basic" import { DetailCopr } from "./detail-copr" import { DetailTime } from "./detail-time" import { DetailTxn } from "./detail-txn" function DetailAll({ data }: { data: SlowqueryModel }) { return ( {({ copied, copy }) => ( {copied ? : } )} ) } export function DetailTabs({ data }: { data: SlowqueryModel }) { const { tt } = useTn("slow-query") const tabs = useMemo(() => { const _tabs = [ { label: tt("Basic"), value: "basic", component: , }, { label: tt("Time"), value: "time", component: , }, { label: tt("Coprocessor"), value: "copr", component: , }, { label: tt("Transaction"), value: "txn", component: , }, ] if (data.warnings) { let jsonData = {} if (typeof data.warnings === "string") { // data.warnings maybe "null", after JSON.parse, it will be null jsonData = JSON.parse(data.warnings) } else if (typeof data.warnings === "object") { jsonData = data.warnings } if (jsonData) { _tabs.push({ label: tt("Warnings"), value: "warnings", component: , }) } } // all _tabs.push({ label: tt("All (Raw JSON)"), value: "all", component: , }) return _tabs }, [data, tt]) return ( {tt("Detail")} {tabs.map((tab) => ( {tab.label} ))} {tabs.map((tab) => ( {tab.component} ))} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/detail-time.tsx ================================================ import { InfoModel, InfoTable, } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { formatNumByUnit, useTn, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { useMemo } from "react" import { SlowqueryModel } from "../../models" function getData( data: SlowqueryModel, tk: (key: string) => string, ): InfoModel[] { return [ { name: tk("fields.query_time_2"), value: formatNumByUnit(data.query_time! * 10e8, "ns"), level: 0, desc: tk("fields.query_time_2.desc"), }, { name: tk("fields.parse_time"), value: formatNumByUnit(data.parse_time! * 10e8, "ns"), level: 1, desc: tk("fields.parse_time.desc"), }, { name: tk("fields.compile_time"), value: formatNumByUnit(data.compile_time! * 10e8, "ns"), level: 1, desc: "", }, { name: tk("fields.rewrite_time"), value: formatNumByUnit(data.rewrite_time! * 10e8, "ns"), level: 2, desc: "", }, { name: tk("fields.preproc_subqueries_time"), value: formatNumByUnit(data.preproc_subqueries_time! * 10e8, "ns"), level: 3, desc: tk("fields.preproc_subqueries_time.desc"), }, { name: tk("fields.optimize_time"), value: formatNumByUnit(data.optimize_time! * 10e8, "ns"), level: 2, desc: tk("fields.optimize_time.desc"), }, { name: tk("fields.cop_time"), value: formatNumByUnit(data.cop_time! * 10e8, "ns"), level: 1, desc: tk("fields.cop_time.desc"), }, { name: tk("fields.wait_time"), value: formatNumByUnit(data.wait_time! * 10e8, "ns"), level: 2, desc: tk("fields.wait_time.desc"), }, { name: tk("fields.process_time"), value: formatNumByUnit(data.process_time! * 10e8, "ns"), level: 2, desc: tk("fields.process_time.desc"), }, { name: tk("fields.local_latch_wait_time"), value: formatNumByUnit(data.local_latch_wait_time! * 10e8, "ns"), level: 1, desc: tk("fields.local_latch_wait_time.desc"), }, { name: tk("fields.lock_keys_time"), value: formatNumByUnit(data.lock_keys_time! * 10e8, "ns"), level: 1, desc: tk("fields.lock_keys_time.desc"), }, { name: tk("fields.resolve_lock_time"), value: formatNumByUnit(data.resolve_lock_time! * 10e8, "ns"), level: 1, desc: tk("fields.resolve_lock_time.desc"), }, { name: tk("fields.wait_ts"), value: formatNumByUnit(data.wait_ts! * 10e8, "ns"), level: 1, desc: tk("fields.wait_ts.desc"), }, { name: tk("fields.get_commit_ts_time"), value: formatNumByUnit(data.get_commit_ts_time! * 10e8, "ns"), level: 1, desc: tk("fields.get_commit_ts_time.desc"), }, { name: tk("fields.prewrite_time"), value: formatNumByUnit(data.prewrite_time! * 10e8, "ns"), level: 1, desc: tk("fields.prewrite_time.desc"), }, { name: tk("fields.commit_time"), value: formatNumByUnit(data.commit_time! * 10e8, "ns"), level: 1, desc: tk("fields.commit_time.desc"), }, { name: tk("fields.backoff_time"), value: formatNumByUnit(data.backoff_time! * 10e8, "ns"), level: 1, desc: tk("fields.backoff_time.desc"), }, { name: tk("fields.commit_backoff_time"), value: formatNumByUnit(data.commit_backoff_time! * 10e8, "ns"), level: 1, desc: tk("fields.commit_backoff_time.desc"), }, { name: tk("fields.exec_retry_time"), value: formatNumByUnit(data.exec_retry_time! * 10e8, "ns"), level: 1, desc: tk("fields.exec_retry_time.desc"), }, { name: tk("fields.write_sql_response_total"), value: formatNumByUnit(data.write_sql_response_total! * 10e8, "ns"), level: 1, desc: tk("fields.write_sql_response_total.desc"), }, { name: tk("fields.wait_prewrite_binlog_time"), value: formatNumByUnit(data.wait_prewrite_binlog_time! * 10e8, "ns"), level: 1, desc: tk("fields.wait_prewrite_binlog_time.desc"), }, ] } export function DetailTime({ data }: { data: SlowqueryModel }) { const { tk } = useTn("slow-query") const infoData = useMemo(() => getData(data, tk), [data, tk]) return } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/detail-txn.tsx ================================================ import { InfoModel, InfoTable, } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { formatNumByUnit, useTn, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { useMemo } from "react" import { SlowqueryModel } from "../../models" function getData( data: SlowqueryModel, tk: (key: string) => string, ): InfoModel[] { return [ { name: tk("fields.txn_start_ts"), value: data.txn_start_ts!, desc: tk("fields.txn_start_ts.desc"), }, { name: tk("fields.write_keys"), value: formatNumByUnit(data.write_keys || 0, "short"), }, { name: tk("fields.write_size"), value: formatNumByUnit(data.write_size || 0, "bytes"), }, { name: tk("fields.prewrite_region"), value: formatNumByUnit(data.prewrite_region || 0, "short"), }, { name: tk("fields.txn_retry"), value: formatNumByUnit(data.txn_retry || 0, "short"), }, ] } export function DetailTxn({ data }: { data: SlowqueryModel }) { const { tk } = useTn("slow-query") const infoData = useMemo(() => getData(data, tk), [data, tk]) return } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/index.tsx ================================================ import { LoadingSkeleton } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { ActionIcon, Group, Stack, Typography } from "@tidbcloud/uikit" import { IconChevronLeft } from "@tidbcloud/uikit/icons" import { useAppContext } from "../../ctx" import { useDetailData } from "../../utils/use-data" import { DetailTabs } from "./detail-tabs" import { DetailPlan } from "./plan" import { DetailQuery } from "./query" import { RelatedStatementButton } from "./related-statement-button" import { SqlHistory } from "./sql-history" import { SqlLimit } from "./sql-limit" export function Detail() { const ctx = useAppContext() const { data: detailData, isLoading } = useDetailData() const { tt } = useTn("slow-query") return ( {ctx.cfg.showDetailBack !== false && ( {tt("Slow Query Detail")} )} {isLoading && } {detailData && ( {/* */} {detailData.plan && } )} ) } export { RelatedStatementButton } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/plan.tsx ================================================ import { PlanTable } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Card, Stack, Tabs, Title } from "@tidbcloud/uikit" import { CodeBlock } from "@tidbcloud/uikit/biz" import { useMemo } from "react" export function DetailPlan({ plan }: { plan: string }) { const { tt } = useTn("slow-query") const tabs = useMemo(() => { return [ { label: tt("Text"), value: "text", component: (
    {content}
    } foldProps={{ persistenceKey: "slow-query.detail.plan", iconVisible: true, }} > {plan}
    ), }, { label: tt("Table"), value: "table", component: , }, ] }, [plan, tt]) return ( {tt("Plan")} {tabs.map((tab) => ( {tab.label} ))} {tabs.map((tab) => ( {tab.component} ))} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/query.tsx ================================================ import { formatSql, useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Card, Stack, Title } from "@tidbcloud/uikit" import { CodeBlock } from "@tidbcloud/uikit/biz" import { useMemo } from "react" export function DetailQuery({ sql }: { sql: string }) { const formattedSQL = useMemo(() => formatSql(sql), [sql]) const { tt } = useTn("slow-query") return ( {tt("Query")} {formattedSQL} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/related-statement-button.tsx ================================================ import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Button, Tooltip } from "@tidbcloud/uikit" import { useMemo } from "react" import { useAppContext } from "../../ctx" import { useDetailUrlState } from "../../shared-state/detail-url-state" import { useDetailData } from "../../utils/use-data" export function RelatedStatementButton() { const ctx = useAppContext() const { id } = useDetailUrlState() const { data: detailData } = useDetailData() const statementId = useMemo(() => { const [sqlDigest, _connectionId, _timestamp, from, to] = id.split(",") return [from, to, sqlDigest, detailData?.db].join(",") }, [id, detailData?.db]) const { tt } = useTn("slow-query") if (!detailData) { return null } return ( ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/related-statement-link.tsx ================================================ import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Anchor, Card, Group } from "@tidbcloud/uikit" import { IconLinkExternal01 } from "@tidbcloud/uikit/icons" import { useMemo } from "react" import { useAppContext } from "../../ctx" import { useDetailUrlState } from "../../shared-state/detail-url-state" export function RelatedStatementLink({ dbName }: { dbName: string }) { const { id } = useDetailUrlState() const ctx = useAppContext() const statementId = useMemo(() => { const [sqlDigest, _connectionId, _timestamp, from, to] = id.split(",") return [from, to, sqlDigest, dbName].join(",") }, [id, dbName]) const { tt } = useTn("slow-query") return ( { ctx.actions.openStatement(statementId) }} w="fit-content" > {tt( "View related statement in statement page (but it may be evicted already)", )} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/sql-history.tsx ================================================ import { TimeRange } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { useMemo } from "react" import { AppProvider, SqlHistoryCard } from "../../../_shared/sql-history" import { useAppContext } from "../../ctx" import { useDetailUrlState } from "../../shared-state/detail-url-state" export function SqlHistory({ sqlDigest }: { sqlDigest: string }) { const ctx = useAppContext() const { id } = useDetailUrlState() const initialTimeRange = useMemo(() => { const [_sqlDigest, _connectionId, _timestamp, from, to] = id.split(",") return { type: "absolute", value: [Number(from), Number(to)] } }, [id]) const ctxValue = useMemo( () => ({ ...ctx, cfg: { parentAppName: "slow-query", sqlDigest, initialTimeRange, timeRangeMaxDuration: 24 * 60 * 60, }, }), [ctx, sqlDigest, initialTimeRange], ) return ( ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/detail/sql-limit.tsx ================================================ import { useMemo } from "react" import { AppProvider, SqlLimitCard } from "../../../_shared/sql-limit" import { useAppContext } from "../../ctx" export function SqlLimit({ sqlDigest }: { sqlDigest: string }) { const ctx = useAppContext() const ctxValue = useMemo( () => ({ ...ctx, sqlDigest, }), [ctx, sqlDigest], ) return ( ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/list/advanced-filters-modal.tsx ================================================ import { AdvancedFiltersModal as AFModal } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { useMemo } from "react" import { useAppContext } from "../../ctx" import { useListUrlState } from "../../shared-state/list-url-state" import { useAdvancedFilterNamesData } from "../../utils/use-data" export function AdvancedFiltersModal() { const ctx = useAppContext() const { data: availableFiltersData } = useAdvancedFilterNamesData() const { advancedFilters, setAdvancedFilters } = useListUrlState() const { tk } = useTn("slow-query") const availableFilters = useMemo( () => (availableFiltersData || []).map((f) => ({ label: tk(`fields.${f}`), value: f, })), [availableFiltersData, tk], ) function handleReqFilterInfo(name: string) { return ctx.api.getAdvancedFilterInfo({ name }) } return ( ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/list/cols-select.tsx ================================================ import { ColumnMultiSelect } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { useMemo } from "react" import { useListUrlState } from "../../shared-state/list-url-state" import { useAvailableFieldsData } from "../../utils/use-data" import { useListTableColumns } from "./cols" export function ColsSelect() { const { cols, setCols } = useListUrlState() const { data: availableFields } = useAvailableFieldsData() const tableColumns = useListTableColumns() const colsData = useMemo(() => { return tableColumns .filter((f) => f.id !== undefined) .filter((f) => availableFields?.includes(f.id!)) .map((f) => ({ label: f.header, val: f.id! })) }, [availableFields, tableColumns]) function handleColsChange(newCols: string[]) { // to avoid conflict with the default value ("query,timestamp,query_time,memory_max") when cols is no value if (newCols.length === 0) { setCols(["empty"]) } else { setCols(newCols) } } return ( setCols([])} /> ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/list/cols.tsx ================================================ import { SQLWithHover } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { Trans, useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Box, Kbd, Typography, openConfirmModal } from "@tidbcloud/uikit" import { useMemo } from "react" import { TableColsFactory } from "../../../_shared/cols-factory" import { useAppContext } from "../../ctx" import { SlowqueryModel } from "../../models" import { useSelectedSlowQueryState, useTimeRangeValueState, } from "../../shared-state/memory-state" const REMEMBER_KEY = "slow-query.press_ctrl_to_open_in_new_tab.tip.remember" // @ts-expect-error @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars function useLocales() { const { tt } = useTn("slow-query") // used for gogocode to scan and generate en.json before build tt( "When opening the detail page, you can press Ctrl or to view it in a new tab, or Shift to view it in a new window.", ) } function SqlCell({ row }: { row: SlowqueryModel }) { const { tt } = useTn("slow-query") const ctx = useAppContext() const trv = useTimeRangeValueState((s) => s.trv) const setSelectedSlowQuery = useSelectedSlowQueryState( (s) => s.setSelectedSlowQuery, ) function handleClick(ev: React.MouseEvent) { const { digest, connection_id, timestamp } = row const slowQueryId = [digest, connection_id, timestamp].join(",") setSelectedSlowQuery(slowQueryId) const detailId = [slowQueryId, trv[0], trv[1]].join(",") const newTab = ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey // if the user don't press the ctrl/cmd/shift/alt and don't know this operation before // we should show a confirm dialog to tell the user this tip // after he know it, we won't show it again if (!newTab) { const remember = localStorage.getItem(REMEMBER_KEY) if (remember !== "true") { openConfirmModal({ title: tt("Tips"), children: ( Ctrl or to view it in a new tab, or Shift to view it in a new window." } components={{ kbd: }} /> ), labels: { confirm: tt("I got it"), cancel: tt("Tell me again next time"), }, onConfirm: () => { localStorage.setItem(REMEMBER_KEY, "true") }, }) } } ctx.actions.openDetail(detailId, newTab) } return ( ) } export function useListTableColumns() { const { tk } = useTn("slow-query") const columns = useMemo(() => { const tcf = new TableColsFactory(tk) return tcf.columns([ // basic tcf.text("query").patchConfig({ minSize: 600, accessorFn: (row) => , }), tcf.text("digest"), tcf.text("instance"), tcf.text("db"), tcf.text("connection_id"), tcf.timestamp("timestamp"), tcf.number("query_time", "s"), tcf.number("parse_time", "s"), tcf.number("compile_time", "s"), tcf.number("process_time", "s"), tcf.number("memory_max", "bytes"), tcf.number("disk_max", "bytes"), tcf.text("txn_start_ts"), tcf.text("success").patchConfig({ accessorFn: (row) => (row.success === 1 ? "Yes" : "No"), }), tcf.text("is_internal").patchConfig({ accessorFn: (row) => (row.is_internal === 1 ? "Yes" : "No"), }), tcf.text("prepared").patchConfig({ accessorFn: (row) => (row.prepared === 1 ? "Yes" : "No"), }), tcf.text("index_names"), tcf.text("stats"), tcf.text("backoff_types"), // connection tcf.text("user"), tcf.text("host"), // time tcf.number("wait_time", "s"), tcf.number("backoff_time", "s"), tcf.number("get_commit_ts_time", "s"), tcf.number("local_latch_wait_time", "s"), tcf.number("prewrite_time", "s"), tcf.number("commit_time", "s"), tcf.number("commit_backoff_time", "s"), tcf.number("resolve_lock_time", "s"), // cop tcf.number("cop_proc_avg", "s"), tcf.number("cop_proc_max", "s"), tcf.number("cop_proc_p90", "s"), tcf.number("cop_wait_avg", "s"), tcf.number("cop_wait_max", "s"), tcf.number("cop_wait_p90", "s"), // tcf.number("cop_time", "s"), tcf.number("request_count", "short"), tcf.number("process_keys", "short"), tcf.number("total_keys", "short"), tcf.text("cop_proc_addr"), tcf.text("cop_wait_addr"), // transaction tcf.number("write_keys", "short"), tcf.number("write_size", "bytes"), tcf.number("prewrite_region", "short"), tcf.number("txn_retry", "short"), // rocksdb tcf.number("rocksdb_delete_skipped_count", "short"), tcf.number("rocksdb_key_skipped_count", "short"), tcf.number("rocksdb_block_cache_hit_count", "short"), tcf.number("rocksdb_block_read_count", "short"), tcf.number("rocksdb_block_read_byte", "bytes"), // resource control tcf.number("ru", "short"), // @todo: fix tcf.text("resource_group"), tcf.number("time_queued_by_rc", "s"), tcf.number("ia_remote_read_segment_size", "bytes"), tcf.number("ia_remote_read_segment_wait_time", "s"), ]) }, [tk]) return columns } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/list/filters-with-advanced.tsx ================================================ import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Group } from "@tidbcloud/uikit" import { MemoryStateResetButton, UrlStateSearchInput, UrlStateTimeRangePicker, } from "../../../_shared/state-filters" import { AdvancedFiltersModal } from "./advanced-filters-modal" export function FiltersWithAdvanced() { const { tt } = useTn("slow-query") return ( ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/slow-query/pages/list/filters.tsx ================================================ import { FilterMultiSelect } from "@pingcap-incubator/tidb-dashboard-lib-biz-ui" import { useTn } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { Group, Select, TextInput } from "@tidbcloud/uikit" import { MemoryStateResetButton, UrlStateSearchInput, UrlStateTimeRangePicker, } from "../../../_shared/state-filters" import { useListUrlState } from "../../shared-state/list-url-state" import { useDbsData, useRuGroupsData } from "../../utils/use-data" const SLOW_QUERY_LIMIT = [100, 200, 500, 1000].map((l) => ({ value: `${l}`, label: `Limit ${l}`, })) function LimitSelect() { const { limit, setLimit } = useListUrlState() return ( setMaxSize(Number(v))} data={MAX_SIZE_OPTIONS} /> setWindowsNumber(Number(v))} data={WINDOWS_NUMBER_OPTIONS} /> {tt("SQL Statement Total History Size")} {tt("Window Size x Windows Number")} {total} setInternalQuery(event.currentTarget.checked)} label={tt("Collect Internal Queries")} description={tt( "After enabled, TiDB internal queries will be collected as well.", )} /> )} ) } export function StatementSettingDrawer({ visible, onClose, }: { visible: boolean onClose: () => void }) { const { tt } = useTn("statement") const { data: configData, isLoading } = useStmtConfigData() return ( {isLoading && } {!isLoading && configData && ( )} ) } ================================================ FILE: ui-v2/packages/libs/4-apps/src/statement/shared-state/detail-url-state.ts ================================================ import { useUrlState } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { useCallback } from "react" type DetailUrlState = Partial> export function useDetailUrlState() { const [queryParams, setQueryParams] = useUrlState() const id = queryParams.id ?? "" const plan = queryParams.plan ?? "" const setPlan = useCallback( (newPlan: string) => { setQueryParams({ plan: newPlan || undefined }) }, [setQueryParams], ) return { id, plan, setPlan, } } ================================================ FILE: ui-v2/packages/libs/4-apps/src/statement/shared-state/list-url-state.ts ================================================ import { AdvancedFiltersUrlState, PaginationUrlState, SortUrlState, TimeRangeUrlState, useAdvancedFiltersUrlState, usePaginationUrlState, useResetFiltersState, useSortUrlState, useTimeRangeUrlState, useUrlState, } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { useCallback, useEffect, useMemo } from "react" type ListUrlState = Partial< Record<"dbs" | "ruGroups" | "kinds" | "term" | "cols", string> > & SortUrlState & PaginationUrlState & TimeRangeUrlState & AdvancedFiltersUrlState export function useListUrlState() { const [queryParams, setQueryParams] = useUrlState() const { sortRule, setSortRule } = useSortUrlState("sum_latency") const { pagination, setPagination } = usePaginationUrlState() const { timeRange, setTimeRange } = useTimeRangeUrlState() const { advancedFilters, setAdvancedFilters } = useAdvancedFiltersUrlState() // dbs const dbs = useMemo(() => { const _dbs = queryParams.dbs return _dbs ? _dbs.split(",") : [] }, [queryParams.dbs]) const setDbs = useCallback( (v: string[]) => { setQueryParams({ dbs: v.join(","), pageIndex: undefined }) }, [setQueryParams], ) // ruGroups const ruGroups = useMemo(() => { const _ruGroups = queryParams.ruGroups return _ruGroups ? _ruGroups.split(",") : [] }, [queryParams.ruGroups]) const setRuGroups = useCallback( (v: string[]) => { setQueryParams({ ruGroups: v.join(","), pageIndex: undefined }) }, [setQueryParams], ) // kinds const kinds = useMemo(() => { const _kinds = queryParams.kinds return _kinds ? _kinds.split(",") : [] }, [queryParams.kinds]) const setKinds = useCallback( (newKinds: string[]) => { setQueryParams({ kinds: newKinds.join(","), pageIndex: undefined }) }, [setQueryParams], ) // term const term = decodeURIComponent(queryParams.term ?? "") const setTerm = useCallback( (v?: string) => { setQueryParams({ term: v ? encodeURIComponent(v) : v, pageIndex: undefined, }) }, [setQueryParams], ) // reset filters, not include sort const resetFilters = useCallback(() => { setQueryParams({ from: undefined, to: undefined, dbs: undefined, ruGroups: undefined, kinds: undefined, term: undefined, af: undefined, pageIndex: undefined, }) }, [setQueryParams]) const resetVal = useResetFiltersState((s) => s.resetVal) useEffect(() => { if (resetVal > 0) { resetFilters() } }, [resetVal]) // cols const cols = useMemo(() => { const _cols = queryParams.cols || "digest_text,sum_latency,avg_latency,exec_count,plan_count" return _cols ? _cols.split(",") : [] }, [queryParams.cols]) const setCols = useCallback( (v: string[]) => { setQueryParams({ cols: v.join(",") }) }, [setQueryParams], ) return { timeRange, setTimeRange, dbs, setDbs, ruGroups, setRuGroups, kinds, setKinds, term, setTerm, advancedFilters, setAdvancedFilters, sortRule, setSortRule, pagination, setPagination, cols, setCols, queryParams, setQueryParams, } } ================================================ FILE: ui-v2/packages/libs/4-apps/src/statement/shared-state/memory-state.ts ================================================ import { create } from "zustand" interface SelectedStatementState { statementId: string setSelectedStatement: (statementId: string) => void } export const useSelectedStatementState = create( (set) => ({ statementId: "", setSelectedStatement: (statementId: string) => set({ statementId }), }), ) //------------------------------------------------------------------ interface SettingDrawerState { visible: boolean setVisible: (visible: boolean) => void } export const useSettingDrawerState = create((set) => ({ visible: false, setVisible: (visible: boolean) => set({ visible }), })) ================================================ FILE: ui-v2/packages/libs/4-apps/src/statement/utils/constants.ts ================================================ export const QUICK_RANGES: number[] = [ 5 * 60, // 5 mins 15 * 60, 30 * 60, 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60, 2 * 24 * 60 * 60, 3 * 24 * 60 * 60, // 3 days 7 * 24 * 60 * 60, // 7 days ] ================================================ FILE: ui-v2/packages/libs/4-apps/src/statement/utils/use-data.ts ================================================ import { toTimeRangeValue } from "@pingcap-incubator/tidb-dashboard-lib-utils" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useAppContext } from "../ctx" import { StatementConfigModel } from "../models" import { useDetailUrlState } from "../shared-state/detail-url-state" import { useListUrlState } from "../shared-state/list-url-state" export function useStmtConfigData() { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "statement", "config"], queryFn: () => ctx.api.getStmtConfig(), }) } export function useUpdateStmtConfigData() { const ctx = useAppContext() const queryClient = useQueryClient() return useMutation({ mutationFn: (params: StatementConfigModel) => ctx.api.updateStmtConfig(params), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ctx.ctxId, "statement", "config"], }) }, }) } export function useDbsData() { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "statement", "dbs"], queryFn: () => ctx.api.getDbs(), }) } export function useRuGroupsData() { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "statement", "ru-groups"], queryFn: () => ctx.api.getRuGroups(), }) } export function useStmtKindsData() { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "statement", "stmt-kinds"], queryFn: () => ctx.api.getStmtKinds(), }) } export function useListData() { const ctx = useAppContext() const { timeRange, dbs, ruGroups, kinds, term, advancedFilters, cols, sortRule, pagination, } = useListUrlState() const query = useQuery({ queryKey: [ ctx.ctxId, "statement", "list", timeRange, dbs, ruGroups, kinds, term, advancedFilters, cols, sortRule, pagination, ], queryFn: () => { const tr = toTimeRangeValue(timeRange) return ctx.api.getStmtList({ beginTime: tr[0], endTime: tr[1], dbs, ruGroups, stmtKinds: kinds, term, advancedFilters, fields: cols.filter((c) => c !== "empty"), ...sortRule, ...pagination, }) }, }) return query } export function usePlansListData() { const ctx = useAppContext() const { id } = useDetailUrlState() return useQuery({ queryKey: [ctx.ctxId, "statement", "plans-list", id], queryFn: () => ctx.api.getStmtPlans({ id }), }) } export function usePlanDetailData(plan: string) { const ctx = useAppContext() const { id } = useDetailUrlState() return useQuery({ queryKey: [ctx.ctxId, "statement", "plan-detail", id, plan], queryFn: () => ctx.api.getStmtPlansDetail({ id, plans: [plan], }), }) } // sql plan bind export function usePlanBindSupportData() { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "statement", "plan-bind-support"], queryFn: () => ctx.api.checkPlanBindSupport(), }) } export function usePlanBindStatusData( sqlDigest: string, beginTime: number, endTime: number, ) { const ctx = useAppContext() return useQuery({ queryKey: [ ctx.ctxId, "statement", "plan-bind-status", sqlDigest, beginTime, endTime, ], queryFn: () => ctx.api.getPlanBindStatus({ sqlDigest, beginTime, endTime }), }) } export function useCreatePlanBindData(sqlDigest: string, planDigest: string) { const ctx = useAppContext() const queryClient = useQueryClient() return useMutation({ mutationFn: () => { return ctx.api.createPlanBind({ planDigest }) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ctx.ctxId, "statement", "plan-bind-status", sqlDigest], }) }, }) } export function useDeletePlanBindData(sqlDigest: string) { const ctx = useAppContext() const queryClient = useQueryClient() return useMutation({ mutationFn: () => { return ctx.api.deletePlanBind({ sqlDigest }) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ctx.ctxId, "statement", "plan-bind-status", sqlDigest], }) }, }) } // advanced filters export function useAdvancedFilterNamesData() { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "statement", "advanced-filter-names"], queryFn: () => ctx.api.getAdvancedFilterNames(), }) } export function useAdvancedFilterInfoData(name: string) { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "statement", "advanced-filter-info", name], queryFn: () => ctx.api.getAdvancedFilterInfo({ name }), enabled: !!name, }) } // available fields export function useAvailableFieldsData() { const ctx = useAppContext() return useQuery({ queryKey: [ctx.ctxId, "statement", "available-fields"], queryFn: () => ctx.api.getAvailableFields(), }) } ================================================ FILE: ui-v2/packages/libs/4-apps/tsconfig.json ================================================ { "extends": "../../tsconfig.app.json", "compilerOptions": { "outDir": "./dist" }, "include": ["./src"] } ================================================ FILE: ui-v2/packages/portals/test/CHANGELOG.md ================================================ # test-tidb-dashboard-ui-lib ## 0.13.2 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.20.2 ## 0.13.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.20.1 ## 0.13.0 ### Minor Changes - bump version ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-api-client@0.13.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.20.0 ## 0.12.7 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.19.7 ## 0.12.2 ### Patch Changes - @pingcap-incubator/tidb-dashboard-lib-apps@0.19.2 ## 0.12.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.19.1 ## 0.12.0 ### Minor Changes - refactor: re-exports lib-uitls/lib-charts/lib-primitive-ui/lib-biz-ui from lib-apps ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-api-client@0.12.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.19.0 ## 0.11.22 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.14.1 - @pingcap-incubator/tidb-dashboard-lib-apps@0.18.2 ## 0.11.21 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.12.1 - @pingcap-incubator/tidb-dashboard-lib-apps@0.18.1 ## 0.11.20 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.14.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.12.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.18.0 ## 0.11.19 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.17.6 ## 0.11.18 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.17.5 ## 0.11.17 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.17.4 ## 0.11.16 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.11.2 - @pingcap-incubator/tidb-dashboard-lib-apps@0.17.3 ## 0.11.15 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.11.1 - @pingcap-incubator/tidb-dashboard-lib-apps@0.17.2 ## 0.11.14 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.17.1 ## 0.11.13 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.17.0 ## 0.11.12 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.16.2 ## 0.11.11 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.16.1 ## 0.11.10 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.16.0 ## 0.11.9 ### Patch Changes - Updated dependencies - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.15.1 - @pingcap-incubator/tidb-dashboard-lib-charts@0.13.1 ## 0.11.8 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.15.0 ## 0.11.7 ### Patch Changes - Updated dependencies - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.13.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.14.0 ## 0.11.6 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.12.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.13.0 ## 0.11.5 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.12.0 ## 0.11.4 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.11.4 ## 0.11.3 ### Patch Changes - @pingcap-incubator/tidb-dashboard-lib-apps@0.11.3 ## 0.11.2 ### Patch Changes - @pingcap-incubator/tidb-dashboard-lib-apps@0.11.2 ## 0.11.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.11.1 ## 0.11.0 ### Minor Changes - fix time-range-picker, refine cols-multi-select ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-api-client@0.11.0 - @pingcap-incubator/tidb-dashboard-lib-charts@0.11.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.11.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.11.0 ## 0.10.0 ### Minor Changes - i18n for slow query and statement app ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-api-client@0.10.0 - @pingcap-incubator/tidb-dashboard-lib-charts@0.10.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.10.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.10.0 ## 0.9.1 ### Patch Changes - @pingcap-incubator/tidb-dashboard-lib-apps@0.9.1 ## 0.9.0 ### Minor Changes - upgrade uikit ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-api-client@0.9.0 - @pingcap-incubator/tidb-dashboard-lib-charts@0.9.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.9.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.9.0 ## 0.8.5 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.8.5 ## 0.8.4 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.8.4 ## 0.8.3 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.8.3 ## 0.8.2 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.8.2 ## 0.8.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.8.1 - @pingcap-incubator/tidb-dashboard-lib-apps@0.8.1 ## 0.8.0 ### Minor Changes - support advanced filters for diagnosis ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-api-client@0.8.0 - @pingcap-incubator/tidb-dashboard-lib-charts@0.8.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.8.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.8.0 ## 0.7.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.7.1 ## 0.7.0 ### Minor Changes - refine pagination and sort url state ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-api-client@0.7.0 - @pingcap-incubator/tidb-dashboard-lib-charts@0.7.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.7.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.7.0 ## 0.6.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.6.1 ## 0.6.0 ### Minor Changes - refine ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-api-client@0.6.0 - @pingcap-incubator/tidb-dashboard-lib-charts@0.6.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.6.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.6.0 ## 0.5.4 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.5.4 ## 0.5.3 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.5.3 ## 0.5.2 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.5.2 ## 0.5.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.5.1 ## 0.5.0 ### Minor Changes - refine slow-query and statement apps ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-api-client@0.5.0 - @pingcap-incubator/tidb-dashboard-lib-charts@0.5.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.5.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.5.0 ## 0.4.2 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.4.2 ## 0.4.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.4.1 ## 0.4.0 ### Minor Changes - update uikit ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.4.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.4.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.4.0 ## 0.3.3 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.3.3 ## 0.3.2 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.3.2 ## 0.3.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.3.1 ## 0.3.0 ### Minor Changes - add azores cluster metrics page ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.3.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.3.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.3.0 ## 0.2.1 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.2.1 ## 0.2.0 ### Minor Changes - add metrics azores host page ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.2.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.2.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.2.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.2.0 ## 0.1.0 ### Minor Changes - update i18n, format utils ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-charts@0.1.0 - @pingcap-incubator/tidb-dashboard-lib-utils@0.1.0 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.1.0 - @pingcap-incubator/tidb-dashboard-lib-apps@0.1.0 ## 0.0.12 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.10 - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.13 ## 0.0.11 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.9 - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.12 ## 0.0.10 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.8 - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.11 ## 0.0.9 ### Patch Changes - add i18n - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-utils@0.0.7 - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.7 - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.10 ## 0.0.8 ### Patch Changes - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.9 ## 0.0.7 ### Patch Changes - support dark mode for lib-charts - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.6 - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.8 ## 0.0.6 ### Patch Changes - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.7 ## 0.0.5 ### Patch Changes - refine - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.5 - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.6 ## 0.0.4 ### Patch Changes - refactor metric - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.4 - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.5 ## 0.0.3 ### Patch Changes - upgrade uikit - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.3 - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.4 ## 0.0.2 ### Patch Changes - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.3 ## 0.0.1 ### Patch Changes - first release - Updated dependencies - @pingcap-incubator/tidb-dashboard-lib-primitive-ui@0.0.2 - @pingcap-incubator/tidb-dashboard-lib-apps@0.0.2 ================================================ FILE: ui-v2/packages/portals/test/index.html ================================================ Vite + React + TS
    ================================================ FILE: ui-v2/packages/portals/test/package.json ================================================ { "name": "test-tidb-dashboard-ui-lib", "private": true, "version": "0.13.2", "type": "module", "scripts": { "dev": "vite", "build": "vite build && tsc -b", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "@pingcap-incubator/tidb-dashboard-lib-api-client": "workspace:^", "@pingcap-incubator/tidb-dashboard-lib-apps": "workspace:^", "@tanstack/react-query": "^5.59.16", "@tanstack/react-router": "^1.85.0", "@tidbcloud/uikit": "catalog:", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@eslint/js": "^9.13.0", "@tanstack/router-devtools": "^1.85.0", "@tanstack/router-plugin": "^1.84.4", "@types/node": "^22.10.1", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", "vite": "^5.4.9" } } ================================================ FILE: ui-v2/packages/portals/test/public/swagger.index-advisor.json ================================================ { "swagger": "2.0", "info": { "title": "Cluster APIs for TiDB Cloud", "version": "alpha" }, "tags": [ { "name": "ClusterService" } ], "schemes": ["https"], "consumes": ["application/json"], "produces": ["application/json"], "paths": { "/api/v1/serverless/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/advise_indexes": { "post": { "summary": "Advise indexes on the specified cluster.", "operationId": "AdviseIndexes", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/serverlessClusterAdviseIndexesResp" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/googlerpcStatus" } } }, "parameters": [ { "name": "org_id", "description": "The ID of the org.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "project_id", "description": "The ID of the project.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "cluster_id", "description": "The ID of the cluster.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "payload", "in": "body", "required": true, "schema": { "$ref": "#/definitions/serverlessClusterAdviseIndexesReqPayload" } }, { "name": "with_cloud_admin", "in": "query", "required": false, "type": "boolean" } ], "tags": ["ClusterService"] } }, "/api/v1/serverless/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/apply_advice": { "post": { "summary": "Apply an index advisor record.", "operationId": "ApplyAdvice", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/serverlessApplyIndexAdviceResp" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/googlerpcStatus" } } }, "parameters": [ { "name": "org_id", "description": "The ID of the org.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "project_id", "description": "The ID of the project.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "cluster_id", "description": "The ID of the cluster.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "payload", "in": "body", "required": true, "schema": { "$ref": "#/definitions/serverlessApplyIndexAdviceReqPayload" } }, { "name": "with_cloud_admin", "in": "query", "required": false, "type": "boolean" } ], "tags": ["ClusterService"] } }, "/api/v1/serverless/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/close_advice": { "post": { "summary": "Close an index advisor record.", "operationId": "CloseAdvice", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/serverlessCloseIndexAdviceResp" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/googlerpcStatus" } } }, "parameters": [ { "name": "org_id", "description": "The ID of the org.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "project_id", "description": "The ID of the project.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "cluster_id", "description": "The ID of the cluster.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "payload", "in": "body", "required": true, "schema": { "$ref": "#/definitions/serverlessCloseIndexAdviceReqPayload" } }, { "name": "with_cloud_admin", "in": "query", "required": false, "type": "boolean" } ], "tags": ["ClusterService"] } }, "/api/v1/serverless/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/databases": { "get": { "summary": "List databases of a cluster.", "operationId": "ListDatabases", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/serverlessClusterListDatabasesResp" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/googlerpcStatus" } } }, "parameters": [ { "name": "org_id", "description": "The ID of the org.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "project_id", "description": "The ID of the project.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "cluster_id", "description": "The ID of the cluster.", "in": "path", "required": true, "type": "string", "format": "uint64" } ], "tags": ["ClusterService"] }, "post": { "summary": "Create a database of a cluster.", "operationId": "CreateDatabase", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/serverlessClusterCreateDatabaseResp" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/googlerpcStatus" } } }, "parameters": [ { "name": "org_id", "description": "The ID of the org.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "project_id", "description": "The ID of the project.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "cluster_id", "description": "The ID of the cluster.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "payload", "in": "body", "required": true, "schema": { "$ref": "#/definitions/serverlessClusterCreateDatabaseReqPayload" } } ], "tags": ["ClusterService"] } }, "/api/v1/serverless/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/index_advices": { "get": { "summary": "List index advisor results of a cluster.", "operationId": "ListIndexAdvices", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/serverlessListIndexAdvicesResp" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/googlerpcStatus" } } }, "parameters": [ { "name": "org_id", "description": "The ID of the org.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "project_id", "description": "The ID of the project.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "cluster_id", "description": "The ID of the cluster.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "page_token", "description": "The number of pages.", "in": "query", "required": false, "type": "integer", "format": "int64", "default": 1 }, { "name": "page_size", "description": "The size of a page.", "in": "query", "required": false, "type": "integer", "format": "int64", "default": 10 }, { "name": "state_filter", "description": "The state to filter result.", "in": "query", "required": false, "type": "string" }, { "name": "name_filter", "description": "The name of database or table to filter result.", "in": "query", "required": false, "type": "string" }, { "name": "order_by", "description": "The column used to order result.", "in": "query", "required": false, "type": "string" }, { "name": "desc", "description": "If ordered result should be in descending order.", "in": "query", "required": false, "type": "boolean", "default": "false" } ], "tags": ["ClusterService"] } }, "/api/v1/serverless/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/index_advices/{advice_id}": { "get": { "summary": "Get detail of a index advice.", "operationId": "GetIndexAdvice", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/serverlessGetIndexAdviceResp" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/googlerpcStatus" } } }, "parameters": [ { "name": "org_id", "description": "The ID of the org.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "project_id", "description": "The ID of the project.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "cluster_id", "description": "The ID of the cluster.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "advice_id", "description": "The ID of the advice.", "in": "path", "required": true, "type": "string", "format": "uint64" } ], "tags": ["ClusterService"] } }, "/api/v1/serverless/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/index_advices_summary": { "get": { "summary": "Get summary of open index advices.", "operationId": "GetIndexAdviceSummary", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/serverlessGetIndexAdviceSummaryResp" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/googlerpcStatus" } } }, "parameters": [ { "name": "org_id", "description": "The ID of the org.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "project_id", "description": "The ID of the project.", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "cluster_id", "description": "The ID of the cluster.", "in": "path", "required": true, "type": "string", "format": "uint64" } ], "tags": ["ClusterService"] } } }, "definitions": { "baseBaseResp": { "type": "object", "properties": { "tags": { "type": "object", "additionalProperties": { "type": "string" }, "title": "such as: request_id, trace_id" }, "err_code": { "type": "string", "format": "int64" }, "err_msg": { "type": "string" } } }, "googlerpcStatus": { "type": "object", "properties": { "code": { "type": "integer", "format": "int32" }, "message": { "type": "string" }, "details": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/protobufAny" } } } }, "protobufAny": { "type": "object", "properties": { "@type": { "type": "string" } }, "additionalProperties": {} }, "serverlessApplyIndexAdviceReqPayload": { "type": "object", "properties": { "advice_id": { "type": "string", "format": "uint64", "example": 1, "description": "The ID of the advice record." } }, "required": ["advice_id"] }, "serverlessApplyIndexAdviceResp": { "type": "object", "properties": { "base_resp": { "$ref": "#/definitions/baseBaseResp" } }, "required": ["base_resp"] }, "serverlessCloseIndexAdviceReqPayload": { "type": "object", "properties": { "advice_id": { "type": "string", "format": "uint64", "example": 1, "description": "The ID of the advice record." } }, "required": ["advice_id"] }, "serverlessCloseIndexAdviceResp": { "type": "object", "properties": { "base_resp": { "$ref": "#/definitions/baseBaseResp" } }, "required": ["base_resp"] }, "serverlessClusterAdviseIndexesReqPayload": { "type": "object", "properties": { "database_name": { "type": "string", "example": "helloworld", "description": "The name of the database." }, "queries": { "type": "array", "items": { "type": "string" }, "description": "Queries" } }, "required": ["database_name", "queries"] }, "serverlessClusterAdviseIndexesResp": { "type": "object", "properties": { "text": { "type": "string", "description": "The items of databases in the cluster." }, "base_resp": { "$ref": "#/definitions/baseBaseResp" } }, "required": ["text", "base_resp"] }, "serverlessClusterCreateDatabaseReqPayload": { "type": "object", "properties": { "database_name": { "type": "string", "example": "helloworld", "description": "The name of the database." } }, "required": ["database_name"] }, "serverlessClusterCreateDatabaseResp": { "type": "object", "properties": { "base_resp": { "$ref": "#/definitions/baseBaseResp" } }, "required": ["base_resp"] }, "serverlessClusterListDatabasesResp": { "type": "object", "properties": { "databases": { "type": "array", "items": { "type": "string" }, "description": "The items of databases in the cluster." }, "base_resp": { "$ref": "#/definitions/baseBaseResp" } }, "required": ["databases", "base_resp"] }, "serverlessGetIndexAdviceResp": { "type": "object", "properties": { "advice": { "$ref": "#/definitions/serverlessIndexAdvice" }, "base_resp": { "$ref": "#/definitions/baseBaseResp" } } }, "serverlessGetIndexAdviceSummaryResp": { "type": "object", "properties": { "open_count": { "type": "integer", "format": "int64" }, "improvement": { "type": "number", "format": "double" }, "cost_saving_monthly": { "type": "number", "format": "double" }, "base_resp": { "$ref": "#/definitions/baseBaseResp" } } }, "serverlessIndexAdvice": { "type": "object", "properties": { "id": { "type": "string", "format": "uint64" }, "name": { "type": "string" }, "database": { "type": "string" }, "table": { "type": "string" }, "last_recommend_time": { "type": "string", "format": "date-time" }, "state": { "type": "string" }, "index_statement": { "type": "string" }, "improvement": { "type": "number", "format": "double" }, "index_size": { "type": "number", "format": "double" }, "reason": { "type": "string" }, "top_impacted_queries": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/serverlessIndexAdviceImpact" } }, "state_reason": { "type": "string" }, "cost_saving_monthly": { "type": "number", "format": "double" }, "cost_saving_per_query": { "type": "number", "format": "double" } } }, "serverlessIndexAdviceImpact": { "type": "object", "properties": { "query": { "type": "string" }, "improvement": { "type": "number", "format": "double" } } }, "serverlessListIndexAdvicesResp": { "type": "object", "properties": { "advices": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/serverlessIndexAdvice" }, "description": "The items of index advisor results." }, "total": { "type": "integer", "format": "int64", "description": "total items count considering filter" }, "base_resp": { "$ref": "#/definitions/baseBaseResp" } }, "required": ["advices", "base_resp"] } }, "securityDefinitions": { "bearer": { "type": "apiKey", "description": "Authentication token, prefixed by Bearer: 'Bearer token'", "name": "Authorization", "in": "header" } } } ================================================ FILE: ui-v2/packages/portals/test/src/App.css ================================================ ================================================ FILE: ui-v2/packages/portals/test/src/App.tsx ================================================ import { UiKitThemeProvider } from "@pingcap-incubator/tidb-dashboard-lib-apps/primitive-ui" import { ReactQueryProvider } from "./providers/react-query-provider" import { RouterProvider } from "./router/provider" import "@tidbcloud/uikit/style.css" import "@pingcap-incubator/tidb-dashboard-lib-apps/charts-css" import "./App.css" function App() { return ( ) } export default App ================================================ FILE: ui-v2/packages/portals/test/src/apps/metric/mock-api-app-provider.tsx ================================================ import { V2Metrics, metricsServiceGetClusterMetricData, metricsServiceGetClusterMetricInstance, metricsServiceGetHostMetricData, metricsServiceGetMetrics, metricsServiceGetTopMetricConfig, metricsServiceGetTopMetricData, } from "@pingcap-incubator/tidb-dashboard-lib-api-client" import { AppCtxValue, SinglePanelConfig, } from "@pingcap-incubator/tidb-dashboard-lib-apps/metric" import { PromResultItem, TransformNullValue, delay, } from "@pingcap-incubator/tidb-dashboard-lib-apps/utils" import { useMemo } from "react" import { normalQueryConfig } from "./sample-data/normal-configs" import qpsType from "./sample-data/qps-type.json" const testHostId = import.meta.env.VITE_TEST_HOST_ID const testClusterId = import.meta.env.VITE_TEST_CLUSTER_ID function transformConfigs(metrics: V2Metrics["metrics"]): SinglePanelConfig[] { const configs: SinglePanelConfig[] = [] const groups = [...new Set((metrics || []).map((m) => m.group || ""))] groups.forEach((group) => { const categories = [ ...new Set( (metrics?.filter((m) => m.group === group) || []).map( (m) => m.type || "", ), ), ] categories.forEach((category) => { const charts = metrics?.filter( (m) => m.type === category && m.group === group && m.name, ) configs.push({ group, category, displayName: category, charts: charts?.map((metric) => ({ metricName: metric.name!, title: metric.displayName!, label: metric.description, queries: [], nullValue: TransformNullValue.AS_ZERO, unit: metric.metric?.unit ?? "short", })) ?? [], }) }) }) return configs } export function useCtxValue(): AppCtxValue { let lastKind = "azores-overview" return useMemo( () => ({ ctxId: "metric", api: { getMetricQueriesConfig: async (kind) => { lastKind = kind if (kind === "normal") { return normalQueryConfig } let metrics if (kind === "azores-overview") { metrics = await metricsServiceGetMetrics({ class: "overview", group: "overview", }).then((res) => res.metrics) } else if (kind === "azores-host") { metrics = await metricsServiceGetMetrics({ class: "host", }).then((res) => res.metrics) } else if (kind === "azores-cluster-overview") { metrics = await metricsServiceGetMetrics({ class: "cluster", group: "overview", }).then((res) => res.metrics) } else { // kind === 'azores-cluster' metrics = await metricsServiceGetMetrics({ class: "cluster", }).then((res) => res.metrics) } return transformConfigs(metrics) }, getMetricConfig() { return metricsServiceGetTopMetricConfig().then((res) => ({ delaySec: (res.cacheFlushIntervalInMinutes || 0) * 60, })) }, getMetricLabelValues(params) { return metricsServiceGetClusterMetricInstance( testClusterId, params.metricName, ).then((res) => res.instanceList ?? []) }, getMetricDataByPromQL() { return delay(1000).then( () => qpsType.data.result as unknown as PromResultItem[], ) }, getMetricDataByMetricName: async ({ metricName, beginTime, endTime, step, label, }) => { let queryData if (lastKind === "azores-overview") { queryData = await metricsServiceGetTopMetricData(metricName, { startTime: beginTime.toString(), endTime: endTime.toString(), step: step.toString(), }).then((res) => res.data) } else if (lastKind === "azores-host") { queryData = await metricsServiceGetHostMetricData( testHostId, metricName, { startTime: beginTime.toString(), endTime: endTime.toString(), step: step.toString(), }, ).then((res) => res.data) } else { // lastKind === 'azores-cluster-overview' || lastKind === 'azores-cluster' queryData = await metricsServiceGetClusterMetricData( testClusterId, metricName, { startTime: beginTime.toString(), endTime: endTime.toString(), step: step.toString(), label, }, ).then((res) => res.data) } const ret = queryData?.map((d) => ({ expr: d.expr ?? "", legend: d.legend ?? "", result: (d.result as PromResultItem[]) ?? [], promAddr: d.prometheusAddress ?? "", })) return ret ?? [] }, }, cfg: { title: "", scrapeInterval: 30, }, actions: { openDiagnosis(id) { const [from, to] = id.split(",") window.open(`/statement?from=${from}&to=${to}`, "_blank") }, openHostMonitoring(id) { window.open(`/metrics/azores-host?host_id=${id}`, "_blank") }, }, }), [], ) } ================================================ FILE: ui-v2/packages/portals/test/src/apps/metric/sample-data/normal-configs.ts ================================================ import { SinglePanelConfig } from "@pingcap-incubator/tidb-dashboard-lib-apps/metric" import { TransformNullValue } from "@pingcap-incubator/tidb-dashboard-lib-apps/utils" export const normalQueryConfig: SinglePanelConfig[] = [ { group: "", category: "cluster_status", displayName: "Cluster Status", charts: [ { title: "Query Per Second", label: "The number of SQL statements executed per second, which are collected by SQL types, such as `SELECT`, `INSERT`, and `UPDATE`.", metricName: "tidb_executor_statement_total", queries: [ { promql: `sum(rate(tidb_executor_statement_total{db!=""}[$__rate_interval])) or vector(0)`, legendName: "All", type: "line", }, { promql: `sum(rate(tidb_executor_statement_total{db!=""}[$__rate_interval])) by (type)`, legendName: "{type}", type: "line", }, ], nullValue: TransformNullValue.AS_ZERO, unit: "short", }, { title: "Used Storage Size", label: "The size of the row store and the size of the column store.", metricName: "tikv_store_size_bytes", queries: [ { promql: 'quantile_over_time(0.5, sum(avg by(keyspace_id, region_id) (tikv_store_size_bytes{type="used"}))[$__rate_interval]) or vector(0)', legendName: "Row-based storage", type: "line", }, { promql: 'quantile_over_time(0.5, sum(avg by(keyspace_id, region_id) (tikv_store_size_bytes{type="tiflash_used"}))[$__rate_interval]) or vector(0)', legendName: "Columnar storage", type: "line", }, ], nullValue: TransformNullValue.AS_ZERO, unit: "bytes", }, ], }, { group: "", category: "database_status", displayName: "Database Status", charts: [ { title: "QPS Per DB", label: "The number of SQL statements executed per second on every Database, which are collected by SQL types, such as `SELECT`, `INSERT`, and `UPDATE`.", metricName: "tidb_executor_statement_total", queries: [ { promql: `sum(rate(tidb_executor_statement_total{db!=""}[$__rate_interval])) default 0`, legendName: "All", type: "line", }, { promql: `sum(rate(tidb_executor_statement_total{db!=""}[$__rate_interval])) by (db) >0 and on(db) (sum(rate(tidb_server_handle_query_duration_seconds_count{db!=""}[$__rate_interval])) by (db) >0)`, legendName: "{db}", type: "line", }, ], nullValue: TransformNullValue.AS_ZERO, unit: "short", }, { title: "Average Query Duration Per DB", label: "The duration from receiving a request from the client to a database until the database executes the request and returns the result to the client.", metricName: "tidb_server_handle_query_duration_seconds", queries: [ { promql: 'sum(rate(tidb_server_handle_query_duration_seconds_sum{db!="",sql_type!="internal"}[$__rate_interval])) / sum(rate(tidb_server_handle_query_duration_seconds_count{db!="",sql_type!="internal"}[$__rate_interval])) default 0', legendName: "All", type: "line", }, { promql: '(sum(rate(tidb_server_handle_query_duration_seconds_sum{db!="",sql_type!="internal"}[$__rate_interval])) by (db) / sum(rate(tidb_server_handle_query_duration_seconds_count{db!="",sql_type!="internal"}[$__rate_interval])) by (db) > 0) and on (db) (sum(rate(tidb_executor_statement_total{db!="",sql_type!="internal"}[$__rate_interval])) by (db) >0)', legendName: "{db}", type: "line", }, ], nullValue: TransformNullValue.AS_ZERO, unit: "s", }, ], }, ] ================================================ FILE: ui-v2/packages/portals/test/src/apps/metric/sample-data/qps-type.json ================================================ { "status": "success", "isPartial": false, "data": { "resultType": "matrix", "result": [ { "metric": { "type": "Delete" }, "values": [ [1731595980, "0"], [1731596010, "0"], [1731596040, "0"], [1731596070, "0"], [1731596100, "0"], [1731596130, "0"], [1731596160, "0"], [1731596190, "0"], [1731596220, "0.008333333333333333"], [1731596250, "0.008333333333333333"], [1731596280, "0.008333333333333333"], [1731596310, "0.008333333333333333"], [1731596340, "0"], [1731596370, "0"], [1731596400, "0"], [1731596430, "0"], [1731596460, "0.008333333333333333"], [1731596490, "0.008333333333333333"], [1731596520, "0.008333333333333333"], [1731596550, "0.008333333333333333"], [1731596580, "0"], [1731596610, "0"], [1731596640, "0"], [1731596670, "0"], [1731596700, "0"], [1731596730, "0"], [1731596760, "0"], [1731596790, "0"], [1731596820, "0"], [1731596850, "0"], [1731596880, "0.008333333333333333"], [1731596910, "0.008333333333333333"], [1731596940, "0.008333333333333333"], [1731596970, "0.008333333333333333"], [1731597000, "0"], [1731597030, "0"], [1731597060, "0"], [1731597090, "0"], [1731597120, "0"], [1731597150, "0"], [1731597180, "0"], [1731597210, "0"], [1731597240, "0"], [1731597270, "0"], [1731597300, "0.008333333333333333"], [1731597330, "0.008333333333333333"], [1731597360, "0.008333333333333333"], [1731597390, "0.008333333333333333"], [1731597420, "0"], [1731597450, "0"], [1731597480, "0"], [1731597510, "0"], [1731597540, "0"], [1731597570, "0"], [1731597600, "0"], [1731597630, "0"], [1731597660, "0"], [1731597690, "0"], [1731597720, "0.2916666666666667"], [1731597750, "0.2916666666666667"], [1731597780, "0.2916666666666667"], [1731597810, "0.2916666666666667"], [1731597840, "0"], [1731597870, "0"], [1731597900, "0"], [1731597930, "0"], [1731597960, "0"], [1731597990, "0"], [1731598020, "0"], [1731598050, "0"], [1731598080, "0"], [1731598110, "0"], [1731598140, "0.008333333333333333"], [1731598170, "0.008333333333333333"], [1731598200, "0.008333333333333333"], [1731598230, "0.008333333333333333"], [1731598260, "0"], [1731598290, "0"], [1731598320, "0"], [1731598350, "0"], [1731598380, "0"], [1731598410, "0"], [1731598440, "0"], [1731598470, "0"], [1731598500, "0"], [1731598530, "0"], [1731598560, "0.008333333333333333"], [1731598590, "0.008333333333333333"], [1731598620, "0.008333333333333333"], [1731598650, "0.008333333333333333"], [1731598680, "0"], [1731598710, "0"], [1731598740, "0"], [1731598770, "0"], [1731598800, "0"], [1731598830, "0"], [1731598860, "0"], [1731598890, "0"], [1731598920, "0"], [1731598950, "0"], [1731598980, "0.008333333333333333"], [1731599010, "0.008333333333333333"], [1731599040, "0.008333333333333333"], [1731599070, "0.008333333333333333"], [1731599100, "0"], [1731599130, "0"], [1731599160, "0"], [1731599190, "0"], [1731599220, "0"], [1731599250, "0"], [1731599280, "0"], [1731599310, "0"], [1731599340, "0"], [1731599370, "0"], [1731599400, "0.008333333333333333"], [1731599430, "0.008333333333333333"], [1731599460, "0.008333333333333333"], [1731599490, "0.008333333333333333"], [1731599520, "0"], [1731599550, "0"], [1731599580, "0"], [1731599610, "0"] ] }, { "metric": { "type": "Insert" }, "values": [ [1731595980, "17.508333333333333"], [1731596010, "17.508333333333333"], [1731596040, "16.641666666666666"], [1731596070, "16.641666666666666"], [1731596100, "15.674999999999999"], [1731596130, "15.674999999999999"], [1731596160, "16.483333333333334"], [1731596190, "16.483333333333334"], [1731596220, "17.741666666666667"], [1731596250, "17.741666666666667"], [1731596280, "16.616666666666667"], [1731596310, "16.616666666666667"], [1731596340, "15.9"], [1731596370, "15.9"], [1731596400, "16.116666666666667"], [1731596430, "16.116666666666667"], [1731596460, "16.075"], [1731596490, "16.075"], [1731596520, "16.566666666666666"], [1731596550, "16.566666666666666"], [1731596580, "16.791666666666664"], [1731596610, "16.791666666666664"], [1731596640, "16.2"], [1731596670, "16.2"], [1731596700, "17.183333333333334"], [1731596730, "17.183333333333334"], [1731596760, "17.158333333333335"], [1731596790, "17.158333333333335"], [1731596820, "16.333333333333332"], [1731596850, "16.333333333333332"], [1731596880, "16.466666666666665"], [1731596910, "16.466666666666665"], [1731596940, "16.966666666666665"], [1731596970, "16.966666666666665"], [1731597000, "17.416666666666664"], [1731597030, "17.416666666666664"], [1731597060, "16.741666666666667"], [1731597090, "16.741666666666667"], [1731597120, "17.016666666666666"], [1731597150, "17.016666666666666"], [1731597180, "17.95"], [1731597210, "17.95"], [1731597240, "18.141666666666666"], [1731597270, "18.141666666666666"], [1731597300, "18.224999999999998"], [1731597330, "18.224999999999998"], [1731597360, "18.599999999999998"], [1731597390, "18.599999999999998"], [1731597420, "18.633333333333333"], [1731597450, "18.633333333333333"], [1731597480, "18.25"], [1731597510, "18.25"], [1731597540, "18.108333333333334"], [1731597570, "18.108333333333334"], [1731597600, "17.883333333333333"], [1731597630, "17.883333333333333"], [1731597660, "17.391666666666666"], [1731597690, "17.391666666666666"], [1731597720, "17.766666666666666"], [1731597750, "17.766666666666666"], [1731597780, "18.266666666666666"], [1731597810, "18.266666666666666"], [1731597840, "18.091666666666665"], [1731597870, "18.091666666666665"], [1731597900, "18.091666666666665"], [1731597930, "18.091666666666665"], [1731597960, "17.666666666666668"], [1731597990, "17.666666666666668"], [1731598020, "18.133333333333333"], [1731598050, "18.133333333333333"], [1731598080, "17.833333333333332"], [1731598110, "17.833333333333332"], [1731598140, "16.966666666666665"], [1731598170, "16.966666666666665"], [1731598200, "18.241666666666667"], [1731598230, "18.241666666666667"], [1731598260, "18.09166666666667"], [1731598290, "18.09166666666667"], [1731598320, "17.383333333333333"], [1731598350, "17.383333333333333"], [1731598380, "16.84166666666667"], [1731598410, "16.84166666666667"], [1731598440, "16.583333333333336"], [1731598470, "16.583333333333336"], [1731598500, "17.208333333333332"], [1731598530, "17.208333333333332"], [1731598560, "17.183333333333334"], [1731598590, "17.183333333333334"], [1731598620, "15.991666666666667"], [1731598650, "15.991666666666667"], [1731598680, "16.28333333333333"], [1731598710, "16.28333333333333"], [1731598740, "18.216666666666665"], [1731598770, "18.216666666666665"], [1731598800, "20.116666666666667"], [1731598830, "20.116666666666667"], [1731598860, "20.208333333333332"], [1731598890, "20.208333333333332"], [1731598920, "19.258333333333333"], [1731598950, "19.258333333333333"], [1731598980, "19.4"], [1731599010, "19.4"], [1731599040, "19.608333333333334"], [1731599070, "19.608333333333334"], [1731599100, "18.933333333333334"], [1731599130, "18.933333333333334"], [1731599160, "18.491666666666667"], [1731599190, "18.491666666666667"], [1731599220, "18.425"], [1731599250, "18.425"], [1731599280, "17.883333333333333"], [1731599310, "17.883333333333333"], [1731599340, "17.616666666666667"], [1731599370, "17.616666666666667"], [1731599400, "17.758333333333333"], [1731599430, "17.758333333333333"], [1731599460, "18.941666666666666"], [1731599490, "18.941666666666666"], [1731599520, "19.041666666666668"], [1731599550, "19.041666666666668"], [1731599580, "19.041666666666668"], [1731599610, "19.041666666666668"] ] }, { "metric": { "type": "Select" }, "values": [ [1731595980, "12.216666666666667"], [1731596010, "12.216666666666667"], [1731596040, "11.416666666666666"], [1731596070, "11.416666666666666"], [1731596100, "12.433333333333334"], [1731596130, "12.433333333333334"], [1731596160, "12.791666666666668"], [1731596190, "12.791666666666668"], [1731596220, "11.833333333333334"], [1731596250, "11.833333333333334"], [1731596280, "11.741666666666669"], [1731596310, "11.741666666666669"], [1731596340, "12.066666666666666"], [1731596370, "12.066666666666666"], [1731596400, "11.725"], [1731596430, "11.725"], [1731596460, "11.591666666666667"], [1731596490, "11.591666666666667"], [1731596520, "12.174999999999999"], [1731596550, "12.174999999999999"], [1731596580, "13.008333333333333"], [1731596610, "13.008333333333333"], [1731596640, "13.433333333333332"], [1731596670, "13.433333333333332"], [1731596700, "13.166666666666666"], [1731596730, "13.166666666666666"], [1731596760, "11.666666666666666"], [1731596790, "11.666666666666666"], [1731596820, "11.333333333333332"], [1731596850, "11.333333333333332"], [1731596880, "13.325"], [1731596910, "13.325"], [1731596940, "14.033333333333333"], [1731596970, "14.033333333333333"], [1731597000, "14.241666666666667"], [1731597030, "14.241666666666667"], [1731597060, "15.124999999999998"], [1731597090, "15.124999999999998"], [1731597120, "15.758333333333335"], [1731597150, "15.758333333333335"], [1731597180, "20.291666666666664"], [1731597210, "20.291666666666664"], [1731597240, "20.299999999999997"], [1731597270, "20.299999999999997"], [1731597300, "15.983333333333334"], [1731597330, "15.983333333333334"], [1731597360, "15.75"], [1731597390, "15.75"], [1731597420, "14.416666666666666"], [1731597450, "14.416666666666666"], [1731597480, "13.616666666666667"], [1731597510, "13.616666666666667"], [1731597540, "14.2"], [1731597570, "14.2"], [1731597600, "15.424999999999999"], [1731597630, "15.424999999999999"], [1731597660, "14.6"], [1731597690, "14.6"], [1731597720, "13.825"], [1731597750, "13.825"], [1731597780, "14.75"], [1731597810, "14.75"], [1731597840, "14.208333333333332"], [1731597870, "14.208333333333332"], [1731597900, "14.133333333333333"], [1731597930, "14.133333333333333"], [1731597960, "15.191666666666668"], [1731597990, "15.191666666666668"], [1731598020, "14.933333333333334"], [1731598050, "14.933333333333334"], [1731598080, "14.541666666666668"], [1731598110, "14.541666666666668"], [1731598140, "13.858333333333334"], [1731598170, "13.858333333333334"], [1731598200, "13.466666666666667"], [1731598230, "13.466666666666667"], [1731598260, "13.241666666666667"], [1731598290, "13.241666666666667"], [1731598320, "13.316666666666666"], [1731598350, "13.316666666666666"], [1731598380, "12.716666666666667"], [1731598410, "12.716666666666667"], [1731598440, "12.899999999999999"], [1731598470, "12.899999999999999"], [1731598500, "14.216666666666669"], [1731598530, "14.216666666666669"], [1731598560, "13.966666666666665"], [1731598590, "13.966666666666665"], [1731598620, "13.316666666666666"], [1731598650, "13.316666666666666"], [1731598680, "13.558333333333334"], [1731598710, "13.558333333333334"], [1731598740, "13.808333333333334"], [1731598770, "13.808333333333334"], [1731598800, "13.833333333333332"], [1731598830, "13.833333333333332"], [1731598860, "12.95"], [1731598890, "12.95"], [1731598920, "12.375"], [1731598950, "12.375"], [1731598980, "11.991666666666667"], [1731599010, "11.991666666666667"], [1731599040, "12.891666666666667"], [1731599070, "12.891666666666667"], [1731599100, "13.508333333333335"], [1731599130, "13.508333333333335"], [1731599160, "13.008333333333333"], [1731599190, "13.008333333333333"], [1731599220, "13.691666666666666"], [1731599250, "13.691666666666666"], [1731599280, "13.8"], [1731599310, "13.8"], [1731599340, "13.208333333333334"], [1731599370, "13.208333333333334"], [1731599400, "12.983333333333334"], [1731599430, "12.983333333333334"], [1731599460, "13.233333333333333"], [1731599490, "13.233333333333333"], [1731599520, "13.216666666666667"], [1731599550, "13.216666666666667"], [1731599580, "13.216666666666667"], [1731599610, "13.216666666666667"] ] }, { "metric": { "type": "Show" }, "values": [ [1731597060, "0"], [1731597090, "0"], [1731597120, "0"], [1731597150, "0"], [1731597180, "0"], [1731597210, "0"], [1731597240, "0"], [1731597270, "0"], [1731597300, "0"], [1731597330, "0"], [1731597360, "0"], [1731597390, "0"], [1731597420, "0"], [1731597450, "0"], [1731597480, "0"], [1731597510, "0"], [1731597540, "0"], [1731597570, "0"], [1731597600, "0"], [1731597630, "0"], [1731597660, "0"], [1731597690, "0"], [1731597720, "0"], [1731597750, "0"], [1731597780, "0"], [1731597810, "0"], [1731597840, "0"], [1731597870, "0"], [1731597900, "0"], [1731597930, "0"], [1731597960, "0.05"], [1731597990, "0.05"], [1731598020, "0.025"], [1731598050, "0.025"], [1731598080, "0"], [1731598110, "0"], [1731598140, "0"], [1731598170, "0"], [1731598200, "0"], [1731598230, "0"], [1731598260, "0"], [1731598290, "0"], [1731598320, "0"], [1731598350, "0"], [1731598380, "0"], [1731598410, "0"], [1731598440, "0"], [1731598470, "0"], [1731598500, "0"], [1731598530, "0"], [1731598560, "0"], [1731598590, "0"], [1731598620, "0"], [1731598650, "0"], [1731598680, "0"], [1731598710, "0"], [1731598740, "0"], [1731598770, "0"], [1731598800, "0"], [1731598830, "0"], [1731598860, "0"], [1731598890, "0"], [1731598920, "0"], [1731598950, "0"], [1731598980, "0"], [1731599010, "0"], [1731599040, "0"], [1731599070, "0"], [1731599100, "0"], [1731599130, "0"], [1731599160, "0"], [1731599190, "0"], [1731599220, "0"], [1731599250, "0"], [1731599280, "0"], [1731599310, "0"], [1731599340, "0"], [1731599370, "0"], [1731599400, "0"], [1731599430, "0"], [1731599460, "0"], [1731599490, "0"], [1731599520, "0"], [1731599550, "0"], [1731599580, "0"], [1731599610, "0"] ] }, { "metric": { "type": "Update" }, "values": [ [1731595980, "0.03333333333333333"], [1731596010, "0.03333333333333333"], [1731596040, "0.03333333333333333"], [1731596070, "0.03333333333333333"], [1731596100, "0.03333333333333333"], [1731596130, "0.03333333333333333"], [1731596160, "0.03333333333333333"], [1731596190, "0.03333333333333333"], [1731596220, "0.03333333333333333"], [1731596250, "0.03333333333333333"], [1731596280, "0.025"], [1731596310, "0.025"], [1731596340, "0.03333333333333333"], [1731596370, "0.03333333333333333"], [1731596400, "0.04166666666666667"], [1731596430, "0.04166666666666667"], [1731596460, "0.06666666666666667"], [1731596490, "0.06666666666666667"], [1731596520, "0.125"], [1731596550, "0.125"], [1731596580, "0.13333333333333333"], [1731596610, "0.13333333333333333"], [1731596640, "0.125"], [1731596670, "0.125"], [1731596700, "0.125"], [1731596730, "0.125"], [1731596760, "0.125"], [1731596790, "0.125"], [1731596820, "0.09999999999999999"], [1731596850, "0.09999999999999999"], [1731596880, "0.10833333333333332"], [1731596910, "0.10833333333333332"], [1731596940, "0.13333333333333333"], [1731596970, "0.13333333333333333"], [1731597000, "0.10833333333333332"], [1731597030, "0.10833333333333332"], [1731597060, "0.11666666666666667"], [1731597090, "0.11666666666666667"], [1731597120, "0.15833333333333333"], [1731597150, "0.15833333333333333"], [1731597180, "0.15"], [1731597210, "0.15"], [1731597240, "0.09999999999999999"], [1731597270, "0.09999999999999999"], [1731597300, "0.09999999999999999"], [1731597330, "0.09999999999999999"], [1731597360, "0.11666666666666667"], [1731597390, "0.11666666666666667"], [1731597420, "0.09166666666666666"], [1731597450, "0.09166666666666666"], [1731597480, "0.075"], [1731597510, "0.075"], [1731597540, "0.09999999999999999"], [1731597570, "0.09999999999999999"], [1731597600, "0.125"], [1731597630, "0.125"], [1731597660, "0.125"], [1731597690, "0.125"], [1731597720, "0.14166666666666666"], [1731597750, "0.14166666666666666"], [1731597780, "0.15833333333333333"], [1731597810, "0.15833333333333333"], [1731597840, "0.08333333333333333"], [1731597870, "0.08333333333333333"], [1731597900, "0.03333333333333333"], [1731597930, "0.03333333333333333"], [1731597960, "0.08333333333333333"], [1731597990, "0.08333333333333333"], [1731598020, "0.06666666666666667"], [1731598050, "0.06666666666666667"], [1731598080, "0.03333333333333333"], [1731598110, "0.03333333333333333"], [1731598140, "0.03333333333333333"], [1731598170, "0.03333333333333333"], [1731598200, "0.04166666666666667"], [1731598230, "0.04166666666666667"], [1731598260, "0.025"], [1731598290, "0.025"], [1731598320, "0.03333333333333333"], [1731598350, "0.03333333333333333"], [1731598380, "0.04166666666666667"], [1731598410, "0.04166666666666667"], [1731598440, "0.025"], [1731598470, "0.025"], [1731598500, "0.03333333333333333"], [1731598530, "0.03333333333333333"], [1731598560, "0.04166666666666667"], [1731598590, "0.04166666666666667"], [1731598620, "0.11666666666666667"], [1731598650, "0.11666666666666667"], [1731598680, "0.13333333333333333"], [1731598710, "0.13333333333333333"], [1731598740, "0.05"], [1731598770, "0.05"], [1731598800, "0.03333333333333333"], [1731598830, "0.03333333333333333"], [1731598860, "0.03333333333333333"], [1731598890, "0.03333333333333333"], [1731598920, "0.03333333333333333"], [1731598950, "0.03333333333333333"], [1731598980, "0.04166666666666667"], [1731599010, "0.04166666666666667"], [1731599040, "0.03333333333333333"], [1731599070, "0.03333333333333333"], [1731599100, "0.03333333333333333"], [1731599130, "0.03333333333333333"], [1731599160, "0.04166666666666667"], [1731599190, "0.04166666666666667"], [1731599220, "0.025"], [1731599250, "0.025"], [1731599280, "0.03333333333333333"], [1731599310, "0.03333333333333333"], [1731599340, "0.04166666666666667"], [1731599370, "0.04166666666666667"], [1731599400, "0.03333333333333333"], [1731599430, "0.03333333333333333"], [1731599460, "0.03333333333333333"], [1731599490, "0.03333333333333333"], [1731599520, "0.03333333333333333"], [1731599550, "0.03333333333333333"], [1731599580, "0.03333333333333333"], [1731599610, "0.03333333333333333"] ] }, { "metric": { "type": "Use" }, "values": [ [1731595980, "1.125"], [1731596010, "1.125"], [1731596040, "1.2583333333333333"], [1731596070, "1.2583333333333333"], [1731596100, "1.2833333333333332"], [1731596130, "1.2833333333333332"], [1731596160, "1.0833333333333333"], [1731596190, "1.0833333333333333"], [1731596220, "1.0916666666666666"], [1731596250, "1.0916666666666666"], [1731596280, "1.1333333333333333"], [1731596310, "1.1333333333333333"], [1731596340, "1.1583333333333332"], [1731596370, "1.1583333333333332"], [1731596400, "1.125"], [1731596430, "1.125"], [1731596460, "1.3583333333333332"], [1731596490, "1.3583333333333332"], [1731596520, "1.5"], [1731596550, "1.5"], [1731596580, "1.4166666666666665"], [1731596610, "1.4166666666666665"], [1731596640, "1.2916666666666665"], [1731596670, "1.2916666666666665"], [1731596700, "1.0833333333333333"], [1731596730, "1.0833333333333333"], [1731596760, "1.1166666666666667"], [1731596790, "1.1166666666666667"], [1731596820, "1.275"], [1731596850, "1.275"], [1731596880, "1.375"], [1731596910, "1.375"], [1731596940, "1.4416666666666667"], [1731596970, "1.4416666666666667"], [1731597000, "1.4583333333333333"], [1731597030, "1.4583333333333333"], [1731597060, "1.4333333333333333"], [1731597090, "1.4333333333333333"], [1731597120, "1.5916666666666668"], [1731597150, "1.5916666666666668"], [1731597180, "1.7916666666666665"], [1731597210, "1.7916666666666665"], [1731597240, "1.5583333333333333"], [1731597270, "1.5583333333333333"], [1731597300, "1.4416666666666667"], [1731597330, "1.4416666666666667"], [1731597360, "1.4833333333333332"], [1731597390, "1.4833333333333332"], [1731597420, "1.3166666666666667"], [1731597450, "1.3166666666666667"], [1731597480, "1.0333333333333332"], [1731597510, "1.0333333333333332"], [1731597540, "1.2333333333333332"], [1731597570, "1.2333333333333332"], [1731597600, "1.7166666666666666"], [1731597630, "1.7166666666666666"], [1731597660, "1.5499999999999998"], [1731597690, "1.5499999999999998"], [1731597720, "1.4083333333333332"], [1731597750, "1.4083333333333332"], [1731597780, "1.5416666666666665"], [1731597810, "1.5416666666666665"], [1731597840, "1.5333333333333332"], [1731597870, "1.5333333333333332"], [1731597900, "1.4"], [1731597930, "1.4"], [1731597960, "1.425"], [1731597990, "1.425"], [1731598020, "1.6"], [1731598050, "1.6"], [1731598080, "1.5999999999999999"], [1731598110, "1.5999999999999999"], [1731598140, "1.5666666666666667"], [1731598170, "1.5666666666666667"], [1731598200, "1.375"], [1731598230, "1.375"], [1731598260, "1.1083333333333332"], [1731598290, "1.1083333333333332"], [1731598320, "1.1416666666666666"], [1731598350, "1.1416666666666666"], [1731598380, "1.2916666666666665"], [1731598410, "1.2916666666666665"], [1731598440, "1.275"], [1731598470, "1.275"], [1731598500, "1.0583333333333333"], [1731598530, "1.0583333333333333"], [1731598560, "1.0333333333333332"], [1731598590, "1.0333333333333332"], [1731598620, "1.125"], [1731598650, "1.125"], [1731598680, "1.1416666666666666"], [1731598710, "1.1416666666666666"], [1731598740, "1.1916666666666667"], [1731598770, "1.1916666666666667"], [1731598800, "1.2666666666666666"], [1731598830, "1.2666666666666666"], [1731598860, "1.5"], [1731598890, "1.5"], [1731598920, "1.4916666666666667"], [1731598950, "1.4916666666666667"], [1731598980, "1.15"], [1731599010, "1.15"], [1731599040, "0.9500000000000001"], [1731599070, "0.9500000000000001"], [1731599100, "0.9916666666666667"], [1731599130, "0.9916666666666667"], [1731599160, "0.9666666666666667"], [1731599190, "0.9666666666666667"], [1731599220, "1.025"], [1731599250, "1.025"], [1731599280, "1.3166666666666667"], [1731599310, "1.3166666666666667"], [1731599340, "1.275"], [1731599370, "1.275"], [1731599400, "1.0583333333333333"], [1731599430, "1.0583333333333333"], [1731599460, "1.0583333333333333"], [1731599490, "1.0583333333333333"], [1731599520, "1.0583333333333333"], [1731599550, "1.0583333333333333"], [1731599580, "1.0583333333333333"], [1731599610, "1.0583333333333333"] ] } ] }, "stats": { "seriesFetched": "39", "executionTimeMsec": 14 } } ================================================ FILE: ui-v2/packages/portals/test/src/apps/slow-query/mock-api-app-provider.tsx ================================================ import { DiagnosisServiceAddSqlLimitBodyAction, diagnosisServiceAddSqlLimit, diagnosisServiceCheckSqlLimitSupport, diagnosisServiceGetResourceGroupList, diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfo, diagnosisServiceGetSlowQueryAvailableAdvancedFilters, diagnosisServiceGetSlowQueryAvailableFields, diagnosisServiceGetSlowQueryDetail, diagnosisServiceGetSlowQueryList, diagnosisServiceGetSqlLimitList, diagnosisServiceRemoveSqlLimit, } from "@pingcap-incubator/tidb-dashboard-lib-api-client" import { AppCtxValue } from "@pingcap-incubator/tidb-dashboard-lib-apps/slow-query" import { useNavigate } from "@tanstack/react-router" import { useMemo } from "react" declare global { interface Window { preUrl?: string[] } } const clusterId = import.meta.env.VITE_TEST_CLUSTER_ID export function useCtxValue(): AppCtxValue { const navigate = useNavigate() return useMemo( () => ({ ctxId: `slow-query-${clusterId}`, api: { getDbs() { return diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfo( clusterId, "db", { skipGlobalErrorHandling: true, }, ).then((res) => res.valueList ?? []) }, getRuGroups() { return diagnosisServiceGetResourceGroupList(clusterId, { skipGlobalErrorHandling: true, }).then((res) => (res.resourceGroups ?? []).map((r) => r.name || "")) }, getAdvancedFilterNames() { return diagnosisServiceGetSlowQueryAvailableAdvancedFilters( clusterId, ).then((res) => res.filters ?? []) }, getAdvancedFilterInfo(params) { return diagnosisServiceGetSlowQueryAvailableAdvancedFilterInfo( clusterId, params.name, ).then((res) => ({ name: res.name ?? "", type: res.type ?? "string", unit: res.unit ?? "", values: res.valueList ?? [], })) }, getAvailableFields() { return diagnosisServiceGetSlowQueryAvailableFields(clusterId).then( (res) => res.fields ?? [], ) }, getSlowQueries(params) { const advancedFiltersStrArr: string[] = [] for (const filter of params.advancedFilters) { const filterValue = filter.values .map((v) => encodeURIComponent(v)) .join(",") if (filterValue !== "") { advancedFiltersStrArr.push( `${filter.name} ${filter.operator} ${filterValue}`, ) } } const fieldsStr = params.fields.includes("all") ? "*" : params.fields.join(",") return diagnosisServiceGetSlowQueryList(clusterId, { beginTime: params.beginTime + "", endTime: params.endTime + "", db: params.dbs, text: params.term, orderBy: params.orderBy, isDesc: params.desc, advancedFilter: advancedFiltersStrArr, fields: fieldsStr, pageSize: params.pageSize, skip: params.pageSize * params.pageIndex, }).then((res) => ({ total: res.totalSize ?? 0, items: res.data ?? [], })) }, getSlowQuery(params: { id: string }) { const [digest, connectionId, timestamp] = params.id.split(",") return diagnosisServiceGetSlowQueryDetail(clusterId, digest, { connectionId, timestamp: Number(timestamp), }).then((d) => { if (d.binary_plan_text) { d.plan = d.binary_plan_text delete d.binary_plan_text } return d }) }, // sql limit checkSqlLimitSupport() { return diagnosisServiceCheckSqlLimitSupport(clusterId).then( (res) => ({ is_support: res.isSupport! }), ) }, getSqlLimitStatus(params) { return diagnosisServiceGetSqlLimitList(clusterId, { watchText: params.watchText, }).then((res) => (res.data || []).map((d) => ({ id: d.id ?? "", ru_group: d.resourceGroupName ?? "", action: d.action ?? "", start_time: d.startTime ?? "", })), ) }, createSqlLimit(params) { return diagnosisServiceAddSqlLimit(clusterId, { watchText: params.watchText, resourceGroup: params.ruGroup, action: params.action as DiagnosisServiceAddSqlLimitBodyAction, }).then(() => {}) }, deleteSqlLimit(params) { return diagnosisServiceRemoveSqlLimit(clusterId, params).then( () => {}, ) }, // sql history getHistoryMetricNames() { return Promise.resolve([ { name: "query_time", unit: "s" }, { name: "memory_max", unit: "bytes" }, { name: "disk_max", unit: "bytes" }, { name: "parse_time", unit: "s" }, { name: "compile_time", unit: "s" }, { name: "rewrite_time", unit: "s" }, { name: "preproc_subqueries_time", unit: "s" }, { name: "optimize_time", unit: "s" }, { name: "cop_time", unit: "s" }, { name: "wait_time", unit: "s" }, { name: "process_time", unit: "s" }, { name: "local_latch_wait_time", unit: "s" }, { name: "lock_keys_time", unit: "s" }, { name: "resolve_lock_time", unit: "s" }, { name: "wait_ts", unit: "s" }, { name: "get_commit_ts_time", unit: "s" }, { name: "prewrite_time", unit: "s" }, { name: "commit_time", unit: "s" }, { name: "backoff_time", unit: "s" }, { name: "commit_backoff_time", unit: "s" }, { name: "exec_retry_time", unit: "s" }, { name: "write_sql_response_total", unit: "s" }, { name: "wait_prewrite_binlog_time", unit: "s" }, ]) }, getHistoryMetricData(params) { return diagnosisServiceGetSlowQueryList(clusterId, { beginTime: params.beginTime + "", endTime: params.endTime + "", orderBy: "timestamp", isDesc: false, pageSize: 1000, fields: ["timestamp", params.metricName].join(","), advancedFilter: [`digest = ${params.sqlDigest}`], }).then((res) => (res.data ?? []).map((d) => [ d.timestamp! * 1000, d[params.metricName as keyof typeof d]! as number, ]), ) }, }, cfg: { title: "", }, actions: { openDetail: (id: string, newTab: boolean) => { window.preUrl = [window.location.pathname + window.location.search] if (newTab) { window.open(`/slow-query/detail?id=${id}`, "_blank") } else { navigate({ to: `/slow-query/detail?id=${id}` }) } }, backToList: () => { const preUrl = window.preUrl?.pop() navigate({ to: preUrl || "/slow-query" }) }, openStatement(id) { const [from, to, sqlDigest, dbName] = id.split(",") const fullUrl = `/statement?from=${from}&to=${to}&af=digest,${encodeURIComponent("=")},${sqlDigest};schema_name,in,${dbName}` window.open(fullUrl, "_blank") }, }, }), [navigate], ) } ================================================ FILE: ui-v2/packages/portals/test/src/apps/statement/mock-api-app-provider.tsx ================================================ import { DiagnosisServiceAddSqlLimitBodyAction, diagnosisServiceAddSqlLimit, diagnosisServiceBindSqlPlan, diagnosisServiceCheckSqlLimitSupport, diagnosisServiceCheckSqlPlanSupport, diagnosisServiceGetResourceGroupList, diagnosisServiceGetSqlLimitList, diagnosisServiceGetSqlPlanBindingList, diagnosisServiceGetSqlPlanList, diagnosisServiceGetTopSqlAvailableAdvancedFilterInfo, diagnosisServiceGetTopSqlAvailableAdvancedFilters, diagnosisServiceGetTopSqlAvailableFields, diagnosisServiceGetTopSqlConfigs, diagnosisServiceGetTopSqlDetail, diagnosisServiceGetTopSqlList, diagnosisServiceRemoveSqlLimit, diagnosisServiceUnbindSqlPlan, diagnosisServiceUpdateTopSqlConfigs, } from "@pingcap-incubator/tidb-dashboard-lib-api-client" import { AppCtxValue } from "@pingcap-incubator/tidb-dashboard-lib-apps/statement" import { useNavigate } from "@tanstack/react-router" import { useMemo } from "react" import { STMT_TYPES } from "./stmt-types" declare global { interface Window { preUrl?: string[] } } const clusterId = import.meta.env.VITE_TEST_CLUSTER_ID export function useCtxValue(): AppCtxValue { const navigate = useNavigate() return useMemo( () => ({ ctxId: `statement-${clusterId}`, api: { getStmtKinds() { return Promise.resolve(STMT_TYPES) }, getDbs() { return diagnosisServiceGetTopSqlAvailableAdvancedFilterInfo( clusterId, "schema_name", { skipGlobalErrorHandling: true, }, ).then((res) => res.valueList ?? []) }, getRuGroups() { return diagnosisServiceGetResourceGroupList(clusterId, { skipGlobalErrorHandling: true, }).then((res) => (res.resourceGroups ?? []).map((r) => r.name || "")) }, getAdvancedFilterNames() { return diagnosisServiceGetTopSqlAvailableAdvancedFilters( clusterId, ).then((res) => res.filters ?? []) }, getAdvancedFilterInfo(params) { if (params.name === "stmt_type") { return Promise.resolve({ name: "stmt_type", type: "string", unit: "", values: STMT_TYPES, }) } return diagnosisServiceGetTopSqlAvailableAdvancedFilterInfo( clusterId, params.name, ).then((res) => ({ name: res.name ?? "", type: res.type ?? "string", unit: res.unit ?? "", values: res.valueList ?? [], })) }, getAvailableFields() { return diagnosisServiceGetTopSqlAvailableFields(clusterId).then( (res) => res.fields ?? [], ) }, // config getStmtConfig() { return diagnosisServiceGetTopSqlConfigs(clusterId).then((res) => ({ enable: res.enable!, max_size: res.maxSize!, refresh_interval: res.refreshInterval!, history_size: res.historySize!, internal_query: res.internalQuery!, })) }, updateStmtConfig(params) { return diagnosisServiceUpdateTopSqlConfigs(clusterId, { enable: params.enable, refreshInterval: params.refresh_interval, historySize: params.history_size, maxSize: params.max_size, internalQuery: params.internal_query, }).then(() => {}) }, getStmtList(params) { const advancedFiltersStrArr: string[] = [] for (const filter of params.advancedFilters) { const filterValue = filter.values .map((v) => encodeURIComponent(v)) .join(",") if (filterValue !== "") { advancedFiltersStrArr.push( `${filter.name} ${filter.operator} ${filterValue}`, ) } } const fieldsStr = params.fields.includes("all") ? "*" : params.fields.join(",") return diagnosisServiceGetTopSqlList(clusterId, { beginTime: params.beginTime + "", endTime: params.endTime + "", db: params.dbs, text: params.term, orderBy: params.orderBy, isDesc: params.desc, advancedFilter: advancedFiltersStrArr, fields: fieldsStr, pageSize: params.pageSize, skip: params.pageSize * params.pageIndex, }).then((res) => ({ total: res.totalSize ?? 0, items: res.data ?? [], })) }, getStmtPlans(params) { const [beginTime, endTime, digest, schemaName] = params.id.split(",") return diagnosisServiceGetSqlPlanList(clusterId, { beginTime: beginTime + "", endTime: endTime + "", digest, schemaName, }).then((res) => res.data ?? []) }, getStmtPlansDetail(params) { const [beginTime, endTime, digest, _schemaName] = params.id.split(",") return diagnosisServiceGetTopSqlDetail(clusterId, digest, { beginTime: beginTime + "", endTime: endTime + "", planDigest: params.plans.filter(Boolean), }).then((d) => { if (d.binary_plan_text) { d.plan = d.binary_plan_text delete d.binary_plan_text } return d }) }, // sql plan bind checkPlanBindSupport() { return diagnosisServiceCheckSqlPlanSupport(clusterId).then((res) => ({ is_support: res.isSupport!, })) }, getPlanBindStatus(params) { return diagnosisServiceGetSqlPlanBindingList(clusterId, { beginTime: params.beginTime + "", endTime: params.endTime + "", digest: params.sqlDigest, }).then((res) => (res.data ?? []).map((d) => d.planDigest!)) }, createPlanBind(params) { return diagnosisServiceBindSqlPlan(clusterId, params.planDigest).then( () => {}, ) }, deletePlanBind(params) { return diagnosisServiceUnbindSqlPlan(clusterId, { digest: params.sqlDigest, }).then(() => {}) }, // sql limit checkSqlLimitSupport() { return diagnosisServiceCheckSqlLimitSupport(clusterId).then( (res) => ({ is_support: res.isSupport! }), ) }, getSqlLimitStatus(params) { return diagnosisServiceGetSqlLimitList(clusterId, { watchText: params.watchText, }).then((res) => (res.data || []).map((d) => ({ id: d.id ?? "", ru_group: d.resourceGroupName ?? "", action: d.action ?? "", start_time: d.startTime ?? "", })), ) }, createSqlLimit(params) { return diagnosisServiceAddSqlLimit(clusterId, { watchText: params.watchText, resourceGroup: params.ruGroup, action: params.action as DiagnosisServiceAddSqlLimitBodyAction, }).then(() => {}) }, deleteSqlLimit(params) { return diagnosisServiceRemoveSqlLimit(clusterId, params).then( () => {}, ) }, // sql history getHistoryMetricNames() { return Promise.resolve([ { name: "sum_latency", unit: "ns" }, { name: "avg_latency", unit: "ns" }, { name: "max_latency", unit: "ns" }, { name: "min_latency", unit: "ns" }, { name: "avg_disk", unit: "bytes" }, { name: "max_disk", unit: "bytes" }, { name: "exec_count", unit: "short" }, { name: "plan_count", unit: "short" }, { name: "avg_parse_latency", unit: "ns" }, { name: "avg_compile_latency", unit: "ns" }, { name: "avg_wait_time", unit: "ns" }, { name: "avg_process_time", unit: "ns" }, { name: "avg_backoff_time", unit: "ns" }, { name: "avg_get_commit_ts_time", unit: "ns" }, { name: "avg_local_latch_wait_time", unit: "ns" }, { name: "avg_resolve_lock_time", unit: "ns" }, { name: "avg_prewrite_time", unit: "ns" }, { name: "avg_commit_time", unit: "ns" }, { name: "avg_commit_backoff_time", unit: "ns" }, ]) }, getHistoryMetricData(params) { return diagnosisServiceGetTopSqlList(clusterId, { beginTime: params.beginTime + "", endTime: params.endTime + "", orderBy: "summary_begin_time", isDesc: false, pageSize: 1000, fields: ["summary_begin_time", params.metricName].join(","), advancedFilter: [`digest = ${params.sqlDigest}`], isGroupByTime: true, }).then((res) => (res.data ?? []).map((d) => [ d.summary_begin_time! * 1000, d[params.metricName as keyof typeof d]! as number, ]), ) }, }, cfg: { title: "", }, actions: { openDetail: (id: string, newTab) => { window.preUrl = [window.location.pathname + window.location.search] if (newTab) { window.open(`/statement/detail?id=${id}`, "_blank") } else { navigate({ to: `/statement/detail?id=${id}` }) } }, backToList: () => { const preUrl = window.preUrl?.pop() navigate({ to: preUrl || "/statement" }) }, openSlowQueryList(id) { const [from, to, digest, _schema, plan] = id.split(",") const fullUrl = `/slow-query?from=${from}&to=${to}&af=digest,${encodeURIComponent("=")},${digest};plan_digest,${encodeURIComponent("=")},${plan || ""}` // open in a new tab window.open(fullUrl, "_blank") }, }, }), [navigate], ) } ================================================ FILE: ui-v2/packages/portals/test/src/apps/statement/stmt-types.ts ================================================ export const STMT_TYPES = [ "AlterTable", "AnalyzeTable", "Begin", "Change", "Commit", "CompactTable", "CreateDatabase", "CreateIndex", "CreateTable", "CreateView", "CreateUser", "Delete", "DropDatabase", "DropIndex", "DropView", "DropTable", "DescTable", "ExplainAnalyzeSQL", "ExplainSQL", "Replace", "Insert", "LoadData", "Rollback", "Select", "Set", "Show", "TruncateTable", "Update", "Grant", "Revoke", "Deallocate", "Execute", "Prepare", "Use", "CreateBinding", "IndexAdvise", "DropBinding", "Trace", "Shutdown", "Savepoint", "other", ] ================================================ FILE: ui-v2/packages/portals/test/src/index.css ================================================ ================================================ FILE: ui-v2/packages/portals/test/src/main.tsx ================================================ import { axiosClient } from "@pingcap-incubator/tidb-dashboard-lib-api-client" import { initI18n } from "@pingcap-incubator/tidb-dashboard-lib-apps/utils" import { StrictMode } from "react" import { createRoot } from "react-dom/client" import App from "./App.tsx" import "./index.css" initI18n() axiosClient.interceptors.request.use((config) => { // env: '' // prod: 'https://tidb-dashboard-lib-api-server.2008-hbl-cf.workers.dev' config.baseURL = import.meta.env.VITE_API_BASE_URL // eslint-disable-next-line @typescript-eslint/no-explicit-any config.headers = { "Ti-Env": "dev" } as any return config }) // handle error axiosClient.interceptors.response.use( (response) => response, (error) => { console.log(error) return Promise.reject(error) }, ) createRoot(document.getElementById("root")!).render( , ) ================================================ FILE: ui-v2/packages/portals/test/src/providers/react-query-provider.tsx ================================================ import { QueryClient, QueryClientProvider } from "@tanstack/react-query" // Create a react query client const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, // retry: 1 // refetchOnMount: false, // refetchOnReconnect: false, }, }, }) export function ReactQueryProvider({ children, }: { children: React.ReactNode }) { return ( // Provide the client to your App {children} ) } ================================================ FILE: ui-v2/packages/portals/test/src/providers/url-state-provider.tsx ================================================ import { UrlStateProvider } from "@pingcap-incubator/tidb-dashboard-lib-apps/utils" import { useLocation, useNavigate } from "@tanstack/react-router" import { useMemo } from "react" export function TanStackRouterUrlStateProvider(props: { children: React.ReactNode }) { const loc = useLocation() const navigate = useNavigate() const ctxValue = useMemo(() => { return { urlQuery: loc.searchStr, setUrlQuery(v: string) { navigate({ to: `${loc.pathname}?${v}` }) }, } }, [loc.searchStr, loc.pathname, navigate]) return {props.children} } ================================================ FILE: ui-v2/packages/portals/test/src/router/devtools.tsx ================================================ import React from "react" export const RouterDevtools = process.env.NODE_ENV === "production" || import.meta.env.VITE_TANSTACK_ROUTER_DEVTOOLS_ENABLED !== "true" ? () => null : React.lazy(() => import("@tanstack/router-devtools").then((res) => ({ default: res.TanStackRouterDevtools, })), ) ================================================ FILE: ui-v2/packages/portals/test/src/router/provider.tsx ================================================ import { RouterProvider as TanstackRouterProvider } from "@tanstack/react-router" import { router } from "./router" export function RouterProvider() { return } ================================================ FILE: ui-v2/packages/portals/test/src/router/router.ts ================================================ import { createRouter } from "@tanstack/react-router" import { routeTree } from "./routeTree.gen" export const router = createRouter({ routeTree, }) // Register the router instance for type safety declare module "@tanstack/react-router" { interface Register { router: typeof router } } ================================================ FILE: ui-v2/packages/portals/test/src/routes/__root.tsx ================================================ import { ChartThemeSwitch } from "@pingcap-incubator/tidb-dashboard-lib-apps/charts" import { useHotkeyChangeLang } from "@pingcap-incubator/tidb-dashboard-lib-apps/utils" import { Outlet, createRootRoute } from "@tanstack/react-router" import { useComputedColorScheme } from "@tidbcloud/uikit" import { RouterDevtools } from "../router/devtools" export const Route = createRootRoute({ component: RootComponent, }) function RootComponent() { useHotkeyChangeLang() const theme = useComputedColorScheme() return ( <> ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/metrics/azores-cluster-overview.lazy.tsx ================================================ import { AzoresClusterOverviewMetricsPage } from "@pingcap-incubator/tidb-dashboard-lib-apps/metric" import { createLazyFileRoute } from "@tanstack/react-router" import { TanStackRouterUrlStateProvider } from "../../../providers/url-state-provider" export const Route = createLazyFileRoute( "/_apps-layout/metrics/azores-cluster-overview", )({ component: RouteComponent, }) function RouteComponent() { return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/metrics/azores-cluster.lazy.tsx ================================================ import { AzoresClusterMetricsPage } from "@pingcap-incubator/tidb-dashboard-lib-apps/metric" import { createLazyFileRoute } from "@tanstack/react-router" import { TanStackRouterUrlStateProvider } from "../../../providers/url-state-provider" export const Route = createLazyFileRoute( "/_apps-layout/metrics/azores-cluster", )({ component: RouteComponent, }) function RouteComponent() { return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/metrics/azores-host.lazy.tsx ================================================ import { AzoresHostMetricsPage } from "@pingcap-incubator/tidb-dashboard-lib-apps/metric" import { createLazyFileRoute } from "@tanstack/react-router" import { TanStackRouterUrlStateProvider } from "../../../providers/url-state-provider" export const Route = createLazyFileRoute("/_apps-layout/metrics/azores-host")({ component: RouteComponent, }) function RouteComponent() { return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/metrics/azores-overview.lazy.tsx ================================================ import { AzoresOverviewMetricsPage } from "@pingcap-incubator/tidb-dashboard-lib-apps/metric" import { createLazyFileRoute } from "@tanstack/react-router" import { TanStackRouterUrlStateProvider } from "../../../providers/url-state-provider" export const Route = createLazyFileRoute( "/_apps-layout/metrics/azores-overview", )({ component: RouteComponent, }) function RouteComponent() { return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/metrics/index.tsx ================================================ import { createFileRoute, redirect } from "@tanstack/react-router" export const Route = createFileRoute("/_apps-layout/metrics/")({ beforeLoad: () => { throw redirect({ to: "/metrics/normal" }) }, }) ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/metrics/normal.lazy.tsx ================================================ import { NormalMetricsPage } from "@pingcap-incubator/tidb-dashboard-lib-apps/metric" import { createLazyFileRoute } from "@tanstack/react-router" import { TanStackRouterUrlStateProvider } from "../../../providers/url-state-provider" export const Route = createLazyFileRoute("/_apps-layout/metrics/normal")({ component: RouteComponent, }) function RouteComponent() { return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/metrics.lazy.tsx ================================================ import { AppProvider } from "@pingcap-incubator/tidb-dashboard-lib-apps/metric" import { Outlet, createLazyFileRoute } from "@tanstack/react-router" import { useCtxValue } from "../../apps/metric/mock-api-app-provider" export const Route = createLazyFileRoute("/_apps-layout/metrics")({ component: RouteComponent, }) function RouteComponent() { const ctxValue = useCtxValue() return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/slow-query/detail.lazy.tsx ================================================ import { Detail as SlowQueryDetailPage } from "@pingcap-incubator/tidb-dashboard-lib-apps/slow-query" import { createLazyFileRoute } from "@tanstack/react-router" import { TanStackRouterUrlStateProvider } from "../../../providers/url-state-provider" export const Route = createLazyFileRoute("/_apps-layout/slow-query/detail")({ component: RouteComponent, }) function RouteComponent() { return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/slow-query/index.lazy.tsx ================================================ import { List as SlowQueryListPage } from "@pingcap-incubator/tidb-dashboard-lib-apps/slow-query" import { createLazyFileRoute } from "@tanstack/react-router" import { TanStackRouterUrlStateProvider } from "../../../providers/url-state-provider" export const Route = createLazyFileRoute("/_apps-layout/slow-query/")({ component: RouteComponent, }) function RouteComponent() { return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/slow-query.lazy.tsx ================================================ import { AppProvider } from "@pingcap-incubator/tidb-dashboard-lib-apps/slow-query" import { Outlet, createLazyFileRoute } from "@tanstack/react-router" import { useCtxValue } from "../../apps/slow-query/mock-api-app-provider" export const Route = createLazyFileRoute("/_apps-layout/slow-query")({ component: RouteComponent, }) function RouteComponent() { const ctxValue = useCtxValue() return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/statement/detail.lazy.tsx ================================================ import { Detail as StatementDetailPage } from "@pingcap-incubator/tidb-dashboard-lib-apps/statement" import { createLazyFileRoute } from "@tanstack/react-router" import { TanStackRouterUrlStateProvider } from "../../../providers/url-state-provider" export const Route = createLazyFileRoute("/_apps-layout/statement/detail")({ component: RouteComponent, }) function RouteComponent() { return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/statement/index.lazy.tsx ================================================ import { List as StatementListPage } from "@pingcap-incubator/tidb-dashboard-lib-apps/statement" import { createLazyFileRoute } from "@tanstack/react-router" import { TanStackRouterUrlStateProvider } from "../../../providers/url-state-provider" export const Route = createLazyFileRoute("/_apps-layout/statement/")({ component: RouteComponent, }) function RouteComponent() { return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout/statement.lazy.tsx ================================================ import { AppProvider } from "@pingcap-incubator/tidb-dashboard-lib-apps/statement" import { Outlet, createLazyFileRoute } from "@tanstack/react-router" import { useCtxValue } from "../../apps/statement/mock-api-app-provider" export const Route = createLazyFileRoute("/_apps-layout/statement")({ component: RouteComponent, }) function RouteComponent() { const ctxValue = useCtxValue() return ( ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/_apps-layout.tsx ================================================ import { Link, Outlet, createFileRoute } from "@tanstack/react-router" import { AppShell, NavLink, ScrollArea, Stack } from "@tidbcloud/uikit" import { createStyles } from "@tidbcloud/uikit/emotion" export const Route = createFileRoute("/_apps-layout")({ component: RouteComponent, }) export const useStyles = createStyles(() => { return { navItems: { a: { textDecoration: "none", }, }, } }) function RouteComponent() { const { classes } = useStyles() return ( TiDB Dashboard Lib {({ isActive }) => ( )} {({ isActive }) => ( )} {({ isActive }) => ( )} {({ isActive }) => ( )} {({ isActive }) => ( )} {({ isActive }) => ( )} {({ isActive }) => ( )} Theme / Language ) } ================================================ FILE: ui-v2/packages/portals/test/src/routes/index.ts ================================================ import { createFileRoute, redirect } from "@tanstack/react-router" export const Route = createFileRoute("/")({ beforeLoad: () => { throw redirect({ to: "/metrics" }) }, }) ================================================ FILE: ui-v2/packages/portals/test/src/routes/login.lazy.tsx ================================================ import { createLazyFileRoute } from "@tanstack/react-router" export const Route = createLazyFileRoute("/login")({ component: LoginPage, }) function LoginPage() { return <>Login } ================================================ FILE: ui-v2/packages/portals/test/src/vite-env.d.ts ================================================ /// ================================================ FILE: ui-v2/packages/portals/test/tsconfig.json ================================================ { "extends": "../../tsconfig.app.json", "compilerOptions": { "outDir": "./dist" }, "include": ["./src"] } ================================================ FILE: ui-v2/packages/portals/test/vite.config.ts ================================================ import { TanStackRouterVite } from "@tanstack/router-plugin/vite" import react from "@vitejs/plugin-react" import { defineConfig, loadEnv } from "vite" export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd()) return { plugins: [ TanStackRouterVite({ generatedRouteTree: "./src/router/routeTree.gen.ts", }), react(), ], server: { proxy: { "/api": { target: env.VITE_API_SERVER, changeOrigin: true, }, }, }, } }) ================================================ FILE: ui-v2/packages/tsconfig.app.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* declaration */ "declaration": true, "emitDeclarationOnly": true, // "noEmit": false, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "resolveJsonModule": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true } } ================================================ FILE: ui-v2/pnpm-workspace.yaml ================================================ packages: # all packages in subdirs of packages/ - "packages/**" catalog: "@tidbcloud/uikit": "^2.1.7" ================================================ FILE: ui-v2/scripts/gen-locales.ts ================================================ import * as fs from "fs" import * as path from "path" import { glob } from "glob" import $ from "gogocode" interface LocaleData { [app: string]: { keys: Record texts: Record } } function sortLocaleData(localeData: LocaleData) { Object.keys(localeData).forEach((appName) => { localeData[appName].keys = Object.fromEntries( Object.entries(localeData[appName].keys).sort(), ) localeData[appName].texts = Object.fromEntries( Object.entries(localeData[appName].texts).sort(), ) }) } function handleAppFiles(appFolder: string, localeData: LocaleData) { const outputDir = `${appFolder}/locales` fs.mkdirSync(outputDir, { recursive: true }) const appName = appFolder.split("/").pop() || "" let outputData = { __namespace__: appName, __comment__: "this file can be updated by running `pnpm gen:locales` command", ...localeData[appFolder].keys, // ...localeData[appFolder].texts, // texts doesn't need to write in the en.json, to save space } // Write en.json fs.writeFileSync( path.join(outputDir, "en.json"), JSON.stringify(outputData, null, 2) + "\n", ) // Write zh.json outputData = { ...outputData, ...localeData[appFolder].texts, } // Update zh.json // Check if zh.json exists and merge with existing translations const zhPath = path.join(outputDir, "zh.json") if (fs.existsSync(zhPath)) { const existedZh = JSON.parse(fs.readFileSync(zhPath, "utf-8")) // replace outputData with existedZh for (const key in existedZh) { outputData[key] = existedZh[key] } } fs.writeFileSync(zhPath, JSON.stringify(outputData, null, 2) + "\n") // write index.ts const indexPath = path.join(outputDir, "index.ts") fs.writeFileSync( indexPath, `import { addLangsLocales } from "@pingcap-incubator/tidb-dashboard-lib-utils" import en from "./en.json" import zh from "./zh.json" addLangsLocales({ en, zh }) `, ) } const bizUiNsPathMap: Map = new Map() function handleBizUIFiles(appName: string, localeData: LocaleData) { const filePath = bizUiNsPathMap.get(appName) if (!filePath) { return } const code = fs.readFileSync(filePath, "utf-8") const ast = $(code) const allKeys = Object.keys(localeData[appName].keys).concat( Object.keys(localeData[appName].texts), ) ast.replace( `type I18nLocaleKeys = $_$`, `type I18nLocaleKeys = ${allKeys.map((k) => `\n | "${k}"`).join("")}`, ) // update en const keyPartKeys = Object.keys(localeData[appName].keys) ast.replace( `const en: I18nLocale = { $$$0 }`, `const en: I18nLocale = { ${keyPartKeys.map((k) => `"${k}": "${localeData[appName].keys[k]}"`).join(", ")} }`, ) // get existed zh const existedZh = ast.find(`const zh: I18nLocale = { $$$0 }`).match["$$$0"] const existedZhLocales = existedZh.reduce( (acc, kv) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const _kv = kv as any acc[_kv.key.name] = _kv.value.value return acc }, {} as Record, ) const zhLocales = { ...localeData[appName].keys, ...localeData[appName].texts, } // merge Object.keys(existedZhLocales).forEach((k) => { zhLocales[k] = existedZhLocales[k] }) // update zh ast.replace( `const zh: I18nLocale = { $$$0 }`, `const zh: I18nLocale = { ${allKeys.map((k) => `\n "${k}": "${zhLocales[k]}",`).join("")} }`, ) fs.writeFileSync(filePath, ast.generate()) } async function generateLocales() { // Initialize locale data structure const localeData: LocaleData = {} // Scan all TypeScript/JavaScript files in the apps folder const files = await glob([ "packages/libs/3-biz-ui/src/**/*.{ts,tsx,js,jsx}", "packages/libs/4-apps/src/**/*.{ts,tsx,js,jsx}", ]) // Parse and extract locale data for (const filePath of files) { const code = fs.readFileSync(filePath, "utf-8") const ast = $(code) // get biz-ui locales path const ns = ast.find("const I18nNamespace = $_$0") if (ns.length >= 1) { const nsVal = ns.match[0][0].value bizUiNsPathMap.set(nsVal, filePath) } // check app name // app name in the `useTn` call should be the same as the file path let hasTn = false let appFolder = "" ast .find("const { $$$0 } = useTn($_$0)") // eslint-disable-next-line @typescript-eslint/no-explicit-any .each((item: any) => { // $$$0 --> item.match['$$$0'] // $_$0 --> item.match[0][0].value const appName = item.match[0][0].value const appFolderPos = filePath.indexOf(`/${appName}`) if (appFolderPos === -1) { console.error(filePath) console.error(`app name mismatch: ${appName}`) return } else { if (filePath.indexOf("/3-biz-ui/") !== -1) { appFolder = appName } else { appFolder = filePath.slice(0, appFolderPos) + "/" + appName } } hasTn = true }) if (!hasTn) { continue } // init app data if (!localeData[appFolder]) { localeData[appFolder] = { keys: {}, texts: {}, } } // Handle `tt` calls, likes: // tt('Clear Filters') // tt("hello {{name}}", { name: "world" }) ast .find("tt($_$)") // same as `find("tt($_$0)")`, only match the first argument // eslint-disable-next-line @typescript-eslint/no-explicit-any .each((tnItem: any) => { const text = tnItem.match[0][0].value localeData[appFolder].texts[text] = text }) // Handle `tk` calls, likes: // tk(`panels.${props.config.category}`) // tk("panels.instance_top", "Top 5 Node Utilization") // tk("time_range.hour", "{{count}} hr", { count: 1 }) // tk("time_range.hour", "{{count}} hrs", { count: 24 }) // tk("time_range.hour", "", {count: n}) ast .find("tk($$$0)") // match all arguments // eslint-disable-next-line @typescript-eslint/no-explicit-any .each((tkItem: any) => { const match = tkItem.match["$$$0"] if (match.length === 1 || match[0].value === undefined) { // ignore this kind of case, likes: // tk(`panels.${props.config.category}`) // tk(`panels.${props.config.category}`, props.config.category) } else { let key = match[0].value const value = match[1].value if (!value) { // continue return } if (match.length === 3) { // handle plural case // eslint-disable-next-line @typescript-eslint/no-explicit-any const options: any = {} for (const option of match[2].properties) { options[option.key.name] = option.value.value } // {count: n} --> {count: undefined} // {count: 1} --> {count: 1} // {count: 24} --> {count: 24} if (options.count === 0) { key = `${key}_zero` } else if (options.count === 1) { key = `${key}_one` } else if (options.count > 1) { key = `${key}_other` } } // check whether value is existed const existedVal = localeData[appFolder].keys[key] if (existedVal !== undefined && existedVal !== value) { console.error( `same keys but have different values, key: ${key}, values: ${existedVal}, ${value}`, ) // break return false } localeData[appFolder].keys[key] = value } }) } // Sort sortLocaleData(localeData) // Output for (const appFolder of Object.keys(localeData)) { if (appFolder.indexOf("/4-apps/") !== -1) { handleAppFiles(appFolder, localeData) } else { handleBizUIFiles(appFolder, localeData) } } } generateLocales().catch(console.error) ================================================ FILE: ui-v2/scripts/gogocode-test/1.js ================================================ // https://github.com/thx/gogocode/blob/main/docs/specification/basic.zh.md import $ from "gogocode" // const source = ` // function log(a) { // console.log(a); // } // function alert(a) { // alert(a); // } // ` // const ast = $(source); // const fns = ast.find(`function $_$() {}`); // length = 2 // console.log(fns.match) // 只返回第一个 match,其值相当于 fns[0].match // console.log(fns.length) // const names = []; // fns.each((fnNode) => { // const fnName = fnNode.match[0][0].value; // names.push(fnName); // }); // names = ['log', 'alert'] // console.log(names) const source = ` console.log(a); console.log(b, c); console.log(d, e, f); ` const ast = $(source) const logs = ast.find(`console.log($_$1, $_$2)`) // console.log(logs.match) console.log(logs[0].match) console.log(logs[1].match) console.log(logs.length) // const res = ast.find('console.log($$$0)'); // console.log(res.length) // const params = res[2].match['$$$0']; // const paramNames = params.map((p) => p.name); // console.log(paramNames); // ['d', 'e', 'f'] ================================================ FILE: ui-v2/scripts/gogocode-test/2.js ================================================ // https://github.com/thx/gogocode/blob/main/docs/specification/basic.zh.md import $ from "gogocode" import { inspect } from "util" const source = ` export const I18nNamespace = "advanced-filters" type I18nLocaleKeys = | "Advanced Filters" | "Add Filter" | "Filter Name" | "WHEN" | "AND" | "Cancel" | "Save" type I18nLocale = { [K in I18nLocaleKeys]?: string } const en: I18nLocale = {} const zh: I18nLocale = { "Advanced Filters": "高级筛选", "Add Filter": "添加筛选条件", "Filter Name": "筛选条件名称", WHEN: "当", AND: "且", Cancel: "取消", Save: "保存", } ` const ast = $(source) const namespace = ast.find(`const I18nNamespace = $_$`).match[0][0].value console.log(namespace) const res = ast.find(`const zh: I18nLocale = { $$$0 }`) const kvs = res.match["$$$0"] console.log(inspect(kvs)) console.log(kvs.map((kv) => `${kv.key.name}:${kv.value.value}`)) ast.replace(`type I18nLocaleKeys = $_$`, `type I18nLocaleKeys = "aa" | "bb"`) console.log(ast.generate()) ast.replace( `const zh: I18nLocale = { $$$0 }`, `const zh: I18nLocale = { "aa": "aa", "bb": "bb" }`, ) console.log(ast.generate()) // result: // // const I18nNamespace = "advanced-filters" // type I18nLocaleKeys = "aa" | "bb" // type I18nLocale = { // [K in I18nLocaleKeys]?: string // } // const en: I18nLocale = {} // const zh: I18nLocale = { "aa": "aa", "bb": "bb" } ================================================ FILE: util/assertutil/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package assertutil import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/assertutil/json.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors. package assertutil import ( "encoding/json" "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func JSONContains(t assert.TestingT, src string, contains string, msgAndArgs ...interface{}) bool { var srcJSONMap, containedJSONMap map[string]interface{} if err := json.Unmarshal([]byte(src), &srcJSONMap); err != nil { return assert.Fail(t, fmt.Sprintf("Src value ('%s') is not a valid json object string.\nJSON parsing error: '%s'", src, err.Error()), msgAndArgs...) } if err := json.Unmarshal([]byte(contains), &containedJSONMap); err != nil { return assert.Fail(t, fmt.Sprintf("Contained value ('%s') is not a valid json object string.\nJSON parsing error: '%s'", contains, err.Error()), msgAndArgs...) } for key, value := range containedJSONMap { srcValue, ok := srcJSONMap[key] if !ok || !assert.ObjectsAreEqual(srcValue, value) { return assert.Fail(t, fmt.Sprintf("Src ('%s') does not contain '%s'", src, contains), msgAndArgs...) } } return true } func RequireJSONContains(t require.TestingT, src string, contains string, msgAndArgs ...interface{}) { if JSONContains(t, src, contains, msgAndArgs...) { return } t.FailNow() } ================================================ FILE: util/assertutil/json_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package assertutil import ( "testing" "github.com/stretchr/testify/require" ) func TestJSONContains(t *testing.T) { mockT := new(mockTestingT) require.False(t, JSONContains(mockT, `null`, `{"a":1}`)) require.Contains(t, mockT.errorString(), `Src ('null') does not contain '{"a":1}'`) mockT = new(mockTestingT) require.True(t, JSONContains(mockT, `null`, `null`)) mockT = new(mockTestingT) require.True(t, JSONContains(mockT, `null`, `{}`)) mockT = new(mockTestingT) require.True(t, JSONContains(mockT, `{"a":1}`, `{"a":1}`)) mockT = new(mockTestingT) require.True(t, JSONContains(mockT, `{"a":1}`, `null`)) mockT = new(mockTestingT) require.False(t, JSONContains(mockT, `nullx`, `{"a":1}`)) require.Contains(t, mockT.errorString(), `Src value ('nullx') is not a valid json object string`) mockT = new(mockTestingT) require.False(t, JSONContains(mockT, `{"a":1}`, `nullx`)) require.Contains(t, mockT.errorString(), `Contained value ('nullx') is not a valid json object string`) mockT = new(mockTestingT) require.False(t, JSONContains(mockT, `{"b":1}`, `{"a":1}`)) require.Contains(t, mockT.errorString(), `Src ('{"b":1}') does not contain '{"a":1}'`) mockT = new(mockTestingT) require.False(t, JSONContains(mockT, `{"a":1}`, `{"b":1}`)) require.Contains(t, mockT.errorString(), `Src ('{"a":1}') does not contain '{"b":1}'`) mockT = new(mockTestingT) require.True(t, JSONContains(mockT, `{"a":1,"b":2}`, `{"a":1}`)) mockT = new(mockTestingT) require.True(t, JSONContains(mockT, `{"b":2,"a":1}`, `{"a":1}`)) mockT = new(mockTestingT) require.True(t, JSONContains(mockT, `{"a":1,"b":2}`, `{"b":2,"a":1}`)) mockT = new(mockTestingT) require.False(t, JSONContains(mockT, `{"a":1}`, `{"a":1,"b":2}`)) require.Contains(t, mockT.errorString(), `Src ('{"a":1}') does not contain '{"a":1,"b":2}'`) mockT = new(mockTestingT) require.False(t, JSONContains(mockT, `{"a":2,"b":2}`, `{"a":1}`)) require.Contains(t, mockT.errorString(), `Src ('{"a":2,"b":2}') does not contain '{"a":1}'`) mockT = new(mockTestingT) require.False(t, JSONContains(mockT, `{"a":1}`, `{"A":1}`)) require.Contains(t, mockT.errorString(), `Src ('{"a":1}') does not contain '{"A":1}'`) mockT = new(mockTestingT) require.True(t, JSONContains(mockT, `{"a":{"foo":"bar"},"b":2}`, `{"a":{"foo":"bar"}}`)) mockT = new(mockTestingT) require.False(t, JSONContains(mockT, `{"a":{"foo":"bar"},"b":2}`, `{"a":1}`)) require.Contains(t, mockT.errorString(), `Src ('{"a":{"foo":"bar"},"b":2}') does not contain '{"a":1}'`) mockT = new(mockTestingT) require.False(t, JSONContains(mockT, `{"a":{"foo":"bar"},"b":2}`, `{"a":{"foo":"box"}}`)) require.Contains(t, mockT.errorString(), `Src ('{"a":{"foo":"bar"},"b":2}') does not contain '{"a":{"foo":"box"}}'`) mockT = new(mockTestingT) require.False(t, JSONContains(mockT, `{"a":{"foo":"bar"},"b":2}`, `{"a":{"FOO":"bar"}}`)) require.Contains(t, mockT.errorString(), `Src ('{"a":{"foo":"bar"},"b":2}') does not contain '{"a":{"FOO":"bar"}}'`) } ================================================ FILE: util/assertutil/mock.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors. package assertutil import "fmt" type mockTestingT struct { errorFmt string args []interface{} } func (m *mockTestingT) errorString() string { return fmt.Sprintf(m.errorFmt, m.args...) } func (m *mockTestingT) Errorf(format string, args ...interface{}) { m.errorFmt = format m.args = args } ================================================ FILE: util/client/httpclient/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package httpclient import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/client/httpclient/client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package httpclient import ( "context" "crypto/tls" "net" "net/http" "runtime" "time" "github.com/joomcode/errorx" "github.com/pingcap/tidb-dashboard/util/nocopy" ) var ( ErrNS = errorx.NewNamespace("http_client") ErrRequestFailed = ErrNS.NewType("request_failed") ) // Client caches connections for future re-use and should be reused instead of // created as needed. type Client struct { nocopy.NoCopy kindTag string transport http.RoundTripper defaultCtx context.Context defaultBaseURL string } func newTransport(tlsConfig *tls.Config) *http.Transport { dialer := &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, } return &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: dialer.DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, TLSClientConfig: tlsConfig, } } func New(config Config) *Client { return &Client{ kindTag: config.KindTag, transport: newTransport(config.TLSConfig), defaultCtx: config.DefaultCtx, defaultBaseURL: config.DefaultBaseURL, } } // Clone creates a new client with the same configuration. Subsequent SetXxx() calls will not // affect the current client. The transport will be shared unless it is changed to something else later. func (c *Client) Clone() *Client { return &Client{ kindTag: c.kindTag, transport: c.transport, defaultCtx: c.defaultCtx, defaultBaseURL: c.defaultBaseURL, } } // SetDefaultTransport sets the default HTTP transport for subsequent new requests. // This function should be used only when you want to mock the request. // In other cases, there is usually no need to use a customized HTTP transport. func (c *Client) SetDefaultTransport(transport http.RoundTripper) *Client { c.transport = transport return c } // SetDefaultCtx sets the default context for subsequent new requests. func (c *Client) SetDefaultCtx(ctx context.Context) *Client { c.defaultCtx = ctx return c } // SetDefaultBaseURL sets the default base URL for subsequent new requests. func (c *Client) SetDefaultBaseURL(baseURL string) *Client { c.defaultBaseURL = baseURL return c } func (c *Client) LR() *LazyRequest { lReq := newRequest(c.kindTag, c.transport) if c.defaultCtx != nil { lReq.SetContext(c.defaultCtx) } if len(c.defaultBaseURL) > 0 { lReq.SetTLSAwareBaseURL(c.defaultBaseURL) } return lReq } ================================================ FILE: util/client/httpclient/config.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package httpclient import ( "context" "crypto/tls" ) type Config struct { KindTag string TLSConfig *tls.Config DefaultCtx context.Context DefaultBaseURL string } ================================================ FILE: util/client/httpclient/info.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package httpclient import ( "github.com/pingcap/log" "go.uber.org/zap" ) // execInfo is a copy of necessary information during the execution. // It can be used to print logs when something happens. type execInfo struct { kindTag string reqURL string reqMethod string respStatus string respBody string } func (e *execInfo) Warn(msg string, err error) { fields := []zap.Field{ zap.String("kindTag", e.kindTag), zap.String("url", e.reqURL), zap.String("method", e.reqMethod), } if e.respStatus != "" { fields = append(fields, zap.String("responseStatus", e.respStatus)) } if e.respBody != "" { fields = append(fields, zap.String("responseBody", e.respBody)) } fields = append(fields, zap.Error(err)) log.Warn(msg, fields...) } ================================================ FILE: util/client/httpclient/request.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package httpclient import ( "context" "net/http" "strings" "time" "github.com/go-resty/resty/v2" "github.com/pingcap/tidb-dashboard/util/nocopy" ) // LazyRequest can be used to compose and fire individual request from the client. // The request will not be actually sent until reading from LazyResponse. type LazyRequest struct { // Note: this is a lazy struct. nocopy.NoCopy kindTag string debugTag string transport http.RoundTripper opsR []requestUpdateFn opsC []clientUpdateFn } func newRequest(kindTag string, transport http.RoundTripper) *LazyRequest { return &LazyRequest{ kindTag: kindTag, transport: transport, } } // Clone creates a new request with all settings cloned. func (lReq *LazyRequest) Clone() *LazyRequest { lReqCloned := &LazyRequest{ kindTag: lReq.kindTag, debugTag: lReq.debugTag, transport: lReq.transport, // transport will never change after creation, so this is concurrent-safe opsR: make([]requestUpdateFn, len(lReq.opsR)), opsC: make([]clientUpdateFn, len(lReq.opsC)), } copy(lReqCloned.opsR, lReq.opsR) copy(lReqCloned.opsC, lReq.opsC) return lReqCloned } // SetDebugTag enables the debugging log if tag is not empty, or disables it otherwise. // The debugging log will be printed with log level INFO. func (lReq *LazyRequest) SetDebugTag(debugTag string) *LazyRequest { lReq.debugTag = debugTag return lReq } // SetContext sets the context.Context for current request. It allows // to interrupt the request execution if ctx.Done() channel is closed. // See https://blog.golang.org/context article and the "context" package // documentation. func (lReq *LazyRequest) SetContext(ctx context.Context) *LazyRequest { if ctx == nil { return lReq } lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetContext(ctx) }) return lReq } // SetTimeout sets the total timeout for sending the request and reading the response. func (lReq *LazyRequest) SetTimeout(timeout time.Duration) *LazyRequest { lReq.opsC = append(lReq.opsC, func(c *resty.Client) { c.SetTimeout(timeout) }) return lReq } // SetURL sets the URL for current request. This URL will be used when calling Send(). // // resp := client.LR(). // SetMethod("GET"). // SetURL("http://httpbin.org/get"). // Send() func (lReq *LazyRequest) SetURL(url string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.URL = url }) return lReq } // SetMethod sets the method of the request. This method will be used when calling Send(). // // resp := client.LR(). // SetMethod("GET"). // SetURL("http://httpbin.org/get"). // Send() func (lReq *LazyRequest) SetMethod(method string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.Method = method }) return lReq } func isMTLSConfigured(r http.RoundTripper) bool { transport, ok := r.(*http.Transport) if !ok { return false } if transport.TLSClientConfig == nil { return false } if len(transport.TLSClientConfig.Certificates) > 0 || transport.TLSClientConfig.RootCAs != nil { return true } // It may be possible that transport.TLSClientConfig is &tls.Config{}. In this case // we still treat it as mTLS not configured. return false } // SetTLSAwareBaseURL sets the base URL for current request. Relative URLs will be based on this base URL. // If the client is built with TLS certs, http scheme will be changed to https automatically. // // resp := client.LR(). // SetTLSAwareBaseURL("http://myjeeva.com"). // Get("/foo") func (lReq *LazyRequest) SetTLSAwareBaseURL(baseURL string) *LazyRequest { // Rewrite http URL to https if TLS certificate is specified. if isMTLSConfigured(lReq.transport) && strings.HasPrefix(baseURL, "http://") { baseURL = "https://" + baseURL[len("http://"):] } lReq.opsC = append(lReq.opsC, func(c *resty.Client) { c.SetHostURL(baseURL) }) return lReq } // Send method lazily send the HTTP request using the method and URL already defined // for current LazyRequest. // // resp := client.LR(). // SetMethod("GET"). // SetURL("http://httpbin.org/get"). // Send() func (lReq *LazyRequest) Send() *LazyResponse { return newResponse(lReq.Clone()) } // Execute lazily sends the HTTP request with given HTTP method and URL // for current LazyRequest. // // resp := client.LR(). // Execute("GET", "http://httpbin.org/get") func (lReq *LazyRequest) Execute(method, url string) *LazyResponse { cloned := lReq.Clone() cloned.opsR = append(cloned.opsR, func(r *resty.Request) { r.Method = method r.URL = url }) return newResponse(cloned) } // Get lazily sends a GET request with the specified URL for current LazyRequest. func (lReq *LazyRequest) Get(url string) *LazyResponse { return lReq.Execute(resty.MethodGet, url) } // Post lazily sends a POST request with the specified URL for current LazyRequest. func (lReq *LazyRequest) Post(url string) *LazyResponse { return lReq.Execute(resty.MethodPost, url) } // Put lazily sends a PUT request with the specified URL for current LazyRequest. func (lReq *LazyRequest) Put(url string) *LazyResponse { return lReq.Execute(resty.MethodPut, url) } // Delete lazily sends a DELETE request with the specified URL for current LazyRequest. func (lReq *LazyRequest) Delete(url string) *LazyResponse { return lReq.Execute(resty.MethodDelete, url) } ================================================ FILE: util/client/httpclient/request_resty.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. // This file only contains encapsulated functions implemented over resty.Request package httpclient import ( "io" "net/http" "net/url" "github.com/go-resty/resty/v2" ) // SetHeader method is to set a single header field and its value in the current request. // // For Example: To set `Content-Type` and `Accept` as `application/json`. // // client.LR(). // SetHeader("Content-Type", "application/json"). // SetHeader("Accept", "application/json") // // Also you can override header value, which was set at client instance level. func (lReq *LazyRequest) SetHeader(header, value string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetHeader(header, value) }) return lReq } // SetHeaders method sets multiple headers field and its values at one go in the current request. // // For Example: To set `Content-Type` and `Accept` as `application/json` // // client.LR(). // SetHeaders(map[string]string{ // "Content-Type": "application/json", // "Accept": "application/json", // }) // // Also you can override header value, which was set at client instance level. func (lReq *LazyRequest) SetHeaders(headers map[string]string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetHeaders(headers) }) return lReq } // SetHeaderVerbatim method is to set a single header field and its value verbatim in the current request. // // For Example: To set `all_lowercase` and `UPPERCASE` as `available`. // // client.LR(). // SetHeaderVerbatim("all_lowercase", "available"). // SetHeaderVerbatim("UPPERCASE", "available") // // Also you can override header value, which was set at client instance level. func (lReq *LazyRequest) SetHeaderVerbatim(header, value string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetHeaderVerbatim(header, value) }) return lReq } // SetQueryParam method sets single parameter and its value in the current request. // It will be formed as query string for the request. // // For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark. // // client.LR(). // SetQueryParam("search", "kitchen papers"). // SetQueryParam("size", "large") // // Also you can override query params value, which was set at client instance level. func (lReq *LazyRequest) SetQueryParam(param, value string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetQueryParam(param, value) }) return lReq } // SetQueryParams method sets multiple parameters and its values at one go in the current request. // It will be formed as query string for the request. // // For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark. // // client.LR(). // SetQueryParams(map[string]string{ // "search": "kitchen papers", // "size": "large", // }) // // Also you can override query params value, which was set at client instance level. func (lReq *LazyRequest) SetQueryParams(params map[string]string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetQueryParams(params) }) return lReq } // SetQueryParamsFromValues method appends multiple parameters with multi-value // (`url.Values`) at one go in the current request. It will be formed as // query string for the request. // // For Example: `status=pending&status=approved&status=open` in the URL after `?` mark. // // client.LR(). // SetQueryParamsFromValues(url.Values{ // "status": []string{"pending", "approved", "open"}, // }) // // Also you can override query params value, which was set at client instance level. func (lReq *LazyRequest) SetQueryParamsFromValues(params url.Values) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetQueryParamsFromValues(params) }) return lReq } // SetQueryString method provides ability to use string as an input to set URL query string for the request. // // Using String as an input // // client.LR(). // SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more") func (lReq *LazyRequest) SetQueryString(query string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetQueryString(query) }) return lReq } // SetFormData method sets Form parameters and their values in the current request. // It's applicable only HTTP method `POST` and `PUT` and requests content type would be set as // `application/x-www-form-urlencoded`. // // client.LR(). // SetFormData(map[string]string{ // "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F", // "user_id": "3455454545", // }) // // Also you can override form data value, which was set at client instance level. func (lReq *LazyRequest) SetFormData(data map[string]string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetFormData(data) }) return lReq } // SetFormDataFromValues method appends multiple form parameters with multi-value // (`url.Values`) at one go in the current request. // // client.LR(). // SetFormDataFromValues(url.Values{ // "search_criteria": []string{"book", "glass", "pencil"}, // }) // // Also you can override form data value, which was set at client instance level. func (lReq *LazyRequest) SetFormDataFromValues(data url.Values) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetFormDataFromValues(data) }) return lReq } // SetBody method sets the request body for the request. It supports various realtime needs as easy. // We can say its quite handy or powerful. Supported request body data types is `string`, // `[]byte`, `struct`, `map`, `slice` and `io.Reader`. Body value can be pointer or non-pointer. // Automatic marshalling for JSON and XML content type, if it is `struct`, `map`, or `slice`. // // Note: `io.Reader` is processed as bufferless mode while sending request. // // For Example: Struct as a body input, based on content type, it will be marshalled. // // client.LR(). // SetBody(User{ // Username: "jeeva@myjeeva.com", // Password: "welcome2resty", // }) // // Map as a body input, based on content type, it will be marshalled. // // client.LR(). // SetBody(map[string]interface{}{ // "username": "jeeva@myjeeva.com", // "password": "welcome2resty", // "address": &Address{ // Address1: "1111 This is my street", // Address2: "Apt 201", // City: "My City", // State: "My State", // ZipCode: 00000, // }, // }) // // String as a body input. Suitable for any need as a string input. // // client.LR(). // SetBody(`{ // "username": "jeeva@getrightcare.com", // "password": "admin" // }`) // // []byte as a body input. Suitable for raw request such as file upload, serialize & deserialize, etc. // // client.LR(). // SetBody([]byte("This is my raw request, sent as-is")) func (lReq *LazyRequest) SetBody(body interface{}) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetBody(body) }) return lReq } // Deprecated: This usage is intentionally not supported. func (lReq *LazyRequest) SetResult() { panic("do not use this in LazyRequest") } // Deprecated: This usage is intentionally not supported. func (lReq *LazyRequest) SetError() { panic("do not use this in LazyRequest") } // SetFile method is to set single file field name and its path for multipart upload. // // client.LR(). // SetFile("my_file", "/Users/jeeva/Gas Bill - Sep.pdf") func (lReq *LazyRequest) SetFile(param, filePath string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetFile(param, filePath) }) return lReq } // SetFiles method is to set multiple file field name and its path for multipart upload. // // client.LR(). // SetFiles(map[string]string{ // "my_file1": "/Users/jeeva/Gas Bill - Sep.pdf", // "my_file2": "/Users/jeeva/Electricity Bill - Sep.pdf", // "my_file3": "/Users/jeeva/Water Bill - Sep.pdf", // }) func (lReq *LazyRequest) SetFiles(files map[string]string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetFiles(files) }) return lReq } // SetFileReader method is to set single file using io.Reader for multipart upload. // // client.LR(). // SetFileReader("profile_img", "my-profile-img.png", bytes.NewReader(profileImgBytes)). // SetFileReader("notes", "user-notes.txt", bytes.NewReader(notesBytes)) func (lReq *LazyRequest) SetFileReader(param, fileName string, reader io.Reader) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetFileReader(param, fileName, reader) }) return lReq } // SetMultipartFormData method allows simple form data to be attached to the request as `multipart:form-data`. func (lReq *LazyRequest) SetMultipartFormData(data map[string]string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetMultipartFormData(data) }) return lReq } // SetMultipartField method is to set custom data using io.Reader for multipart upload. func (lReq *LazyRequest) SetMultipartField(param, fileName, contentType string, reader io.Reader) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetMultipartField(param, fileName, contentType, reader) }) return lReq } // SetMultipartFields method is to set multiple data fields using io.Reader for multipart upload. // // For Example: // // client.LR().SetMultipartFields( // &resty.MultipartField{ // Param: "uploadManifest1", // FileName: "upload-file-1.json", // ContentType: "application/json", // Reader: strings.NewReader(`{"input": {"name": "Uploaded document 1", "_filename" : ["file1.txt"]}}`), // }, // &resty.MultipartField{ // Param: "uploadManifest2", // FileName: "upload-file-2.json", // ContentType: "application/json", // Reader: strings.NewReader(`{"input": {"name": "Uploaded document 2", "_filename" : ["file2.txt"]}}`), // }) // // If you have slice already, then simply call- // // client.LR().SetMultipartFields(fields...) func (lReq *LazyRequest) SetMultipartFields(fields ...*resty.MultipartField) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetMultipartFields(fields...) }) return lReq } // SetContentLength method sets the HTTP header `Content-Length` value for current request. // By default Resty won't set `Content-Length`. Also you have an option to enable for every // request. func (lReq *LazyRequest) SetContentLength(l bool) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetContentLength(l) }) return lReq } // SetBasicAuth method sets the basic authentication header in the current HTTP request. // // For Example: // // Authorization: Basic // // To set the header for username "go-resty" and password "welcome" // // client.LR().SetBasicAuth("go-resty", "welcome") // // This method overrides the credentials set by method `Client.SetBasicAuth`. func (lReq *LazyRequest) SetBasicAuth(username, password string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetBasicAuth(username, password) }) return lReq } // SetAuthToken method sets the auth token header(Default Scheme: Bearer) in the current HTTP request. Header example: // // Authorization: Bearer // // For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F // // client.LR().SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F") // // This method overrides the Auth token set by method `Client.SetAuthToken`. func (lReq *LazyRequest) SetAuthToken(token string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetAuthToken(token) }) return lReq } // SetAuthScheme method sets the auth token scheme type in the HTTP request. For Example: // // Authorization: // // For Example: To set the scheme to use OAuth // // client.LR().SetAuthScheme("OAuth") // // This auth header scheme gets added to all the request rasied from this client instance. // Also it can be overridden or set one at the request level is supported. // // Information about Auth schemes can be found in RFC7235 which is linked to below along with the page containing // the currently defined official authentication schemes: // // https://tools.ietf.org/html/rfc7235 // https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes // // This method overrides the Authorization scheme set by method `Client.SetAuthScheme`. func (lReq *LazyRequest) SetAuthScheme(scheme string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetAuthScheme(scheme) }) return lReq } // Deprecated: This usage is intentionally not supported. func (lReq *LazyRequest) SetOutput() { panic("do not use this in LazyRequest") } // SetSRV method sets the details to query the service SRV record and execute the // request. // // client.LR(). // SetSRV(SRVRecord{"web", "testservice.com"}). // Get("/get") func (lReq *LazyRequest) SetSRV(srv *resty.SRVRecord) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetSRV(srv) }) return lReq } // Deprecated: This usage is intentionally not supported. func (lReq *LazyRequest) SetDoNotParseResponse() { panic("do not use this in LazyRequest") } // SetPathParam method sets single URL path key-value pair in the // Resty current request instance. // // client.LR().SetPathParam("userId", "sample@sample.com") // // Result: // URL - /v1/users/{userId}/details // Composed URL - /v1/users/sample@sample.com/details // // It replaces the value of the key while composing the request URL. Also you can // override Path Params value, which was set at client instance level. func (lReq *LazyRequest) SetPathParam(param, value string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetPathParam(param, value) }) return lReq } // SetPathParams method sets multiple URL path key-value pairs at one go in the // Resty current request instance. // // client.LR().SetPathParams(map[string]string{ // "userId": "sample@sample.com", // "subAccountId": "100002", // }) // // Result: // URL - /v1/users/{userId}/{subAccountId}/details // Composed URL - /v1/users/sample@sample.com/100002/details // // It replaces the value of the key while composing request URL. Also you can // override Path Params value, which was set at client instance level. func (lReq *LazyRequest) SetPathParams(params map[string]string) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetPathParams(params) }) return lReq } // Deprecated: This usage is intentionally not supported. func (lReq *LazyRequest) ExpectContentType() { panic("do not use this in LazyRequest") } // Deprecated: This usage is intentionally not supported. func (lReq *LazyRequest) ForceContentType() { panic("do not use this in LazyRequest") } // SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal. // // Note: This option only applicable to standard JSON Marshaller. func (lReq *LazyRequest) SetJSONEscapeHTML(b bool) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetJSONEscapeHTML(b) }) return lReq } // SetCookie method appends a single cookie in the current request instance. // // client.LR().SetCookie(&http.Cookie{ // Name:"go-resty", // Value:"This is cookie value", // }) // // Note: Method appends the Cookie value into existing Cookie if already existing. func (lReq *LazyRequest) SetCookie(hc *http.Cookie) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetCookie(hc) }) return lReq } // SetCookies method sets an array of cookies in the current request instance. // // cookies := []*http.Cookie{ // &http.Cookie{ // Name:"go-resty-1", // Value:"This is cookie 1 value", // }, // &http.Cookie{ // Name:"go-resty-2", // Value:"This is cookie 2 value", // }, // } // // // Setting a cookies into resty's current request // client.LR().SetCookies(cookies) // // Note: Method appends the Cookie value into existing Cookie if already existing. func (lReq *LazyRequest) SetCookies(rs []*http.Cookie) *LazyRequest { lReq.opsR = append(lReq.opsR, func(r *resty.Request) { r.SetCookies(rs) }) return lReq } ================================================ FILE: util/client/httpclient/response.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package httpclient import ( "encoding/json" "fmt" "io" "net/http" "os" "runtime" "runtime/debug" "strings" "time" "github.com/go-resty/resty/v2" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/israce" "github.com/pingcap/tidb-dashboard/util/nocopy" ) const ( defaultTimeout = time.Minute * 5 // Just a default long enough timeout. ) type requestUpdateFn func(r *resty.Request) type clientUpdateFn func(c *resty.Client) // LazyResponse provides access to the response body and response headers in convenient ways. // No request is actually sent until LazyResponse is read. type LazyResponse struct { nocopy.NoCopy // The source request object to execute. It is a clone of the original request object // to allow concurrent executions. requestSnapshot *LazyRequest // stackAtNew is the stack when Response is created. It is only available when Golang race mode is enabled. // This is used to report the missing `Close()` calls. stackAtNew []byte // Fields below are set only after the request is actually sent. isExecuted bool executedResponseWithoutBody *http.Response executedResponseBody io.ReadCloser executedError error executeInfo *execInfo // Contains some execution information. Will be logged when error happens. } func newResponse(sourceSnapshot *LazyRequest) *LazyResponse { er := &LazyResponse{ requestSnapshot: sourceSnapshot, } runtime.SetFinalizer(er, (*LazyResponse).finalize) if israce.Enabled { er.stackAtNew = debug.Stack() } return er } func (lResp *LazyResponse) doExecutionOnce() { if lResp.isExecuted { return } client := resty.NewWithClient(&http.Client{ Transport: lResp.requestSnapshot.transport, }) client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10)) client.SetTimeout(defaultTimeout) if lResp.requestSnapshot.debugTag != "" { client.SetPreRequestHook(func(_ *resty.Client, rr *http.Request) error { log.Info("Send request", zap.String("kindTag", lResp.requestSnapshot.kindTag), zap.String("debugTag", lResp.requestSnapshot.debugTag), zap.String("method", rr.Method), zap.String("url", rr.URL.String())) return nil }) } for _, op := range lResp.requestSnapshot.opsC { op(client) } restyReq := client.R() restyReq.SetDoNotParseResponse(true) for _, op := range lResp.requestSnapshot.opsR { op(restyReq) } info := &execInfo{kindTag: lResp.requestSnapshot.kindTag} info.reqURL = restyReq.URL info.reqMethod = restyReq.Method restyResp, err := restyReq.Send() if err != nil { // Turn all errors into ErrRequestFailed. err = ErrRequestFailed.WrapWithNoMessage(err) } if (restyResp == nil || restyResp.RawResponse == nil) && err == nil { // Response and error come from 3rd-party libraries, we are not sure about this. // Let's try out best to catch it. err = ErrRequestFailed.New("%s %s (%s): internal error, no response", restyResp.Request.Method, restyResp.Request.URL, lResp.requestSnapshot.kindTag) restyResp = nil } if !restyResp.IsSuccess() && err == nil { // Turn all non success responses to an error, like 301 Moved Permanently and 404 Not Found. // Note: IsError != !IsSuccess. err = ErrRequestFailed.New("%s %s (%s): Response status %d", restyResp.Request.Method, restyResp.Request.URL, lResp.requestSnapshot.kindTag, restyResp.StatusCode()) } if err != nil { // Turn response into nil when there is an error. if restyResp != nil && restyResp.RawResponse != nil { data, _ := io.ReadAll(restyResp.RawResponse.Body) _ = restyResp.RawResponse.Body.Close() info.respStatus = restyResp.Status() info.respBody = string(data) restyResp = nil } info.Warn("Request failed", err) } if restyResp != nil && restyResp.RawResponse != nil { lResp.executedResponseBody = restyResp.RawResponse.Body lResp.executedResponseWithoutBody = restyResp.RawResponse restyResp.RawResponse.Body = nil } else { lResp.executedResponseBody = nil lResp.executedResponseWithoutBody = nil } lResp.executedError = err lResp.executeInfo = info lResp.isExecuted = true // The request is executed, no need to schedule a check for the execution any more. runtime.SetFinalizer(lResp, nil) } func (lResp *LazyResponse) close() { _ = lResp.executedResponseBody.Close() } // Finish closes the response and discard any unreaded response body. Read is not possible after that. // The returned raw response does not have a body. To read the body, call PipeBody, ReadBodyAsBytes, // ReadBodyAsString or ReadBodyAsJSON. // If the response status code is not a success status, an error will be returned. func (lResp *LazyResponse) Finish() (respNoBody *http.Response, err error) { lResp.doExecutionOnce() if lResp.executedError != nil { return nil, lResp.executedError } respNoBody = lResp.executedResponseWithoutBody lResp.close() return } // PipeBody pipes the body of the response to a writer. // If the response status code is not a success status, an error will be returned. func (lResp *LazyResponse) PipeBody(w io.Writer) (written int64, respNoBody *http.Response, err error) { lResp.doExecutionOnce() if lResp.executedError != nil { return 0, nil, lResp.executedError } respNoBody = lResp.executedResponseWithoutBody written, err = io.Copy(w, lResp.executedResponseBody) if err != nil { respNoBody = nil err = ErrRequestFailed.WrapWithNoMessage(err) lResp.executeInfo.Warn("Request failed", err) } lResp.close() return } // ReadBodyAsBytes reads all body content of the response to a byte slice. // If the response status code is not a success status, an error will be returned. func (lResp *LazyResponse) ReadBodyAsBytes() (bytes []byte, respNoBody *http.Response, err error) { lResp.doExecutionOnce() if lResp.executedError != nil { return nil, nil, lResp.executedError } respNoBody = lResp.executedResponseWithoutBody bytes, err = io.ReadAll(lResp.executedResponseBody) if err != nil { bytes = nil respNoBody = nil err = ErrRequestFailed.WrapWithNoMessage(err) lResp.executeInfo.Warn("Request failed", err) } lResp.close() return } // ReadBodyAsString reads all body content of the response to a string. // If the response status code is not a success status, an error will be returned. func (lResp *LazyResponse) ReadBodyAsString() (data string, respNoBody *http.Response, err error) { bytes, resp, err := lResp.ReadBodyAsBytes() if err != nil { return "", nil, err } return strings.TrimSpace(string(bytes)), resp, nil } // ReadBodyAsJSON reads all body content of the response as JSON and does unmarshal. // If the response status code is not a success status, an error will be returned. func (lResp *LazyResponse) ReadBodyAsJSON(destination interface{}) (respNoBody *http.Response, err error) { bytes, resp, err := lResp.ReadBodyAsBytes() if err != nil { return nil, err } err = json.Unmarshal(bytes, destination) if err != nil { err = ErrRequestFailed.WrapWithNoMessage(err) ei := *lResp.executeInfo ei.respStatus = lResp.executedResponseWithoutBody.Status ei.respBody = string(bytes) ei.Warn("Request failed", err) return nil, err } return resp, nil } func (lResp *LazyResponse) finalize() { if israce.Enabled { // try to catch incorrect usages _, _ = os.Stderr.Write(lResp.stackAtNew) panic(fmt.Sprintf("%T is not used correctly, one of PipeBody(), ReadBodyAsBytes(), ReadBodyAsString(), ReadBodyAsJSON() or Finish() must be called", lResp)) } // If a LazyResponse is GCed without actually sending the request, then we can just do nothing. // There is even no need to close the response body, since the request is not sent. } ================================================ FILE: util/client/httpclient/response_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package httpclient import ( "bytes" "context" "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io" "net" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/joomcode/errorx" "github.com/stretchr/testify/require" "go.uber.org/atomic" ) func TestReadBodyAsString(t *testing.T) { requestTimes := atomic.Int32{} responseStatus := atomic.Int32{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requestTimes.Inc() w.WriteHeader(int(responseStatus.Load())) _, _ = fmt.Fprintf(w, "Basically OK, Req #%d", requestTimes.Load()) })) defer ts.Close() client := New(Config{}) responseStatus.Store(200) req := client.LR() resp := req.Get(ts.URL) responseStatus.Store(202) // Lazy request require.Equal(t, int32(0), requestTimes.Load()) // Lazy request dataStr, rawResp, err := resp.ReadBodyAsString() require.Equal(t, int32(1), requestTimes.Load()) require.NoError(t, err) require.Equal(t, "Basically OK, Req #1", dataStr) require.Nil(t, rawResp.Body) require.Equal(t, 202, rawResp.StatusCode) // Due to lazy request, we should get 202 // Read again should result in error dataStrE, rawRespE, err := resp.ReadBodyAsString() require.Equal(t, int32(1), requestTimes.Load()) require.Contains(t, err.Error(), "read on closed response body") require.Empty(t, dataStrE) require.Nil(t, rawRespE) // Other kind of read operations should also result in error bytesE, rawRespE, err := resp.ReadBodyAsBytes() require.Equal(t, int32(1), requestTimes.Load()) require.Contains(t, err.Error(), "read on closed response body") require.Nil(t, bytesE) require.Nil(t, rawRespE) // Test sending a new request via Get() over the same request again responseStatus.Store(201) resp = req.Get(ts.URL) require.Equal(t, int32(1), requestTimes.Load()) dataStr2, rawResp2, err := resp.ReadBodyAsString() require.Equal(t, int32(2), requestTimes.Load()) require.NoError(t, err) require.Equal(t, "Basically OK, Req #2", dataStr2) require.Nil(t, rawResp2.Body) require.Equal(t, 202, rawResp.StatusCode) // The previous response should not be changed by a new request require.Equal(t, 201, rawResp2.StatusCode) // Sending a new request via LR() over the same client responseStatus.Store(200) resp = client.LR().Get(ts.URL) require.Equal(t, int32(2), requestTimes.Load()) dataStr3, rawResp3, err := resp.ReadBodyAsString() require.Equal(t, int32(3), requestTimes.Load()) require.NoError(t, err) require.Equal(t, "Basically OK, Req #3", dataStr3) require.Nil(t, rawResp3.Body) require.Equal(t, 202, rawResp.StatusCode) require.Equal(t, 201, rawResp2.StatusCode) require.Equal(t, 200, rawResp3.StatusCode) // Sending a new request via creating a new client client2 := New(Config{}) responseStatus.Store(202) resp = client2.LR().Get(ts.URL) require.Equal(t, int32(3), requestTimes.Load()) dataStr4, rawResp4, err := resp.ReadBodyAsString() require.Equal(t, int32(4), requestTimes.Load()) require.NoError(t, err) require.Equal(t, "Basically OK, Req #4", dataStr4) require.Nil(t, rawResp4.Body) require.Equal(t, 202, rawResp.StatusCode) require.Equal(t, 201, rawResp2.StatusCode) require.Equal(t, 200, rawResp3.StatusCode) require.Equal(t, 202, rawResp4.StatusCode) } func TestFinish(t *testing.T) { requestTimes := atomic.Int32{} responseStatus := atomic.Int32{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requestTimes.Inc() w.WriteHeader(int(responseStatus.Load())) _, _ = fmt.Fprintf(w, "Basically OK, Req #%d", requestTimes.Load()) })) defer ts.Close() client := New(Config{}) responseStatus.Store(200) resp := client.LR().Get(ts.URL) responseStatus.Store(202) // Lazy request require.Equal(t, int32(0), requestTimes.Load()) // Lazy request rawResp, err := resp.Finish() require.Equal(t, int32(1), requestTimes.Load()) require.NoError(t, err) require.Nil(t, rawResp.Body) require.Equal(t, 202, rawResp.StatusCode) // Call Finish() again should not send a new request rawResp, err = resp.Finish() require.Equal(t, int32(1), requestTimes.Load()) require.NoError(t, err) require.Nil(t, rawResp.Body) require.Equal(t, 202, rawResp.StatusCode) // Read after Finish() should become errors dataStrE, rawRespE, err := resp.ReadBodyAsString() require.Equal(t, int32(1), requestTimes.Load()) require.Contains(t, err.Error(), "read on closed response body") require.Empty(t, dataStrE) require.Nil(t, rawRespE) bytesE, rawRespE, err := resp.ReadBodyAsBytes() require.Equal(t, int32(1), requestTimes.Load()) require.Contains(t, err.Error(), "read on closed response body") require.Nil(t, bytesE) require.Nil(t, rawRespE) // Finish() after read is fine. resp = client.LR().Get(ts.URL) responseStatus.Store(200) require.Equal(t, int32(1), requestTimes.Load()) dataStr, rawResp, err := resp.ReadBodyAsString() require.Equal(t, int32(2), requestTimes.Load()) require.NoError(t, err) require.Equal(t, "Basically OK, Req #2", dataStr) require.Nil(t, rawResp.Body) require.Equal(t, 200, rawResp.StatusCode) rawResp2, err := resp.Finish() require.Equal(t, int32(2), requestTimes.Load()) require.NoError(t, err) require.Same(t, rawResp, rawResp2) } func TestReadBodyAsJSON(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprintln(w, `{"foo":"bar"}`) })) defer ts.Close() // Unmarshal into map client := New(Config{}) var respMap map[string]interface{} rawResp, err := client.LR().Get(ts.URL).ReadBodyAsJSON(&respMap) require.NoError(t, err) require.Equal(t, http.StatusOK, rawResp.StatusCode) expectedMap := map[string]interface{}{ "foo": "bar", } require.Equal(t, expectedMap, respMap) // Unmarshal into struct type Response struct { Foo string `json:"foo"` } var respStruct Response req := client.LR().Get(ts.URL) rawResp, err = req.ReadBodyAsJSON(&respStruct) require.NoError(t, err) require.Equal(t, http.StatusOK, rawResp.StatusCode) require.Equal(t, Response{Foo: "bar"}, respStruct) } func TestReadBodyAsJSON_UnmarshalFailure(t *testing.T) { requestTimes := atomic.Int32{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requestTimes.Inc() _, _ = fmt.Fprintln(w, `bad_json`) })) defer ts.Close() client := New(Config{}) var respMap map[string]interface{} require.Equal(t, int32(0), requestTimes.Load()) req := client.LR().Get(ts.URL) rawResp, err := req.ReadBodyAsJSON(&respMap) require.Equal(t, int32(1), requestTimes.Load()) require.Contains(t, err.Error(), "invalid character") require.Nil(t, rawResp) require.Nil(t, respMap) // Read JSON again should not send new request rawResp, err = req.ReadBodyAsJSON(&respMap) require.Equal(t, int32(1), requestTimes.Load()) require.Contains(t, err.Error(), "read on closed response body") require.Nil(t, rawResp) require.Nil(t, respMap) // Finish should success without sending new requests // Unlike other Read errors, for unmarshal errors, Finish() will succeed since an OK response is read successfully rawResp, err = req.Finish() require.Equal(t, int32(1), requestTimes.Load()) require.NoError(t, err) require.Equal(t, http.StatusOK, rawResp.StatusCode) } type myWriter struct { writedBytes int writeCalled int errorRaised int } func (w *myWriter) Write(p []byte) (int, error) { w.writeCalled++ if w.writedBytes > 5 { w.errorRaised++ return 0, fmt.Errorf("write too many bytes") } w.writedBytes += len(p) return len(p), nil } func TestPipeBody(t *testing.T) { requestTimes := atomic.Int32{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requestTimes.Inc() _, _ = fmt.Fprintln(w, "Hello world") })) defer ts.Close() client := New(Config{}) buf := bytes.Buffer{} require.Equal(t, int32(0), requestTimes.Load()) req := client.LR().Get(ts.URL) wBytes, rawResp, err := req.PipeBody(&buf) require.Equal(t, int32(1), requestTimes.Load()) require.NoError(t, err) require.Equal(t, http.StatusOK, rawResp.StatusCode) require.Equal(t, "Hello world\n", buf.String()) require.Equal(t, int64(12), wBytes) // The copy chunk size is large, so that there will be only one write call to the writer w := myWriter{} require.Equal(t, int32(1), requestTimes.Load()) wBytes, rawResp, err = client.LR().Get(ts.URL).PipeBody(&w) require.Equal(t, int32(2), requestTimes.Load()) require.Equal(t, int64(12), wBytes) require.NoError(t, err) require.Equal(t, http.StatusOK, rawResp.StatusCode) require.Equal(t, 1, w.writeCalled) require.Equal(t, 12, w.writedBytes) require.Equal(t, 0, w.errorRaised) // Now the server write data chunk by chunk... ctx, cancel := context.WithCancel(context.Background()) ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requestTimes.Inc() _, _ = w.Write([]byte("Partial...")) w.(http.Flusher).Flush() select { case <-ctx.Done(): return case <-time.After(1 * time.Second): _, _ = fmt.Fprintln(w, "Done") } })) defer ts.Close() defer cancel() // PipeData should produce data chunk by chunk w = myWriter{} require.Equal(t, int32(2), requestTimes.Load()) resp := client.LR().Get(ts.URL) wBytes, rawResp, err = resp.PipeBody(&w) require.Equal(t, int32(3), requestTimes.Load()) require.Equal(t, int64(10), wBytes) require.Error(t, err) require.Contains(t, err.Error(), "write too many bytes") require.Nil(t, rawResp) require.Equal(t, 2, w.writeCalled) require.Equal(t, 10, w.writedBytes) // The size of the first chunk require.Equal(t, 1, w.errorRaised) // Call PipeBody again should fail due to response is closed wBytes, rawResp, err = resp.PipeBody(&w) require.Equal(t, int32(3), requestTimes.Load()) require.Equal(t, int64(0), wBytes) require.Error(t, err) require.Contains(t, err.Error(), "read on closed response body") require.Nil(t, rawResp) require.Equal(t, 2, w.writeCalled) // Unchanged require.Equal(t, 10, w.writedBytes) require.Equal(t, 1, w.errorRaised) // PipeBody should copy all data when there are multiple chunks from the server buf = bytes.Buffer{} require.Equal(t, int32(3), requestTimes.Load()) req = client.LR().Get(ts.URL) wBytes, rawResp, err = req.PipeBody(&buf) require.Equal(t, int32(4), requestTimes.Load()) require.NoError(t, err) require.Equal(t, http.StatusOK, rawResp.StatusCode) require.Equal(t, "Partial...Done\n", buf.String()) require.Equal(t, int64(15), wBytes) } func TestResponseHeader(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Add("foo", "bar") w.WriteHeader(http.StatusAlreadyReported) _, _ = fmt.Fprintln(w, "Fine!") })) defer ts.Close() client := New(Config{}) resp := client.LR().Get(ts.URL) rawResp, err := resp.Finish() require.NoError(t, err) require.Equal(t, http.StatusAlreadyReported, rawResp.StatusCode) require.Equal(t, "bar", rawResp.Header.Get("foo")) } func TestSetURL(t *testing.T) { ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprintln(w, "Result from server 1") })) defer ts1.Close() ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprintln(w, "Result from server 2") })) defer ts2.Close() client := New(Config{}) req := client.LR() // SetXxx should make changes in place r1 := req.SetURL(ts1.URL) r2 := req.SetURL(ts2.URL) require.Same(t, r1, r2) dataStr, _, err := r1.Send().ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 2", dataStr) dataStr, _, err = r2.Send().ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 2", dataStr) r1.SetURL(ts1.URL) dataStr, _, err = r2.Send().ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 1", dataStr) // SetURL should not affect another request in the same client req2 := client.LR() req2.SetURL(ts2.URL) dataStr, _, err = r1.Send().ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 1", dataStr) dataStr, _, err = r2.Send().ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 1", dataStr) dataStr, _, err = req2.Send().ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 2", dataStr) dataStr, _, err = req.Send().ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 1", dataStr) dataStr, _, err = r1.Send().ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 1", dataStr) } func TestLR(t *testing.T) { client := New(Config{}) req1 := client.LR() req2 := client.LR() require.NotSame(t, req1, req2) } func TestGet(t *testing.T) { ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprintln(w, "Result from server 1") })) defer ts1.Close() ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprintln(w, "Result from server 2") })) defer ts2.Close() client := New(Config{}) // "Get" from different requests should not affect each other resp1 := client.LR().Get(ts1.URL) resp2 := client.LR().Get(ts2.URL) dataStr, _, err := resp1.ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 1", dataStr) dataStr, _, err = resp2.ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 2", dataStr) // "Get" should not affect each other req := client.LR() resp1 = req.Get(ts1.URL) resp2 = req.Get(ts2.URL) dataStr, _, err = resp1.ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 1", dataStr) dataStr, _, err = resp2.ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 2", dataStr) resp3 := req.Get(ts1.URL) dataStr, _, err = resp3.ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 1", dataStr) // "Get()" should not affect the previous "SetURL()" call req = client.LR() req.SetURL(ts1.URL) dataStr, _, err = req.Get(ts2.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 2", dataStr) dataStr, _, err = req.Send().ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 1", dataStr) // "SetURL()" should not affect the previous "Get()" call req = client.LR() resp1 = req.Get(ts1.URL) req.SetURL(ts2.URL) dataStr, _, err = resp1.ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 1", dataStr) dataStr, _, err = req.Send().ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Result from server 2", dataStr) } func TestPost(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) _, _ = fmt.Fprintf(w, "Body is %s", string(body)) })) defer ts.Close() client := New(Config{}) // SetBody from different requests should not affect each other r1 := client.LR().SetBody("foo") dataStr, _, err := r1.Post(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Body is foo", dataStr) r2 := client.LR().SetBody("bar") dataStr, _, err = r2.Post(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Body is bar", dataStr) dataStr, _, err = r1.Post(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Body is foo", dataStr) } func TestSetHeader(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, r.Header.Get("X-Test")) })) defer ts.Close() client := New(Config{}) req := client.LR().SetHeader("X-Test", "foobar") // SetHeader from different requests should not affect each other dataStr, _, err := req.Get(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "foobar", dataStr) dataStr, _, err = client.LR().Get(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "", dataStr) dataStr, _, err = req.Get(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "foobar", dataStr) // SetHeader after Get should not taking effect req = client.LR() resp := req.Get(ts.URL) req.SetHeader("X-Test", "hello") dataStr, _, err = resp.ReadBodyAsString() require.NoError(t, err) require.Equal(t, "", dataStr) resp = req.Get(ts.URL) dataStr, _, err = resp.ReadBodyAsString() require.NoError(t, err) require.Equal(t, "hello", dataStr) } func TestSetTLSAwareBaseURL(t *testing.T) { ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, "ts1"+r.URL.Path) })) defer ts1.Close() ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, "ts2"+r.URL.Path) })) defer ts2.Close() client := New(Config{}) dataStr, _, err := client.LR().SetTLSAwareBaseURL(ts1.URL).Get("/foo").ReadBodyAsString() require.NoError(t, err) require.Equal(t, "ts1/foo", dataStr) // base url can be overwritten dataStr, _, err = client.LR().SetTLSAwareBaseURL(ts1.URL).Get(ts2.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "ts2/", dataStr) // Rewrite http:// to https:// if TLS config is specified tsTLS := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, "tsTLS"+r.URL.Path) })) defer tsTLS.Close() httpURL := "http://" + tsTLS.Listener.Addr().String() certpool := x509.NewCertPool() certpool.AddCert(tsTLS.Certificate()) client = New(Config{TLSConfig: &tls.Config{ RootCAs: certpool, }}) // #nosec G402 _, _, err = client.LR().Get(httpURL).ReadBodyAsString() require.Error(t, err) require.Contains(t, err.Error(), "Response status 400") dataStr, _, err = client.LR().SetTLSAwareBaseURL(httpURL).Get("/bar").ReadBodyAsString() require.NoError(t, err) require.Equal(t, "tsTLS/bar", dataStr) } func TestFailureStatusCode(t *testing.T) { requestTimes := atomic.Int32{} responseStatus := atomic.Int32{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requestTimes.Inc() w.WriteHeader(int(responseStatus.Load())) _, _ = fmt.Fprintf(w, "Fail from req #%d", requestTimes.Load()) })) defer ts.Close() // Although request succeeded, failure status code will turn into errors by design. client := New(Config{}) // ReadBodyAsBytes should fail responseStatus.Store(500) require.Equal(t, int32(0), requestTimes.Load()) bytes, rawResp, err := client.LR().Get(ts.URL).ReadBodyAsBytes() require.Equal(t, int32(1), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), "Response status 500") require.Nil(t, bytes) require.Nil(t, rawResp) // ReadBodyAsString should return empty string responseStatus.Store(400) require.Equal(t, int32(1), requestTimes.Load()) resp := client.LR().Get(ts.URL) dataStr, rawResp, err := resp.ReadBodyAsString() require.Equal(t, int32(2), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), "Response status 400") require.Empty(t, dataStr) require.Nil(t, rawResp) // Read again after failure should not send request again responseStatus.Store(500) dataStr, rawResp, err = resp.ReadBodyAsString() require.Equal(t, int32(2), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), "Response status 400") require.Empty(t, dataStr) require.Nil(t, rawResp) // ReadBodyAsJSON should fail var respMap map[string]interface{} resp = client.LR().Get(ts.URL) rawResp, err = resp.ReadBodyAsJSON(respMap) require.Equal(t, int32(3), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), "Response status 500") require.Empty(t, dataStr) require.Nil(t, rawResp) require.Nil(t, respMap) rawResp, err = resp.ReadBodyAsJSON(respMap) require.Equal(t, int32(3), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), "Response status 500") require.Empty(t, dataStr) require.Nil(t, rawResp) require.Nil(t, respMap) // Finish should fail responseStatus.Store(404) require.Equal(t, int32(3), requestTimes.Load()) resp = client.LR().Get(ts.URL) rawResp, err = resp.Finish() require.Equal(t, int32(4), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), "Response status 404") require.Nil(t, rawResp) // Finish again after failure should not send request again responseStatus.Store(200) rawResp, err = resp.Finish() require.Equal(t, int32(4), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), "Response status 404") require.Nil(t, rawResp) // Mix Finish() and ReadBodyAsString() responseStatus.Store(403) dataStr, rawResp, err = resp.ReadBodyAsString() require.Equal(t, int32(4), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), "Response status 404") require.Empty(t, dataStr) require.Nil(t, rawResp) responseStatus.Store(200) rawResp, err = resp.Finish() require.Equal(t, int32(4), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), "Response status 404") require.Nil(t, rawResp) } func TestBadServer(t *testing.T) { requestTimes := atomic.Int32{} listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) go func() { for { conn, err := listener.Accept() if err != nil { return } requestTimes.Inc() _, _ = conn.Write([]byte("Hello")) _ = conn.Close() } }() defer func() { _ = listener.Close() }() url := fmt.Sprintf("http://%s/foo", listener.Addr().String()) client := New(Config{}) // ReadBodyAsString should return empty string require.Equal(t, int32(0), requestTimes.Load()) resp := client.LR().Get(url) dataStr, rawResp, err := resp.ReadBodyAsString() require.Equal(t, int32(1), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Empty(t, dataStr) require.Nil(t, rawResp) // Call multiple times dataStr, rawResp, err = resp.ReadBodyAsString() require.Equal(t, int32(1), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Empty(t, dataStr) require.Nil(t, rawResp) // Response should fail require.Equal(t, int32(1), requestTimes.Load()) resp = client.LR().Get(url) rawResp, err = resp.Finish() require.Equal(t, int32(2), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Nil(t, rawResp) // Call multiple times rawResp, err = resp.Finish() require.Equal(t, int32(2), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Nil(t, rawResp) // Mix Finish() and ReadBodyAsString() dataStr, rawResp, err = resp.ReadBodyAsString() require.Equal(t, int32(2), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Empty(t, dataStr) require.Nil(t, rawResp) rawResp, err = resp.Finish() require.Equal(t, int32(2), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Nil(t, rawResp) // ReadBodyASJSON should fail require.Equal(t, int32(2), requestTimes.Load()) resp = client.LR().Get(url) var respMap map[string]interface{} rawResp, err = resp.ReadBodyAsJSON(respMap) require.Equal(t, int32(3), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Nil(t, rawResp) require.Nil(t, respMap) // Call multiple times rawResp, err = resp.ReadBodyAsJSON(respMap) require.Equal(t, int32(3), requestTimes.Load()) require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Nil(t, rawResp) require.Nil(t, respMap) } func TestBadScheme(t *testing.T) { client := New(Config{}) bytes, rawResp, err := client.LR().Get("foo://abc.com").ReadBodyAsBytes() require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), `unsupported protocol scheme "foo"`) require.Nil(t, bytes) require.Nil(t, rawResp) rawResp, err = client.LR().Get("bar://abc.com").Finish() require.True(t, errorx.IsOfType(err, ErrRequestFailed)) require.Contains(t, err.Error(), `unsupported protocol scheme "bar"`) require.Nil(t, rawResp) } func TestConnectionReuse(t *testing.T) { newConn := atomic.Int32{} closedConn := atomic.Int32{} requestTimes := atomic.Int32{} ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requestTimes.Inc() _, _ = fmt.Fprintf(w, "Req #%d", requestTimes.Load()) })) ts.Config.ConnState = func(_ net.Conn, cs http.ConnState) { switch cs { case http.StateNew: newConn.Inc() case http.StateHijacked, http.StateClosed: closedConn.Inc() default: // we do not care other states } } ts.Start() defer ts.Close() require.Equal(t, int32(0), newConn.Load()) require.Equal(t, int32(0), closedConn.Load()) client := New(Config{}) dataStr, _, err := client.LR().Get(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Req #1", dataStr) require.Equal(t, int32(1), newConn.Load()) require.Equal(t, int32(0), closedConn.Load()) // Use the same client to send request, the connection is expected to be reused dataStr, _, err = client.LR().Get(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Req #2", dataStr) require.Equal(t, int32(1), newConn.Load()) require.Equal(t, int32(0), closedConn.Load()) // A new client should create a new connection client2 := New(Config{}) dataStr, _, err = client2.LR().Get(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Req #3", dataStr) require.Equal(t, int32(2), newConn.Load()) require.Equal(t, int32(0), closedConn.Load()) // Connections are reused dataStr, _, err = client.LR().Get(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Req #4", dataStr) require.Equal(t, int32(2), newConn.Load()) require.Equal(t, int32(0), closedConn.Load()) dataStr, _, err = client2.LR().Get(ts.URL).ReadBodyAsString() require.NoError(t, err) require.Equal(t, "Req #5", dataStr) require.Equal(t, int32(2), newConn.Load()) require.Equal(t, int32(0), closedConn.Load()) } func TestClone(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { m := make(map[string]string) for header, value := range r.Header { if strings.HasPrefix(header, "X-") { m[header] = value[0] } } j, _ := json.Marshal(m) _, _ = w.Write(j) })) defer ts.Close() client := New(Config{}) req1 := client.LR() req1.SetHeader("x-req1header1", "value1") req2 := req1.Clone() // After clone, they will not affect each other req1.SetHeader("x-req1header2", "value2") req2.SetHeader("x-req2header1", "value1") dataStr, _, err := req1.Get(ts.URL).ReadBodyAsString() require.NoError(t, err) require.JSONEq(t, `{"X-Req1header1":"value1","X-Req1header2":"value2"}`, dataStr) dataStr, _, err = req2.Get(ts.URL).ReadBodyAsString() require.NoError(t, err) require.JSONEq(t, `{"X-Req1header1":"value1","X-Req2header1":"value1"}`, dataStr) } func TestTimeoutHeader(t *testing.T) { requestTimes := atomic.Int32{} ctx, cancel := context.WithCancel(context.Background()) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requestTimes.Inc() select { case <-ctx.Done(): w.WriteHeader(http.StatusGatewayTimeout) case <-time.After(1 * time.Second): _, _ = fmt.Fprintln(w, "OK") } })) defer ts.Close() defer cancel() client := New(Config{}) tBegin := time.Now() require.Equal(t, int32(0), requestTimes.Load()) resp := client.LR().SetTimeout(100 * time.Millisecond).Get(ts.URL) _, rawResp, err := resp.ReadBodyAsString() require.Equal(t, int32(1), requestTimes.Load()) require.Less(t, time.Since(tBegin), 300*time.Millisecond) require.Error(t, err) require.Contains(t, err.Error(), "Client.Timeout") require.Nil(t, rawResp) // Read again _, rawResp, err = resp.ReadBodyAsString() require.Equal(t, int32(1), requestTimes.Load()) require.Error(t, err) require.Contains(t, err.Error(), "Client.Timeout") require.Nil(t, rawResp) rawResp, err = resp.Finish() require.Equal(t, int32(1), requestTimes.Load()) require.Error(t, err) require.Contains(t, err.Error(), "Client.Timeout") require.Nil(t, rawResp) // Even if the request is finished then, we should still get timeout error. time.Sleep(1 * time.Second) _, rawResp, err = resp.ReadBodyAsString() require.Equal(t, int32(1), requestTimes.Load()) require.Error(t, err) require.Contains(t, err.Error(), "Client.Timeout") require.Nil(t, rawResp) // Call Finish() directly should fail tBegin = time.Now() resp = client.LR().SetTimeout(100 * time.Millisecond).Get(ts.URL) rawResp, err = resp.Finish() require.Equal(t, int32(2), requestTimes.Load()) require.Less(t, time.Since(tBegin), 300*time.Millisecond) require.Error(t, err) require.Contains(t, err.Error(), "Client.Timeout") require.Nil(t, rawResp) // Read using long enough timeout should succeed resp = client.LR().SetTimeout(1200 * time.Millisecond).Get(ts.URL) dataStr, rawResp, err := resp.ReadBodyAsString() require.Equal(t, int32(3), requestTimes.Load()) require.NoError(t, err) require.Equal(t, http.StatusOK, rawResp.StatusCode) require.Equal(t, "OK", dataStr) } func TestTimeoutBody(t *testing.T) { requestTimes := atomic.Int32{} ctx, cancel := context.WithCancel(context.Background()) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requestTimes.Inc() _, _ = w.Write([]byte("Partial...")) w.(http.Flusher).Flush() select { case <-ctx.Done(): return case <-time.After(1 * time.Second): _, _ = fmt.Fprintln(w, "Done") } })) defer ts.Close() defer cancel() client := New(Config{}) // Finish() should succeed, since a header is successfully returned tBegin := time.Now() resp := client.LR().SetTimeout(100 * time.Millisecond).Get(ts.URL) rawResp, err := resp.Finish() require.Equal(t, int32(1), requestTimes.Load()) require.Less(t, time.Since(tBegin), 50*time.Millisecond) require.NoError(t, err) require.Equal(t, http.StatusOK, rawResp.StatusCode) // ReadBodyAsString() should fail tBegin = time.Now() resp = client.LR().SetTimeout(100 * time.Millisecond).Get(ts.URL) _, rawResp, err = resp.ReadBodyAsString() require.Equal(t, int32(2), requestTimes.Load()) require.Less(t, time.Since(tBegin), 300*time.Millisecond) require.Error(t, err) require.Contains(t, err.Error(), "Client.Timeout") require.Nil(t, rawResp) // Read again _, rawResp, err = resp.ReadBodyAsString() require.Equal(t, int32(2), requestTimes.Load()) require.Error(t, err) require.Contains(t, err.Error(), "Client.Timeout") require.Nil(t, rawResp) // Wait enough time and read again time.Sleep(1 * time.Second) _, rawResp, err = resp.ReadBodyAsString() require.Equal(t, int32(2), requestTimes.Load()) require.Error(t, err) require.Contains(t, err.Error(), "Client.Timeout") require.Nil(t, rawResp) // Finish() should succeed tBegin = time.Now() rawResp, err = resp.Finish() require.Equal(t, int32(2), requestTimes.Load()) require.Less(t, time.Since(tBegin), 50*time.Millisecond) require.NoError(t, err) require.Equal(t, http.StatusOK, rawResp.StatusCode) // PipeBody() should fail buf := bytes.Buffer{} tBegin = time.Now() resp = client.LR().SetTimeout(100 * time.Millisecond).Get(ts.URL) wBytes, rawResp, err := resp.PipeBody(&buf) require.Equal(t, int32(3), requestTimes.Load()) require.Less(t, time.Since(tBegin), 300*time.Millisecond) require.Error(t, err) require.Contains(t, err.Error(), "Client.Timeout") require.Nil(t, rawResp) require.Equal(t, int64(10), wBytes) // The first chunk is written require.Equal(t, "Partial...", buf.String()) // PipeBody again should fail wBytes, rawResp, err = resp.PipeBody(&buf) require.Equal(t, int32(3), requestTimes.Load()) require.Less(t, time.Since(tBegin), 300*time.Millisecond) require.Error(t, err) require.Contains(t, err.Error(), "Client.Timeout") require.Nil(t, rawResp) require.Equal(t, int64(0), wBytes) // No more chunk is written require.Equal(t, "Partial...", buf.String()) // Read using long enough timeout should succeed resp = client.LR().SetTimeout(1200 * time.Millisecond).Get(ts.URL) dataStr, rawResp, err := resp.ReadBodyAsString() require.Equal(t, int32(4), requestTimes.Load()) require.NoError(t, err) require.Equal(t, http.StatusOK, rawResp.StatusCode) require.Equal(t, "Partial...Done", dataStr) } // FIXME: Seems that there is no way to test the panic happens inside runtime finalizers. // func TestUsageCheck(t *testing.T) { // if !israce.Enabled { // t.Skipf("LazyResponse usage check will be tested only when race detector is enabled") // return // } // client := New(Config{}) // client.LR().Get("foo://example.com") // require.Panics(t, func() { runtime.GC() }) // } // TODO: TestCtxRequest // TODO: TestCtxResponse // This test shows that ctx doesn't really restrict the response's lifetime. // TODO: Test log output ================================================ FILE: util/client/pdclient/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdclient_test import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/client/pdclient/etcd_client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdclient import ( "context" "crypto/tls" "time" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/backoff" ) type EtcdClientConfig struct { Endpoints []string Context context.Context TLS *tls.Config } // NewEtcdClient creates a new etcd client. The client must be closed by calling `client.Close()`. // Returns error when config is invalid. func NewEtcdClient(config EtcdClientConfig) (*clientv3.Client, error) { zapCfg := zap.NewProductionConfig() zapCfg.Encoding = log.ZapEncodingName cli, err := clientv3.New(clientv3.Config{ Context: config.Context, Endpoints: config.Endpoints, AutoSyncInterval: 30 * time.Second, DialTimeout: 5 * time.Second, DialKeepAliveTime: 10 * time.Second, DialKeepAliveTimeout: 3 * time.Second, PermitWithoutStream: false, DialOptions: []grpc.DialOption{ grpc.WithConnectParams(grpc.ConnectParams{ Backoff: backoff.Config{ BaseDelay: 100 * time.Millisecond, // Default was 1 second Multiplier: 1.6, // Default Jitter: 0.2, // Default MaxDelay: 3 * time.Second, // Default was 120 seconds }, MinConnectTimeout: 5 * time.Second, // Default was 20 seconds }), }, TLS: config.TLS, LogConfig: &zapCfg, }) return cli, err } ================================================ FILE: util/client/pdclient/fixture/pd_server.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package fixture import ( "github.com/jarcoal/httpmock" "github.com/pingcap/tidb-dashboard/util/client/httpclient" "github.com/pingcap/tidb-dashboard/util/client/pdclient" "github.com/pingcap/tidb-dashboard/util/testutil/httpmockutil" ) const BaseURL = "http://172.16.6.171:2379" func NewPDServerFixture() (mockTransport *httpmock.MockTransport) { mockTransport = httpmock.NewMockTransport() mockTransport.RegisterResponder("GET", "http://172.16.6.171:2379/pd/api/v1/status", httpmockutil.StringResponder(` { "build_ts": "2021-07-17 05:37:05", "version": "v4.0.14", "git_hash": "0c1246dd219fd16b4b2ff5108941e5d3e958922d", "start_timestamp": 1635762685 } `)) mockTransport.RegisterResponder("GET", "http://172.16.6.171:2379/pd/api/v1/health", httpmockutil.StringResponder(` [ { "name": "pd-172.16.6.170-2379", "member_id": 2939568762143497195, "client_urls": [ "http://172.16.6.170:2379" ], "health": true }, { "name": "pd-172.16.6.169-2379", "member_id": 8776556846936845803, "client_urls": [ "http://172.16.6.169:2379" ], "health": true }, { "name": "pd-172.16.6.171-2379", "member_id": 13248060353287547571, "client_urls": [ "http://172.16.6.171:2379" ], "health": true } ] `)) mockTransport.RegisterResponder("GET", "http://172.16.6.171:2379/pd/api/v1/members", httpmockutil.StringResponder(` { "header": { "cluster_id": 6973530669239952773 }, "members": [ { "name": "pd-172.16.6.170-2379", "member_id": 2939568762143497195, "peer_urls": [ "http://172.16.6.170:2380" ], "client_urls": [ "http://172.16.6.170:2379" ], "deploy_path": "/home/tidb/tidb-deploy/pd-2379/bin", "binary_version": "v4.0.14", "git_hash": "0c1246dd219fd16b4b2ff5108941e5d3e958922d" }, { "name": "pd-172.16.6.169-2379", "member_id": 8776556846936845803, "peer_urls": [ "http://172.16.6.169:2380" ], "client_urls": [ "http://172.16.6.169:2379" ], "deploy_path": "/home/tidb/tidb-deploy/pd-2379/bin", "binary_version": "v4.0.14", "git_hash": "0c1246dd219fd16b4b2ff5108941e5d3e958922d" }, { "name": "pd-172.16.6.171-2379", "member_id": 13248060353287547571, "peer_urls": [ "http://172.16.6.171:2380" ], "client_urls": [ "http://172.16.6.171:2379" ], "deploy_path": "/home/tidb/tidb-deploy/pd-2379/bin", "binary_version": "v4.0.14", "git_hash": "0c1246dd219fd16b4b2ff5108941e5d3e958922d" } ], "leader": { "name": "pd-172.16.6.171-2379", "member_id": 13248060353287547571, "peer_urls": [ "http://172.16.6.171:2380" ], "client_urls": [ "http://172.16.6.171:2379" ] }, "etcd_leader": { "name": "pd-172.16.6.171-2379", "member_id": 13248060353287547571, "peer_urls": [ "http://172.16.6.171:2380" ], "client_urls": [ "http://172.16.6.171:2379" ], "deploy_path": "/home/tidb/tidb-deploy/pd-2379/bin", "binary_version": "v4.0.14", "git_hash": "0c1246dd219fd16b4b2ff5108941e5d3e958922d" } } `)) mockTransport.RegisterResponder("GET", "http://172.16.6.171:2379/pd/api/v1/stores", httpmockutil.StringResponder(` { "count": 3, "stores": [ { "store": { "id": 4, "address": "172.16.6.168:20160", "version": "4.0.14", "status_address": "172.16.6.168:20180", "git_hash": "d7dc4fff51ca71c76a928a0780a069efaaeaae70", "start_timestamp": 1636421304, "deploy_path": "/home/tidb/tidb-deploy/tikv-20160/bin", "last_heartbeat": 1639400885792220820, "state_name": "Up" }, "status": { "capacity": "446.8GiB", "available": "432.7GiB", "used_size": "4.071GiB", "leader_count": 51, "leader_weight": 1, "leader_score": 51, "leader_size": 2839, "region_count": 141, "region_weight": 1, "region_score": 6111, "region_size": 6111, "start_ts": "2021-11-09T09:28:24+08:00", "last_heartbeat_ts": "2021-12-13T21:08:05.79222082+08:00", "uptime": "827h39m41.79222082s" } }, { "store": { "id": 5, "address": "172.16.5.218:20160", "version": "4.0.14", "status_address": "172.16.5.218:20180", "git_hash": "d7dc4fff51ca71c76a928a0780a069efaaeaae70", "start_timestamp": 1636421304, "deploy_path": "/home/tidb/tidb-deploy/tikv-20160/bin", "last_heartbeat": 1639400889610431214, "state_name": "Up" }, "status": { "capacity": "446.8GiB", "available": "432GiB", "used_size": "4.07GiB", "leader_count": 42, "leader_weight": 1, "leader_score": 42, "leader_size": 1016, "region_count": 141, "region_weight": 1, "region_score": 6111, "region_size": 6111, "start_ts": "2021-11-09T09:28:24+08:00", "last_heartbeat_ts": "2021-12-13T21:08:09.610431214+08:00", "uptime": "827h39m45.610431214s" } }, { "store": { "id": 1, "address": "172.16.5.141:20160", "version": "4.0.14", "status_address": "172.16.5.141:20180", "git_hash": "d7dc4fff51ca71c76a928a0780a069efaaeaae70", "start_timestamp": 1636421301, "deploy_path": "/home/tidb/tidb-deploy/tikv-20160/bin", "last_heartbeat": 1639400886447728006, "state_name": "Up" }, "status": { "capacity": "446.8GiB", "available": "409.2GiB", "used_size": "4.077GiB", "leader_count": 48, "leader_weight": 1, "leader_score": 48, "leader_size": 2256, "region_count": 141, "region_weight": 1, "region_score": 6111, "region_size": 6111, "start_ts": "2021-11-09T09:28:21+08:00", "last_heartbeat_ts": "2021-12-13T21:08:06.447728006+08:00", "uptime": "827h39m45.447728006s" } } ] } `)) mockTransport.RegisterResponder("GET", "http://172.16.6.171:2379/pd/api/v1/config", httpmockutil.StringResponder(` { "client-urls": "http://0.0.0.0:2379", "peer-urls": "http://0.0.0.0:2380", "advertise-client-urls": "http://172.16.6.171:2379", "advertise-peer-urls": "http://172.16.6.171:2380", "name": "pd-172.16.6.171-2379", "data-dir": "/home/tidb/tidb-data/pd-2379", "force-new-cluster": false, "enable-grpc-gateway": true, "initial-cluster": "pd-172.16.6.169-2379=http://172.16.6.169:2380,pd-172.16.6.170-2379=http://172.16.6.170:2380,pd-172.16.6.171-2379=http://172.16.6.171:2380", "initial-cluster-state": "new", "initial-cluster-token": "pd-cluster", "join": "", "lease": 3, "log": { "level": "", "format": "text", "disable-timestamp": false, "file": { "filename": "/home/tidb/tidb-deploy/pd-2379/log/pd.log", "max-size": 300, "max-days": 0, "max-backups": 0 }, "development": false, "disable-caller": false, "disable-stacktrace": false, "disable-error-verbose": true, "sampling": null }, "tso-save-interval": "3s", "metric": { "job": "pd-172.16.6.171-2379", "address": "", "interval": "15s" }, "schedule": { "max-snapshot-count": 3, "max-pending-peer-count": 16, "max-merge-region-size": 20, "max-merge-region-keys": 200000, "split-merge-interval": "1h0m0s", "enable-one-way-merge": "false", "enable-cross-table-merge": "false", "patrol-region-interval": "100ms", "max-store-down-time": "30m0s", "leader-schedule-limit": 4, "leader-schedule-policy": "count", "region-schedule-limit": 2048, "replica-schedule-limit": 64, "merge-schedule-limit": 8, "hot-region-schedule-limit": 4, "hot-region-cache-hits-threshold": 3, "store-limit": { "1": { "add-peer": 15, "remove-peer": 15 }, "4": { "add-peer": 15, "remove-peer": 15 }, "5": { "add-peer": 15, "remove-peer": 15 } }, "tolerant-size-ratio": 0, "low-space-ratio": 0.8, "high-space-ratio": 0.7, "scheduler-max-waiting-operator": 5, "enable-remove-down-replica": "true", "enable-replace-offline-replica": "true", "enable-make-up-replica": "true", "enable-remove-extra-replica": "true", "enable-location-replacement": "true", "enable-debug-metrics": "false", "schedulers-v2": [ { "type": "balance-region", "args": null, "disable": false, "args-payload": "" }, { "type": "balance-leader", "args": null, "disable": false, "args-payload": "" }, { "type": "hot-region", "args": null, "disable": false, "args-payload": "" }, { "type": "label", "args": null, "disable": false, "args-payload": "" } ], "schedulers-payload": { "balance-hot-region-scheduler": null, "balance-leader-scheduler": { "name": "balance-leader-scheduler", "ranges": [ { "end-key": "", "start-key": "" } ] }, "balance-region-scheduler": { "name": "balance-region-scheduler", "ranges": [ { "end-key": "", "start-key": "" } ] }, "label-scheduler": { "name": "label-scheduler", "ranges": [ { "end-key": "", "start-key": "" } ] } }, "store-limit-mode": "manual" }, "replication": { "max-replicas": 3, "location-labels": "", "strictly-match-label": "false", "enable-placement-rules": "false" }, "pd-server": { "use-region-storage": "true", "max-gap-reset-ts": "24h0m0s", "key-type": "table", "runtime-services": "", "metric-storage": "", "dashboard-address": "http://172.16.6.169:2379", "trace-region-flow": "true" }, "cluster-version": "4.0.14", "quota-backend-bytes": "8GiB", "auto-compaction-mode": "periodic", "auto-compaction-retention-v2": "1h", "TickInterval": "500ms", "ElectionInterval": "3s", "PreVote": true, "security": { "cacert-path": "", "cert-path": "", "key-path": "", "cert-allowed-cn": null }, "label-property": {}, "WarningMsgs": null, "DisableStrictReconfigCheck": false, "HeartbeatStreamBindInterval": "1m0s", "LeaderPriorityCheckInterval": "1m0s", "dashboard": { "tidb-cacert-path": "", "tidb-cert-path": "", "tidb-key-path": "", "public-path-prefix": "", "internal-proxy": false, "enable-telemetry": true, "enable-experimental": false }, "replication-mode": { "replication-mode": "majority", "dr-auto-sync": { "label-key": "", "primary": "", "dr": "", "primary-replicas": 0, "dr-replicas": 0, "wait-store-timeout": "1m0s", "wait-sync-timeout": "1m0s" } }, "enable-redact-log": false } `)) mockTransport.RegisterResponder("GET", "http://172.16.6.171:2379/pd/api/v1/config/replicate", httpmockutil.StringResponder(` { "max-replicas": 3, "location-labels": "", "strictly-match-label": "false", "enable-placement-rules": "false" } `)) return } // NewAPIClientFixture returns a PD client whose default Base URL is pointing to a mock PD server. func NewAPIClientFixture() *pdclient.APIClient { mockTransport := NewPDServerFixture() apiClient := pdclient.NewAPIClient(httpclient.Config{}) apiClient.SetDefaultBaseURL(BaseURL) apiClient.SetDefaultTransport(mockTransport) return apiClient } ================================================ FILE: util/client/pdclient/pd_api.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdclient import ( "context" ) // TODO: Switch to use swagger. const APIPrefix = "/pd/api/v1" type GetStatusResponse struct { StartTimestamp int64 `json:"start_timestamp"` } // GetStatus returns the content from /status PD API. // You must specify the base URL by calling SetDefaultBaseURL() before using this function. func (api *APIClient) GetStatus(ctx context.Context) (resp *GetStatusResponse, err error) { _, err = api.LR().SetContext(ctx).Get(APIPrefix + "/status").ReadBodyAsJSON(&resp) return } type GetHealthResponseMember struct { MemberID uint64 `json:"member_id"` Health bool `json:"health"` } type GetHealthResponse []GetHealthResponseMember // GetHealth returns the content from /health PD API. // You must specify the base URL by calling SetDefaultBaseURL() before using this function. func (api *APIClient) GetHealth(ctx context.Context) (resp *GetHealthResponse, err error) { _, err = api.LR().SetContext(ctx).Get(APIPrefix + "/health").ReadBodyAsJSON(&resp) return } type GetMembersResponseMember struct { GitHash string `json:"git_hash"` ClientUrls []string `json:"client_urls"` DeployPath string `json:"deploy_path"` BinaryVersion string `json:"binary_version"` MemberID uint64 `json:"member_id"` } type GetMembersResponse struct { Members []GetMembersResponseMember `json:"members"` } // GetMembers returns the content from /members PD API. // You must specify the base URL by calling SetDefaultBaseURL() before using this function. func (api *APIClient) GetMembers(ctx context.Context) (resp *GetMembersResponse, err error) { _, err = api.LR().SetContext(ctx).Get(APIPrefix + "/members").ReadBodyAsJSON(&resp) return } type GetConfigReplicateResponse struct { LocationLabels string `json:"location-labels"` } // GetConfigReplicate returns the content from /config/replicate PD API. // You must specify the base URL by calling SetDefaultBaseURL() before using this function. func (api *APIClient) GetConfigReplicate(ctx context.Context) (resp *GetConfigReplicateResponse, err error) { _, err = api.LR().SetContext(ctx).Get(APIPrefix + "/config/replicate").ReadBodyAsJSON(&resp) return } type GetStoresResponseStoreLabel struct { Key string `json:"key"` Value string `json:"value"` } type GetStoresResponseStore struct { Address string `json:"address"` ID int `json:"id"` Labels []GetStoresResponseStoreLabel `json:"labels"` StateName string `json:"state_name"` Version string `json:"version"` StatusAddress string `json:"status_address"` GitHash string `json:"git_hash"` DeployPath string `json:"deploy_path"` StartTimestamp int64 `json:"start_timestamp"` } type GetStoresResponseStoresElem struct { Store GetStoresResponseStore `json:"store"` } type GetStoresResponse struct { Stores []GetStoresResponseStoresElem `json:"stores"` } // GetStores returns the content from /stores PD API. // You must specify the base URL by calling SetDefaultBaseURL() before using this function. func (api *APIClient) GetStores(ctx context.Context) (resp *GetStoresResponse, err error) { _, err = api.LR().SetContext(ctx).Get(APIPrefix + "/stores").ReadBodyAsJSON(&resp) return } ================================================ FILE: util/client/pdclient/pd_api_client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package pdclient provides a flexible PD API access to any PD instance. package pdclient import ( "github.com/pingcap/tidb-dashboard/util/client/httpclient" "github.com/pingcap/tidb-dashboard/util/distro" ) type APIClient struct { *httpclient.Client } func NewAPIClient(config httpclient.Config) *APIClient { config.KindTag = distro.R().PD return &APIClient{httpclient.New(config)} } func (c *APIClient) Clone() *APIClient { return &APIClient{c.Client.Clone()} } ================================================ FILE: util/client/pdclient/pd_api_highlevel.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // This file contains high level encapsulations over base PD APIs. package pdclient import ( "context" "sort" "strings" ) // HLGetStores returns all stores in PD in order. // You must specify the base URL by calling SetDefaultBaseURL() before using this function. func (api *APIClient) HLGetStores(ctx context.Context) ([]GetStoresResponseStore, error) { resp, err := api.GetStores(ctx) if err != nil { return nil, err } stores := make([]GetStoresResponseStore, 0, len(resp.Stores)) for _, s := range resp.Stores { stores = append(stores, s.Store) } sort.Slice(stores, func(i, j int) bool { return stores[i].Address < stores[j].Address }) return stores, nil } // HLGetLocationLabels returns the location label config in PD. // You must specify the base URL by calling SetDefaultBaseURL() before using this function. func (api *APIClient) HLGetLocationLabels(ctx context.Context) ([]string, error) { resp, err := api.GetConfigReplicate(ctx) if err != nil { return nil, err } if len(resp.LocationLabels) == 0 { return []string{}, nil } labels := strings.Split(resp.LocationLabels, ",") return labels, nil } type StoreLabels struct { Address string `json:"address"` Labels map[string]string `json:"labels"` } type StoreLocations struct { LocationLabels []string `json:"location_labels"` Stores []StoreLabels `json:"stores"` } // HLGetStoreLocations returns the stores and their locations. // You must specify the base URL by calling SetDefaultBaseURL() before using this function. func (api *APIClient) HLGetStoreLocations(ctx context.Context) (*StoreLocations, error) { locationLabels, err := api.HLGetLocationLabels(ctx) if err != nil { return nil, err } stores, err := api.HLGetStores(ctx) if err != nil { return nil, err } nodes := make([]StoreLabels, 0, len(stores)) for _, s := range stores { node := StoreLabels{ Address: s.Address, Labels: map[string]string{}, } for _, l := range s.Labels { node.Labels[l.Key] = l.Value } nodes = append(nodes, node) } return &StoreLocations{ LocationLabels: locationLabels, Stores: nodes, }, nil } ================================================ FILE: util/client/pdclient/pd_api_highlevel_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdclient_test import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/client/pdclient" "github.com/pingcap/tidb-dashboard/util/client/pdclient/fixture" ) func TestAPIClient_HLGetLocationLabels(t *testing.T) { apiClient := fixture.NewAPIClientFixture() resp, err := apiClient.HLGetLocationLabels(context.Background()) require.NoError(t, err) require.Equal(t, []string{}, resp) } func TestAPIClient_HLGetStoreLocations(t *testing.T) { apiClient := fixture.NewAPIClientFixture() resp, err := apiClient.HLGetStoreLocations(context.Background()) require.NoError(t, err) require.Equal(t, &pdclient.StoreLocations{ LocationLabels: []string{}, Stores: []pdclient.StoreLabels{ {Address: "172.16.5.141:20160", Labels: map[string]string{}}, {Address: "172.16.5.218:20160", Labels: map[string]string{}}, {Address: "172.16.6.168:20160", Labels: map[string]string{}}, }, }, resp) } func TestAPIClient_HLGetStores(t *testing.T) { apiClient := fixture.NewAPIClientFixture() resp, err := apiClient.HLGetStores(context.Background()) require.NoError(t, err) require.Equal(t, []pdclient.GetStoresResponseStore{ { Address: "172.16.5.141:20160", ID: 1, Labels: nil, StateName: "Up", Version: "4.0.14", StatusAddress: "172.16.5.141:20180", GitHash: "d7dc4fff51ca71c76a928a0780a069efaaeaae70", DeployPath: "/home/tidb/tidb-deploy/tikv-20160/bin", StartTimestamp: 1636421301, }, { Address: "172.16.5.218:20160", ID: 5, Labels: nil, StateName: "Up", Version: "4.0.14", StatusAddress: "172.16.5.218:20180", GitHash: "d7dc4fff51ca71c76a928a0780a069efaaeaae70", DeployPath: "/home/tidb/tidb-deploy/tikv-20160/bin", StartTimestamp: 1636421304, }, { Address: "172.16.6.168:20160", ID: 4, Labels: nil, StateName: "Up", Version: "4.0.14", StatusAddress: "172.16.6.168:20180", GitHash: "d7dc4fff51ca71c76a928a0780a069efaaeaae70", DeployPath: "/home/tidb/tidb-deploy/tikv-20160/bin", StartTimestamp: 1636421304, }, }, resp) } ================================================ FILE: util/client/pdclient/pd_api_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdclient_test import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/client/pdclient" "github.com/pingcap/tidb-dashboard/util/client/pdclient/fixture" ) func TestAPIClient_GetConfigReplicate(t *testing.T) { apiClient := fixture.NewAPIClientFixture() resp, err := apiClient.GetConfigReplicate(context.Background()) require.NoError(t, err) require.Equal(t, &pdclient.GetConfigReplicateResponse{ LocationLabels: "", }, resp) } func TestAPIClient_GetHealth(t *testing.T) { apiClient := fixture.NewAPIClientFixture() resp, err := apiClient.GetHealth(context.Background()) require.NoError(t, err) require.Equal(t, &pdclient.GetHealthResponse{ pdclient.GetHealthResponseMember{MemberID: 0x28cb7236f465dbeb, Health: true}, pdclient.GetHealthResponseMember{MemberID: 0x79cc97f3bcb16deb, Health: true}, pdclient.GetHealthResponseMember{MemberID: 0xb7da90b338a3eab3, Health: true}, }, resp) } func TestAPIClient_GetMembers(t *testing.T) { apiClient := fixture.NewAPIClientFixture() resp, err := apiClient.GetMembers(context.Background()) require.NoError(t, err) require.Equal(t, &pdclient.GetMembersResponse{ Members: []pdclient.GetMembersResponseMember{ { GitHash: "0c1246dd219fd16b4b2ff5108941e5d3e958922d", ClientUrls: []string{"http://172.16.6.170:2379"}, DeployPath: "/home/tidb/tidb-deploy/pd-2379/bin", BinaryVersion: "v4.0.14", MemberID: 0x28cb7236f465dbeb, }, { GitHash: "0c1246dd219fd16b4b2ff5108941e5d3e958922d", ClientUrls: []string{"http://172.16.6.169:2379"}, DeployPath: "/home/tidb/tidb-deploy/pd-2379/bin", BinaryVersion: "v4.0.14", MemberID: 0x79cc97f3bcb16deb, }, { GitHash: "0c1246dd219fd16b4b2ff5108941e5d3e958922d", ClientUrls: []string{"http://172.16.6.171:2379"}, DeployPath: "/home/tidb/tidb-deploy/pd-2379/bin", BinaryVersion: "v4.0.14", MemberID: 0xb7da90b338a3eab3, }, }, }, resp) } func TestAPIClient_GetStatus(t *testing.T) { apiClient := fixture.NewAPIClientFixture() resp, err := apiClient.GetStatus(context.Background()) require.NoError(t, err) require.Equal(t, &pdclient.GetStatusResponse{ StartTimestamp: 1635762685, }, resp) } func TestAPIClient_GetStores(t *testing.T) { apiClient := fixture.NewAPIClientFixture() resp, err := apiClient.GetStores(context.Background()) require.NoError(t, err) require.Equal(t, &pdclient.GetStoresResponse{ Stores: []pdclient.GetStoresResponseStoresElem{ { Store: pdclient.GetStoresResponseStore{ Address: "172.16.6.168:20160", ID: 4, Labels: nil, StateName: "Up", Version: "4.0.14", StatusAddress: "172.16.6.168:20180", GitHash: "d7dc4fff51ca71c76a928a0780a069efaaeaae70", DeployPath: "/home/tidb/tidb-deploy/tikv-20160/bin", StartTimestamp: 1636421304, }, }, { Store: pdclient.GetStoresResponseStore{ Address: "172.16.5.218:20160", ID: 5, Labels: nil, StateName: "Up", Version: "4.0.14", StatusAddress: "172.16.5.218:20180", GitHash: "d7dc4fff51ca71c76a928a0780a069efaaeaae70", DeployPath: "/home/tidb/tidb-deploy/tikv-20160/bin", StartTimestamp: 1636421304, }, }, { Store: pdclient.GetStoresResponseStore{ Address: "172.16.5.141:20160", ID: 1, Labels: nil, StateName: "Up", Version: "4.0.14", StatusAddress: "172.16.5.141:20180", GitHash: "d7dc4fff51ca71c76a928a0780a069efaaeaae70", DeployPath: "/home/tidb/tidb-deploy/tikv-20160/bin", StartTimestamp: 1636421301, }, }, }, }, resp) } ================================================ FILE: util/client/schedulingclient/status_client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package schedulingclient provides a flexible Scheduling API access to any Scheduling instance. package schedulingclient import ( "github.com/pingcap/tidb-dashboard/util/client/httpclient" "github.com/pingcap/tidb-dashboard/util/distro" ) type StatusClient struct { *httpclient.Client } func NewStatusClient(config httpclient.Config) *StatusClient { config.KindTag = distro.R().Scheduling return &StatusClient{httpclient.New(config)} } func (c *StatusClient) Clone() *StatusClient { return &StatusClient{c.Client.Clone()} } ================================================ FILE: util/client/ticdcclient/status_client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package ticdcclient provides a flexible TiCDC API access to any TiCDC instance. package ticdcclient import ( "github.com/pingcap/tidb-dashboard/util/client/httpclient" "github.com/pingcap/tidb-dashboard/util/distro" ) type StatusClient struct { *httpclient.Client } func NewStatusClient(config httpclient.Config) *StatusClient { config.KindTag = distro.R().TiCDC return &StatusClient{httpclient.New(config)} } func (c *StatusClient) Clone() *StatusClient { return &StatusClient{c.Client.Clone()} } ================================================ FILE: util/client/tidbclient/sql_client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tidbclient import ( "context" "errors" "net" "strconv" "time" "github.com/VividCortex/mysqlerr" "github.com/go-sql-driver/mysql" "github.com/joomcode/errorx" "github.com/pingcap/log" "go.uber.org/zap" mysqlDriver "gorm.io/driver/mysql" "gorm.io/gorm" "github.com/pingcap/tidb-dashboard/util/distro" ) var ( ErrNS = errorx.NewNamespace("tidb_client") // ErrAuthFailed means the authentication is failed when connecting to the TiDB Server. ErrAuthFailed = ErrNS.NewType("tidb_auth_failed") // ErrConnFailed means there is a connection (like network) problem when connecting to the TiDB Server. ErrConnFailed = ErrNS.NewType("tidb_conn_failed") ) type SQLClientConfig struct { BaseContext context.Context Host string Port int TLSKey string } type SQLClient struct { config SQLClientConfig } func NewSQLClient(config SQLClientConfig) *SQLClient { client := &SQLClient{ config: config, } return client } // OpenConn opens a new connection. // NOTICE: The opened connection must be manually closed. func (c *SQLClient) OpenConn(user string, pass string) (*gorm.DB, error) { dsnConfig := mysql.NewConfig() dsnConfig.Net = "tcp" dsnConfig.Addr = net.JoinHostPort(c.config.Host, strconv.Itoa(c.config.Port)) dsnConfig.User = user dsnConfig.Passwd = pass dsnConfig.Timeout = time.Second * 60 dsnConfig.ParseTime = true dsnConfig.Loc = time.Local dsnConfig.MultiStatements = true // TODO: Disable this, as it increase security risk. dsnConfig.TLSConfig = c.config.TLSKey dsn := dsnConfig.FormatDSN() db, err := gorm.Open(mysqlDriver.Open(dsn)) if err != nil { log.Warn("Failed to open SQL connection", zap.String("targetComponent", distro.R().TiDB), zap.Error(err)) var mysqlErr *mysql.MySQLError if errors.As(err, &mysqlErr) { if mysqlErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR { return nil, ErrAuthFailed.New("Bad SQL username or password") } } return nil, ErrConnFailed.Wrap(err, "Failed to connect to %s", distro.R().TiDB) } // Ensure that when the App stops resources are released if c.config.BaseContext != nil { db = db.WithContext(c.config.BaseContext) } return db, nil } ================================================ FILE: util/client/tidbclient/status_client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package tidbclient provides a flexible TiDB API access to any TiDB instance. package tidbclient import ( "github.com/pingcap/tidb-dashboard/util/client/httpclient" "github.com/pingcap/tidb-dashboard/util/distro" ) type StatusClient struct { *httpclient.Client } func NewStatusClient(config httpclient.Config) *StatusClient { config.KindTag = distro.R().TiDB return &StatusClient{httpclient.New(config)} } func (c *StatusClient) Clone() *StatusClient { return &StatusClient{c.Client.Clone()} } ================================================ FILE: util/client/tidbclient/tidbproto/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tidbproto import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/client/tidbclient/tidbproto/codec.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tidbproto import ( "bytes" "encoding/binary" "github.com/pingcap/errors" ) var ( tablePrefix = []byte{'t'} metaPrefix = []byte{'m'} recordPrefix = []byte{'r'} ) const ( signMask uint64 = 0x8000000000000000 encGroupSize = 8 encMarker = byte(0xFF) encPad = byte(0x0) ) // Key represents high-level TiDB Key type. type Key []byte // KeyInfoBuffer can obtain the meta information of the TiDB Key. // It can be reused, thereby reducing memory applications. type KeyInfoBuffer []byte // DecodeKey obtains the KeyInfoBuffer from a TiDB Key. func (buf *KeyInfoBuffer) DecodeKey(key Key) (KeyInfoBuffer, error) { _, result, err := decodeBytes(key, *buf) if err != nil { *buf = (*buf)[:0] return nil, err } *buf = result return result, nil } // MetaOrTable checks if the key is a meta key or table key. // If the key is a meta key, it returns true and 0. // If the key is a table key, it returns false and table ID. // Otherwise, it returns false and 0. func (buf KeyInfoBuffer) MetaOrTable() (isMeta bool, tableID int64) { if bytes.HasPrefix(buf, metaPrefix) { return true, 0 } if bytes.HasPrefix(buf, tablePrefix) { _, tableID, _ := decodeInt(buf[len(tablePrefix):]) return false, tableID } return false, 0 } // RowInfo returns the row ID of the key, if the key is not table key, returns 0. func (buf KeyInfoBuffer) RowInfo() (isCommonHandle bool, rowID int64) { if !bytes.HasPrefix(buf, tablePrefix) || len(buf) < 19 || (buf[9] != '_' || buf[10] != 'r') { return } isCommonHandle = len(buf) != 19 if !isCommonHandle { _, rowID, _ = decodeInt(buf[11:19]) } return } // IndexInfo returns the row ID of the key, if the key is not table key, returns 0. func (buf KeyInfoBuffer) IndexInfo() (indexID int64) { if !bytes.HasPrefix(buf, tablePrefix) || len(buf) < 19 || (buf[9] != '_' || buf[10] != 'i') { return } _, indexID, _ = decodeInt(buf[11:19]) return } // GenerateTableKey generates a table split key. func (buf *KeyInfoBuffer) GenerateKey(tableID, rowID int64) Key { if tableID == 0 { return nil } data := *buf if data == nil { length := len(tablePrefix) + 8 if rowID != 0 { length = len(tablePrefix) + len(recordPrefix) + 8*2 } data = make([]byte, 0, length) } else { data = data[:0] } data = append(data, tablePrefix...) data = encodeInt(data, tableID) if rowID != 0 { data = append(data, recordPrefix...) data = encodeInt(data, rowID) } *buf = data return encodeBytes(data) } var pads = make([]byte, encGroupSize) // decodeBytes decodes bytes which is encoded by encodeBytes before, // returns the leftover bytes and decoded value if no error. func decodeBytes(b []byte, buf []byte) (rest []byte, result []byte, err error) { if buf == nil { buf = make([]byte, 0, len(b)) } buf = buf[:0] for { if len(b) < encGroupSize+1 { return nil, nil, errors.New("insufficient bytes to decode value") } groupBytes := b[:encGroupSize+1] group := groupBytes[:encGroupSize] marker := groupBytes[encGroupSize] padCount := encMarker - marker if padCount > encGroupSize { return nil, nil, errors.Errorf("invalid marker byte, group bytes %q", groupBytes) } realGroupSize := encGroupSize - padCount buf = append(buf, group[:realGroupSize]...) b = b[encGroupSize+1:] if padCount != 0 { // Check validity of padding bytes. for _, v := range group[realGroupSize:] { if v != encPad { return nil, nil, errors.Errorf("invalid padding byte, group bytes %q", groupBytes) } } break } } return b, buf, nil } // encodeBytes guarantees the encoded value is in ascending order for comparison, // encoding with the following rule: // // [group1][marker1]...[groupN][markerN] // group is 8 bytes slice which is padding with 0. // marker is `0xFF - padding 0 count` // // For example: // // [] -> [0, 0, 0, 0, 0, 0, 0, 0, 247] // [1, 2, 3] -> [1, 2, 3, 0, 0, 0, 0, 0, 250] // [1, 2, 3, 0] -> [1, 2, 3, 0, 0, 0, 0, 0, 251] // [1, 2, 3, 4, 5, 6, 7, 8] -> [1, 2, 3, 4, 5, 6, 7, 8, 255, 0, 0, 0, 0, 0, 0, 0, 0, 247] // // Refer: https://github.com/facebook/mysql-5.6/wiki/MyRocks-record-format#memcomparable-format func encodeBytes(data []byte) []byte { // Allocate more space to avoid unnecessary slice growing. // Assume that the byte slice size is about `(len(data) / encGroupSize + 1) * (encGroupSize + 1)` bytes, // that is `(len(data) / 8 + 1) * 9` in our implement. dLen := len(data) result := make([]byte, 0, (dLen/encGroupSize+1)*(encGroupSize+1)) for idx := 0; idx <= dLen; idx += encGroupSize { remain := dLen - idx padCount := 0 if remain >= encGroupSize { result = append(result, data[idx:idx+encGroupSize]...) } else { padCount = encGroupSize - remain result = append(result, data[idx:]...) result = append(result, pads[:padCount]...) } marker := encMarker - byte(padCount) result = append(result, marker) } return result } // decodeInt decodes value encoded by EncodeInt before. // It returns the leftover un-decoded slice, decoded value if no error. func decodeInt(b []byte) ([]byte, int64, error) { if len(b) < 8 { return nil, 0, errors.New("insufficient bytes to decode value") } u := binary.BigEndian.Uint64(b[:8]) v := decodeCmpUintToInt(u) b = b[8:] return b, v, nil } // encodeInt appends the encoded value to slice b and returns the appended slice. // encodeInt guarantees that the encoded value is in ascending order for comparison. func encodeInt(b []byte, v int64) []byte { var data [8]byte u := encodeIntToCmpUint(v) binary.BigEndian.PutUint64(data[:], u) return append(b, data[:]...) } func decodeCmpUintToInt(u uint64) int64 { return int64(u ^ signMask) } func encodeIntToCmpUint(v int64) uint64 { return uint64(v) ^ signMask } ================================================ FILE: util/client/tidbclient/tidbproto/codec_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tidbproto import ( "testing" "github.com/stretchr/testify/require" ) func TestDecodeBytes(t *testing.T) { key := "abcdefghijklmnopqrstuvwxyz" for i := 0; i < len(key); i++ { _, k, err := decodeBytes(encodeBytes([]byte(key[:i])), nil) require.NoError(t, err) require.Equal(t, string(k), key[:i]) } } func TestTiDBInfo(t *testing.T) { buf := new(KeyInfoBuffer) // no encode _, err := buf.DecodeKey([]byte("t\x80\x00\x00\x00\x00\x00\x00\xff")) require.Error(t, err) testcases := []struct { Key string IsMeta bool TableID int64 IsCommonHandle bool RowID int64 IndexID int64 }{ { "T\x00\x00\x00\x00\x00\x00\x00\xff", false, 0, false, 0, 0, }, { "t\x80\x00\x00\x00\x00\x00\xff", false, 0, false, 0, 0, }, { "t\x80\x00\x00\x00\x00\x00\x00\xff", false, 0xff, false, 0, 0, }, { "t\x80\x00\x00\x00\x00\x00\x00\xff_i\x01\x02", false, 0xff, false, 0, 0, }, { "t\x80\x00\x00\x00\x00\x00\x00\xff_i\x80\x00\x00\x00\x00\x00\x00\x02", false, 0xff, false, 0, 2, }, { "t\x80\x00\x00\x00\x00\x00\x00\xff_r\x80\x00\x00\x00\x00\x00\x00\x02", false, 0xff, false, 2, 0, }, { "t\x80\x00\x00\x00\x00\x00\x00\xff_r\x03\x80\x00\x00\x00\x00\x02\r\xaf\x03\x80\x00\x00\x00\x00\x00\x00\x03\x03\x80\x00\x00\x00\x00\x00\b%", false, 0xff, true, 0, 0, }, } for _, testcase := range testcases { key := encodeBytes([]byte(testcase.Key)) _, err := buf.DecodeKey(key) require.NoError(t, err) isMeta, tableID := buf.MetaOrTable() require.Equal(t, testcase.IsMeta, isMeta) require.Equal(t, testcase.TableID, tableID) isCommonHandle, rowID := buf.RowInfo() require.Equal(t, testcase.IsCommonHandle, isCommonHandle) require.Equal(t, testcase.RowID, rowID) indexID := buf.IndexInfo() require.Equal(t, testcase.IndexID, indexID) } } ================================================ FILE: util/client/tidbclient/tidbproto/model.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tidbproto // SchemaState is the state for schema elements. type SchemaState byte const ( // StateNone means this schema element is absent and can't be used. StateNone SchemaState = iota // StateDeleteOnly means we can only delete items for this schema element. StateDeleteOnly // StateWriteOnly means we can use any write operation on this schema element, // but outer can't read the changed data. StateWriteOnly // StateWriteReorganization means we are re-organizing whole data after write only state. StateWriteReorganization // StateDeleteReorganization means we are re-organizing whole data after delete only state. StateDeleteReorganization // StatePublic means this schema element is ok for all write and read operations. StatePublic ) // CIStr is case insensitive string. type CIStr struct { O string `json:"O"` // Original string. L string `json:"L"` // Lower case string. } // DBInfo provides meta data describing a DB. type DBInfo struct { ID int64 `json:"id"` Name CIStr `json:"db_name"` State SchemaState `json:"state"` } // IndexInfo provides meta data describing a DB index. // It corresponds to the statement `CREATE INDEX Name ON Table (Column);` // See https://dev.mysql.com/doc/refman/5.7/en/create-index.html type IndexInfo struct { ID int64 `json:"id"` Name CIStr `json:"idx_name"` } // PartitionDefinition defines a single partition. type PartitionDefinition struct { ID int64 `json:"id"` Name CIStr `json:"name"` } // PartitionInfo provides table partition info. type PartitionInfo struct { // User may already creates table with partition but table partition is not // yet supported back then. When Enable is true, write/read need use tid // rather than pid. Enable bool `json:"enable"` Definitions []*PartitionDefinition `json:"definitions"` } // TableInfo provides meta data describing a DB table. type TableInfo struct { ID int64 `json:"id"` Name CIStr `json:"name"` Indices []*IndexInfo `json:"index_info"` Partition *PartitionInfo `json:"partition"` } // GetPartitionInfo returns the partition information. func (t *TableInfo) GetPartitionInfo() *PartitionInfo { if t.Partition != nil && t.Partition.Enable { return t.Partition } return nil } ================================================ FILE: util/client/tidbclient/tidbproxy/proxy.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package tidbproxy provides a TiDB cluster proxy service. It forwards incoming SQL API and // Status API requests to one of the alive TiDB upstream. package tidbproxy import ( "github.com/pingcap/tidb-dashboard/util/nocopy" "github.com/pingcap/tidb-dashboard/util/proxy" ) type Config struct { Proxy proxy.Config } type Proxy struct { nocopy.NoCopy SQLPortProxy *proxy.Proxy StatusPortProxy *proxy.Proxy } func New(config Config) (*Proxy, error) { sqlProxy, err := proxy.New(config.Proxy) if err != nil { return nil, err } statusProxy, err := proxy.New(config.Proxy) if err != nil { sqlProxy.Close() return nil, err } return &Proxy{ SQLPortProxy: sqlProxy, StatusPortProxy: statusProxy, }, nil } func (f *Proxy) Close() { f.SQLPortProxy.Close() f.StatusPortProxy.Close() } ================================================ FILE: util/client/tiflashclient/status_client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package tiflashclient provides a flexible TiFlash API access to any TiFlash instance. package tiflashclient import ( "github.com/pingcap/tidb-dashboard/util/client/httpclient" "github.com/pingcap/tidb-dashboard/util/distro" ) type StatusClient struct { *httpclient.Client } func NewStatusClient(config httpclient.Config) *StatusClient { config.KindTag = distro.R().TiFlash return &StatusClient{httpclient.New(config)} } func (c *StatusClient) Clone() *StatusClient { return &StatusClient{c.Client.Clone()} } ================================================ FILE: util/client/tikvclient/status_client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package tikvclient provides a flexible TiKV API access to any TiKV instance. package tikvclient import ( "github.com/pingcap/tidb-dashboard/util/client/httpclient" "github.com/pingcap/tidb-dashboard/util/distro" ) type StatusClient struct { *httpclient.Client } func NewStatusClient(config httpclient.Config) *StatusClient { config.KindTag = distro.R().TiKV return &StatusClient{httpclient.New(config)} } func (c *StatusClient) Clone() *StatusClient { return &StatusClient{c.Client.Clone()} } ================================================ FILE: util/client/tiproxyclient/status_client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package tiproxyclient provides a flexible TiProxy API access to any TiProxy instance. package tiproxyclient import ( "github.com/pingcap/tidb-dashboard/util/client/httpclient" "github.com/pingcap/tidb-dashboard/util/distro" ) type StatusClient struct { *httpclient.Client } func NewStatusClient(config httpclient.Config) *StatusClient { config.KindTag = distro.R().TiProxy return &StatusClient{httpclient.New(config)} } func (c *StatusClient) Clone() *StatusClient { return &StatusClient{c.Client.Clone()} } ================================================ FILE: util/client/tsoclient/status_client.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package tsoclient provides a flexible TSO API access to any TSO instance. package tsoclient import ( "github.com/pingcap/tidb-dashboard/util/client/httpclient" "github.com/pingcap/tidb-dashboard/util/distro" ) type StatusClient struct { *httpclient.Client } func NewStatusClient(config httpclient.Config) *StatusClient { config.KindTag = distro.R().TSO return &StatusClient{httpclient.New(config)} } func (c *StatusClient) Clone() *StatusClient { return &StatusClient{c.Client.Clone()} } ================================================ FILE: util/csvutil/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package csvutil import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/csvutil/writer.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package csvutil import ( "encoding/csv" "fmt" "io" "reflect" "time" "github.com/fatih/structtag" "github.com/henrylee2cn/ameda" "github.com/pingcap/tidb-dashboard/util/reflectutil" "github.com/pingcap/tidb-dashboard/util/timeutil" ) // isFieldTaggedAsTime parses the csv field and check whether there is a `time` option. func isFieldTaggedAsTime(field reflect.StructField) bool { switch field.Type.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64: // Accept and do nothing default: return false } tags, err := structtag.Parse(string(field.Tag)) if err != nil { return false } tag, err := tags.Get("csv") if err != nil { return false } return tag.HasOption("time") } type CSVWriter struct { cw *csv.Writer rowBuf []string // The following cache is valid when the passed-in interface's type is unchanged. cacheFieldIsExported []bool cacheFieldIsTime []bool cacheTypeID uintptr } func NewCSVWriter(w io.Writer) *CSVWriter { return &CSVWriter{ cw: csv.NewWriter(w), rowBuf: make([]string, 0, 32), cacheFieldIsExported: make([]bool, 0, 8), cacheFieldIsTime: make([]bool, 0, 8), } } // ensureCacheValid caches information about the fields of `s`. func (w *CSVWriter) ensureCacheValid(objType reflect.Type) { typeID := ameda.RuntimeTypeID(objType) if typeID == w.cacheTypeID { return } w.cacheTypeID = typeID w.cacheFieldIsExported = w.cacheFieldIsExported[:0] w.cacheFieldIsTime = w.cacheFieldIsTime[:0] fieldsCount := objType.NumField() for i := range fieldsCount { f := objType.Field(i) w.cacheFieldIsExported = append(w.cacheFieldIsExported, reflectutil.IsFieldExported(f)) w.cacheFieldIsTime = append(w.cacheFieldIsTime, isFieldTaggedAsTime(f)) } } func (w *CSVWriter) WriteAsHeader(s interface{}) error { objValue := reflect.Indirect(reflect.ValueOf(s)) objType := objValue.Type() fieldsCount := objType.NumField() w.ensureCacheValid(objType) w.rowBuf = w.rowBuf[:0] for i := range fieldsCount { if !w.cacheFieldIsExported[i] { continue } name := objType.Field(i).Name w.rowBuf = append(w.rowBuf, name) } return w.cw.Write(w.rowBuf) } func (w *CSVWriter) WriteAsRow(s interface{}) error { objValue := reflect.Indirect(reflect.ValueOf(s)) objType := objValue.Type() fieldsCount := objType.NumField() w.ensureCacheValid(objType) w.rowBuf = w.rowBuf[:0] for i := range fieldsCount { if !w.cacheFieldIsExported[i] { continue } fv := objValue.Field(i).Interface() // ~300ns if w.cacheFieldIsTime[i] { ts, _ := ameda.InterfaceToInt64(fv) w.rowBuf = append(w.rowBuf, timeutil.FormatInUTC(time.Unix(ts, 0))) continue } w.rowBuf = append(w.rowBuf, fmt.Sprint(fv)) // ~1000ns } return w.cw.Write(w.rowBuf) } func (w *CSVWriter) Flush() { w.cw.Flush() } ================================================ FILE: util/csvutil/writer_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package csvutil import ( "bytes" "strings" "testing" "github.com/stretchr/testify/require" ) type foo struct { Digest string Query string Instance string DB string ConnectionID string Success int Timestamp float64 TimestampAsTime float64 `csv:",time"` QueryTime float64 privateField string } func TestCSVWriter(t *testing.T) { buf := bytes.Buffer{} w := NewCSVWriter(&buf) err := w.WriteAsHeader(&foo{}) require.NoError(t, err) rec := foo{ Digest: "digest_foo", Query: "query_bar", Instance: "instance_box", DB: "db_abc", ConnectionID: "id_123", Success: 1, Timestamp: 456, TimestampAsTime: 1633106800.411, QueryTime: 789, privateField: "pfxyz", } err = w.WriteAsRow(&rec) require.NoError(t, err) rec = foo{ ConnectionID: "id123", Success: 2, Timestamp: 0, TimestampAsTime: 0, QueryTime: 123, privateField: "pro", Digest: "digestFoo", Query: "queryBar", Instance: "instanceBox", DB: "dbAbc", } err = w.WriteAsRow(&rec) require.NoError(t, err) w.Flush() expected := ` Digest,Query,Instance,DB,ConnectionID,Success,Timestamp,TimestampAsTime,QueryTime digest_foo,query_bar,instance_box,db_abc,id_123,1,456,2021-10-01 16:46:40 UTC,789 digestFoo,queryBar,instanceBox,dbAbc,id123,2,0,1970-01-01 00:00:00 UTC,123 ` require.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buf.String())) } func TestCSVWriterWriteTimeTag(t *testing.T) { //nolint:govet type fooStruct struct { Field1 int `csv:` Field2 int `foo` Field3 int `foo,bar` Field4 int `foo:"" csv:"time"` Field5 int `foo:"" csv:","` Field6 int `foo:"" csv:",time"` } buf := bytes.Buffer{} w := NewCSVWriter(&buf) err := w.WriteAsRow(fooStruct{}) require.NoError(t, err) w.Flush() require.Equal(t, "0,0,0,0,0,1970-01-01 00:00:00 UTC\n", buf.String()) type barStruct struct { FieldInt int `csv:",time"` FieldUint64 uint64 `csv:",time"` FieldFloat32 float64 `csv:",time"` } buf.Reset() err = w.WriteAsRow(barStruct{ FieldInt: 1633106800, FieldUint64: 1633106801, FieldFloat32: 1633106802, }) require.NoError(t, err) w.Flush() require.Equal(t, "2021-10-01 16:46:40 UTC,2021-10-01 16:46:41 UTC,2021-10-01 16:46:42 UTC\n", buf.String()) } func BenchmarkCSVWriterWriteAsHeader(b *testing.B) { buf := bytes.Buffer{} w := NewCSVWriter(&buf) b.ResetTimer() for i := 0; i < b.N; i++ { _ = w.WriteAsHeader(&foo{}) } } func BenchmarkCSVWriterWriteAsRow(b *testing.B) { b.ReportAllocs() buf := bytes.Buffer{} w := NewCSVWriter(&buf) rec := foo{ Digest: "digest_foo", Query: "query_bar", Instance: "instance_box", DB: "db_abc", ConnectionID: "id_123", Success: 1, Timestamp: 456, QueryTime: 789, privateField: "pfxyz", } b.ResetTimer() for i := 0; i < b.N; i++ { _ = w.WriteAsRow(&rec) } } ================================================ FILE: util/distro/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package distro import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/distro/distro.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package distro provides a type-safe distribution resource framework. // Distribution resource determines how component names are displayed in errors, logs and so on. // For example, a distribution resource can define "TiDB" to be displayed as "MyTiDB". package distro import ( "encoding/json" "errors" "io" "os" "path/filepath" "sync" "go.uber.org/atomic" ) type DistributionResource struct { IsDistro bool `json:"is_distro,omitempty"` TiDB string `json:"tidb,omitempty"` TiKV string `json:"tikv,omitempty"` PD string `json:"pd,omitempty"` TiFlash string `json:"tiflash,omitempty"` TiCDC string `json:"ticdc,omitempty"` TiProxy string `json:"tiproxy,omitempty"` TSO string `json:"tso,omitempty"` Scheduling string `json:"scheduling,omitempty"` } var defaultDistroRes = DistributionResource{ IsDistro: false, TiDB: "TiDB", TiKV: "TiKV", PD: "PD", TiFlash: "TiFlash", TiCDC: "TiCDC", TiProxy: "TiProxy", TSO: "TSO", Scheduling: "Scheduling", } var ( globalDistroRes atomic.Value replaceGlobalMu sync.Mutex ) // ReplaceGlobal replaces the global distribution resource with the specified one. Missing fields in the // resource will be filled using default values. func ReplaceGlobal(r DistributionResource) func() { // TODO: To be replaced by atomic.Value.Swap() in Go 1.16 replaceGlobalMu.Lock() defer replaceGlobalMu.Unlock() // Save current resources for restoring back. currentGlobals := *R() // Fill missing resources with the default one by using JSON Unmarshal. newResource := defaultDistroRes rJSON, _ := json.Marshal(r) // This will never fail _ = json.Unmarshal(rJSON, &newResource) // This will never fail globalDistroRes.Store(&newResource) return func() { ReplaceGlobal(currentGlobals) } } // R gets the current global distribution resource. The returned value must NOT be modified. func R() *DistributionResource { r := globalDistroRes.Load() if r == nil { return &defaultDistroRes } return r.(*DistributionResource) } func ReadResourceStringsFromFile(filePath string) (DistributionResource, error) { distroStringsRes := DistributionResource{} info, err := os.Stat(filePath) if errors.Is(err, os.ErrNotExist) || info.IsDir() { // ignore if file not exist or it is a folder return distroStringsRes, nil } if err != nil { // may be permission-like errors return distroStringsRes, err } distroStringsFile, err := os.Open(filepath.Clean(filePath)) if err != nil { return distroStringsRes, err } defer func() { _ = distroStringsFile.Close() }() data, err := io.ReadAll(distroStringsFile) if err != nil { return distroStringsRes, err } err = json.Unmarshal(data, &distroStringsRes) return distroStringsRes, err } ================================================ FILE: util/distro/distro_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package distro import ( "testing" "github.com/stretchr/testify/require" ) func TestR(t *testing.T) { require.Equal(t, "TiDB", R().TiDB) } func TestReplaceGlobal(t *testing.T) { restoreFn := ReplaceGlobal(DistributionResource{ TiDB: "myTiDB", PD: "", }) require.Equal(t, false, R().IsDistro) require.Equal(t, "myTiDB", R().TiDB) require.Equal(t, "PD", R().PD) require.Equal(t, "TiKV", R().TiKV) restoreFn() require.Equal(t, "TiDB", R().TiDB) require.Equal(t, "PD", R().PD) require.Equal(t, "TiKV", R().TiKV) restoreFn = ReplaceGlobal(DistributionResource{ IsDistro: true, }) require.Equal(t, true, R().IsDistro) require.Equal(t, "TiDB", R().TiDB) require.Equal(t, "PD", R().PD) require.Equal(t, "TiKV", R().TiKV) restoreFn() } ================================================ FILE: util/featureflag/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package featureflag import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/featureflag/featureflag.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package featureflag import ( "net/http" "strings" "github.com/Masterminds/semver" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/pingcap/tidb-dashboard/util/rest" ) var ErrFeatureUnsupported = errorx.CommonErrors.NewType("feature_unsupported") type FeatureFlag struct { name string constraints []string isSupported bool } func newFeatureFlag(name, targetVersion string, constraints ...string) *FeatureFlag { f := &FeatureFlag{name: name, constraints: constraints} f.isSupported = f.isSupportedIn(targetVersion) return f } func (f *FeatureFlag) Name() string { return f.name } func (f *FeatureFlag) IsSupported() bool { return f.isSupported } // VersionGuard returns gin.HandlerFunc as guard middleware. // It will determine if features are available in the target version. func (f *FeatureFlag) VersionGuard() gin.HandlerFunc { return func(c *gin.Context) { if !f.isSupported { rest.Error(c, ErrFeatureUnsupported.New("%s", f.name).WithProperty(rest.HTTPCodeProperty(http.StatusForbidden))) // nolint: vet c.Abort() return } c.Next() } } // IsSupportedIn checks if a semantic version fits within a set of constraints // pdVersion, standaloneVersion examples: "v5.2.2", "v5.3.0", "v5.4.0-alpha-xxx", "5.3.0" (semver can handle `v` prefix by itself) // constraints examples: "~5.2.2", ">= 5.3.0", see semver docs to get more information. func (f *FeatureFlag) isSupportedIn(targetVersion string) bool { // drop "-alpha-xxx" suffix versionWithoutSuffix := strings.Split(targetVersion, "-")[0] v, err := semver.NewVersion(versionWithoutSuffix) if err != nil { return false } for _, ver := range f.constraints { c, err := semver.NewConstraint(ver) if err != nil { continue } if c.Check(v) { return true } } return false } ================================================ FILE: util/featureflag/featureflag_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package featureflag import ( "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/rest" ) func Test_Name(t *testing.T) { f1 := &FeatureFlag{} require.Equal(t, f1.Name(), "") f2 := newFeatureFlag("testFeature", "v5.3.0", ">= 5.3.0") require.Equal(t, f2.Name(), "testFeature") } func Test_IsSupported(t *testing.T) { type Args struct { target string constraints []string } tests := []struct { want bool args Args }{ {want: false, args: Args{target: "v4.2.0", constraints: []string{">= 5.3.0"}}}, {want: false, args: Args{target: "v5.2.0", constraints: []string{">= 5.3.0"}}}, {want: true, args: Args{target: "v5.3.0", constraints: []string{">= 5.3.0"}}}, {want: false, args: Args{target: "v5.2.0-alpha-xxx", constraints: []string{">= 5.3.0"}}}, {want: true, args: Args{target: "v5.3.0-alpha-xxx", constraints: []string{">= 5.3.0"}}}, {want: true, args: Args{target: "v5.3.0", constraints: []string{"= 5.3.0"}}}, {want: false, args: Args{target: "v5.3.1", constraints: []string{"= 5.3.0"}}}, } for _, tt := range tests { ff := newFeatureFlag("testFeature", tt.args.target, tt.args.constraints...) require.Equal(t, tt.want, ff.IsSupported()) } } func Test_VersionGuard(t *testing.T) { r := require.New(t) f1 := newFeatureFlag("testFeature1", "v5.3.0", ">= 5.3.0") f2 := newFeatureFlag("testFeature2", "v5.3.0", ">= 5.3.1") // success e := gin.Default() e.Use(f1.VersionGuard()) e.GET("/ping", func(c *gin.Context) { c.String(http.StatusOK, "pong") }) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/ping", nil) e.ServeHTTP(w, req) r.Equal(http.StatusOK, w.Code) r.Equal("pong", w.Body.String()) // abort handled := false e2 := gin.Default() e2.Use(func(c *gin.Context) { c.Next() handled = true r.True(errorx.IsOfType(c.Errors.Last().Err, ErrFeatureUnsupported)) }) e2.Use(f1.VersionGuard(), f2.VersionGuard()) e2.GET("/ping", func(c *gin.Context) { c.String(http.StatusOK, "pong") }) w2 := httptest.NewRecorder() req2, _ := http.NewRequest("GET", "/ping", nil) e2.ServeHTTP(w2, req2) r.Equal(true, handled) } func Test_VersionGuardWith_ErrorHandlerFn(t *testing.T) { r := require.New(t) f := newFeatureFlag("testFeature", "v5.3.0", ">= 5.3.1") e := gin.Default() e.Use(rest.ErrorHandlerFn()) e.Use(f.VersionGuard()) e.GET("/ping", func(c *gin.Context) { c.String(http.StatusOK, "pong") }) w2 := httptest.NewRecorder() req2, _ := http.NewRequest("GET", "/ping", nil) e.ServeHTTP(w2, req2) r.Equal(http.StatusForbidden, w2.Code) } ================================================ FILE: util/featureflag/registry.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package featureflag import ( "sort" ) type Registry struct { version string flags map[string]*FeatureFlag supportedFeatures map[string]struct{} } func NewRegistry(version string) *Registry { return &Registry{ version: version, flags: map[string]*FeatureFlag{}, supportedFeatures: map[string]struct{}{}, } } // Register create and register feature flag to registry. func (m *Registry) Register(name string, constraints ...string) *FeatureFlag { if f, ok := m.flags[name]; ok { return f } nf := newFeatureFlag(name, m.version, constraints...) m.flags[name] = nf if nf.IsSupported() { m.supportedFeatures[nf.Name()] = struct{}{} } return nf } // SupportedFeatures returns supported feature's names. func (m *Registry) SupportedFeatures() []string { sf := make([]string, 0, len(m.supportedFeatures)) for k := range m.supportedFeatures { sf = append(sf, k) } sort.Strings(sf) return sf } ================================================ FILE: util/featureflag/registry_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package featureflag import ( "testing" "github.com/stretchr/testify/require" ) func Test_Register(t *testing.T) { m := NewRegistry("v5.3.0") tests := []*struct { supported bool name string constraints []string flag *FeatureFlag }{ {supported: true, name: "testFeature1", constraints: []string{">= 5.3.0"}}, {supported: true, name: "testFeature2", constraints: []string{">= 4.0.0"}}, {supported: false, name: "testFeature3", constraints: []string{">= 5.3.1"}}, } for _, tt := range tests { tt.flag = m.Register(tt.name, tt.constraints...) } for _, tt := range tests { // check whether flag is in flags & supportedFeatures require.Equal(t, m.flags[tt.flag.name], tt.flag) _, ok := m.supportedFeatures[tt.flag.name] require.Equal(t, tt.supported, ok) } // duplicated register f := m.Register("testFeature3", ">= 5.3.2") require.Equal(t, f.name, "testFeature3") require.Equal(t, f.constraints[0], ">= 5.3.1") } func Test_SupportedFeatures(t *testing.T) { m := NewRegistry("v5.3.0") tests := []*struct { supported bool name string constraints []string }{ {supported: true, name: "testFeature1", constraints: []string{">= 5.3.0"}}, {supported: true, name: "testFeature2", constraints: []string{">= 4.0.0"}}, {supported: false, name: "testFeature3", constraints: []string{">= 5.3.1"}}, } for _, tt := range tests { m.Register(tt.name, tt.constraints...) } require.Equal(t, []string{"testFeature1", "testFeature2"}, m.SupportedFeatures()) } ================================================ FILE: util/fxprinter/fxprinter.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package fxprinter import ( "github.com/pingcap/log" "go.uber.org/fx" ) type DebugLogPrinter struct{} func (p DebugLogPrinter) Printf(m string, args ...interface{}) { log.S().Debugf(m, args...) } func NewDebugLogPrinter() fx.Printer { return DebugLogPrinter{} } ================================================ FILE: util/gormutil/datatype/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package datatype import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/gormutil/datatype/int.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package datatype import ( "database/sql" "database/sql/driver" "encoding/json" "fmt" "strconv" "gorm.io/gorm/schema" ) // Int is an alternative type to the standard int type. Int can be mapped to all numeric SQL field types, // including floats. // NULL value will be scanned as 0. To distinguish zero and null, use nullable.Int instead. type Int int var _ sql.Scanner = (*Int)(nil) // Scan implements sql.Scanner. func (n *Int) Scan(value interface{}) error { if value == nil { *n = 0 return nil } switch v := value.(type) { case int64: *n = Int(v) return nil case float64: *n = Int(v) return nil case []uint8: nv, err := strconv.Atoi(string(v)) if err == nil { *n = Int(nv) return nil } fv, err := strconv.ParseFloat(string(v), 64) if err != nil { return err } *n = Int(fv) return nil } return fmt.Errorf("can't convert %T to Int", value) } var _ schema.GormDataTypeInterface = Int(0) // GormDataType implements schema.GormDataTypeInterface. func (n Int) GormDataType() string { return "BIGINT" } var _ driver.Valuer = Int(0) // Value implements driver.Valuer. func (n Int) Value() (driver.Value, error) { return int64(n), nil } var _ json.Marshaler = Int(0) // MarshalJSON implements json.Marshaler. func (n Int) MarshalJSON() ([]byte, error) { return []byte(strconv.FormatInt(int64(n), 10)), nil } var _ json.Unmarshaler = (*Int)(nil) // UnmarshalJSON implements json.Unmarshaler. func (n *Int) UnmarshalJSON(data []byte) error { var v int if err := json.Unmarshal(data, &v); err != nil { return err } *n = Int(v) return nil } ================================================ FILE: util/gormutil/datatype/int_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package datatype import ( "encoding/json" "testing" "github.com/stretchr/testify/require" ) func TestIntJSON(t *testing.T) { nv := Int(123) v, err := json.Marshal(nv) require.NoError(t, err) require.Equal(t, string(v), "123") st := struct { Foo Int }{ Foo: Int(415425), } v, err = json.Marshal(st) require.NoError(t, err) require.JSONEq(t, `{"Foo":415425}`, string(v)) var nv2 Int err = json.Unmarshal([]byte("12345"), &nv2) require.NoError(t, err) require.Equal(t, Int(12345), nv2) err = json.Unmarshal([]byte(`{"Foo":56789}`), &st) require.NoError(t, err) require.Equal(t, Int(56789), st.Foo) err = json.Unmarshal([]byte(`{"Foo":"123"}`), &st) require.Error(t, err) err = json.Unmarshal([]byte(`{"Foo":1300.45}`), &st) require.Error(t, err) nv3 := Int(48691071) v, err = json.Marshal(nv3) require.NoError(t, err) err = json.Unmarshal(v, &nv2) require.NoError(t, err) require.Equal(t, nv2, nv3) } ================================================ FILE: util/gormutil/datatype/int_withdb_test.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. //go:build integration package datatype import ( "fmt" "testing" mysqldriver "github.com/go-sql-driver/mysql" "github.com/stretchr/testify/suite" "gorm.io/gorm" "github.com/pingcap/tidb-dashboard/util/testutil" ) type IntORMSuite struct { suite.Suite usePrepareStatement bool db *testutil.TestDB tableName string } func (suite *IntORMSuite) SetupSuite() { suite.db = testutil.OpenTestDB(suite.T(), func(_ *mysqldriver.Config, config *gorm.Config) { config.PrepareStmt = suite.usePrepareStatement }) suite.tableName = "`" + suite.db.NewID() + "`" suite.db.MustExec(fmt.Sprintf(`CREATE TABLE %s ( a bigint, b double, c varchar(50), d boolean, e varchar(10) )`, suite.tableName)) suite.db.MustExec(fmt.Sprintf(`INSERT INTO %s VALUES ( 73371569, 965511.2870, "123456", 1, "abc" )`, suite.tableName)) } func (suite *IntORMSuite) TearDownSuite() { suite.db.MustExec(fmt.Sprintf(`DROP TABLE IF EXISTS %s`, suite.tableName)) suite.db.MustClose() } func (suite *IntORMSuite) TestScanFromInt() { var r struct { A Int } err := suite.db.Gorm().Table(suite.tableName).Select("a").Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(Int(73371569), r.A) } func (suite *IntORMSuite) TestScanFromDouble() { var r struct { B Int } err := suite.db.Gorm().Table(suite.tableName).Select("b").Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(Int(965511), r.B) } func (suite *IntORMSuite) TestScanFromString() { var r struct { C Int } err := suite.db.Gorm().Table(suite.tableName).Select("c").Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(Int(123456), r.C) } func (suite *IntORMSuite) TestScanFromBoolean() { var r struct { D Int } err := suite.db.Gorm().Table(suite.tableName).Select("d").Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(Int(1), r.D) } func (suite *IntORMSuite) TestScanFromNonNumericString() { var r struct { E Int } err := suite.db.Gorm().Table(suite.tableName).Select("e").Take(&r).Error suite.Require().Error(err) } // Scanning double into int is invalid. That's why we need Int. func (suite *IntORMSuite) TestScanDoubleToStdInt() { var r struct { B int } err := suite.db.Gorm().Table(suite.tableName).Select("b").Take(&r).Error suite.Require().Error(err) } func (suite *IntORMSuite) TestWhere() { var r struct { A Int } err := suite.db.Gorm(). Table(suite.tableName). Select("a"). Where("a = ?", Int(73371569)). Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(Int(73371569), r.A) err = suite.db.Gorm(). Table(suite.tableName). Select("a"). Where("a > ?", Int(0)). Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(Int(73371569), r.A) err = suite.db.Gorm(). Table(suite.tableName). Select("a"). Where("a = ?", Int(123)). Take(&r).Error suite.Require().Error(err) suite.Require().Equal(gorm.ErrRecordNotFound, err) err = suite.db.Gorm(). Table(suite.tableName). Select("a"). Where("a > ?", Int(73371570)). Take(&r).Error suite.Require().Error(err) suite.Require().Equal(gorm.ErrRecordNotFound, err) err = suite.db.Gorm(). Table(suite.tableName). Select("a"). Where("a = ?", "73371569"). Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(Int(73371569), r.A) // Matching a double field will never succeed, since int lose precision val := 965511.2870 err = suite.db.Gorm(). Table(suite.tableName). Select("b"). Where("b = ?", Int(val)). Take(&r).Error suite.Require().Error(err) suite.Require().Equal(gorm.ErrRecordNotFound, err) } func (suite *IntORMSuite) TestWhereInIndex() { tableName := "`" + suite.db.NewID() + "`" suite.db.MustExec(fmt.Sprintf(`CREATE TABLE %s ( id int, val int, INDEX idx (val) )`, tableName)) suite.db.MustExec(fmt.Sprintf(`INSERT INTO %s VALUES ( 1, 42160690 )`, tableName)) defer suite.db.MustExec(fmt.Sprintf(`DROP TABLE IF EXISTS %s`, tableName)) var r struct { Val Int } err := suite.db.Gorm(). Table(tableName). Select("val"). Where("val = ?", Int(42160690)). Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(Int(42160690), r.Val) // Verify Plan is Index Scan explain := suite.db. MustExplain(fmt.Sprintf("SELECT val FROM %s WHERE val = ?", tableName), Int(42160690)) testutil.RequireIndexRangeScan(suite.T(), explain) } func (suite *IntORMSuite) TestInsert() { tableName := "`" + suite.db.NewID() + "`" suite.db.MustExec(fmt.Sprintf(`CREATE TABLE %s ( id int, val bigint )`, tableName)) defer suite.db.MustExec(fmt.Sprintf(`DROP TABLE IF EXISTS %s`, tableName)) type model struct { ID int Val Int } err := suite.db.Gorm().Table(tableName).Create(model{ ID: 10, Val: Int(12346), }).Error suite.Require().NoError(err) var r model err = suite.db.Gorm().Table(tableName).Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(10, r.ID) suite.Require().Equal(Int(12346), r.Val) } func (suite *IntORMSuite) TestScanNull() { tableName := "`" + suite.db.NewID() + "`" suite.db.MustExec(fmt.Sprintf(`CREATE TABLE %s ( id int, val bigint )`, tableName)) suite.db.MustExec(fmt.Sprintf(`INSERT INTO %s VALUES ( 100, null )`, tableName)) defer suite.db.MustExec(fmt.Sprintf(`DROP TABLE IF EXISTS %s`, tableName)) var r struct { Val Int } err := suite.db.Gorm(). Table(tableName). Select("val"). Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(Int(0), r.Val) } func TestIntInORM(t *testing.T) { suite.Run(t, &IntORMSuite{ usePrepareStatement: false, }) } func TestIntInORMWithPrepared(t *testing.T) { suite.Run(t, &IntORMSuite{ usePrepareStatement: true, }) } ================================================ FILE: util/gormutil/datatype/nullable/nullable.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package nullable // NullableTimestamp, NullableInt to be implemented ================================================ FILE: util/gormutil/datatype/timestamp.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package datatype import ( "database/sql" "database/sql/driver" "encoding/json" "fmt" "strconv" "time" "gorm.io/gorm/schema" ) // Timestamp is serialized as microsecond-precision unix timestamps in JSON, and can be mapped to // the "timestamp" field type in MySQL / TiDB. // NULL value will be scanned as unix timestamp 0. To distinguish zero timestamp and null timestamp, // use nullable.Timestamp instead. // // This struct is only used for compatibility with existing "timestamp" field types. // Int should be always preferred to store timestamps wherever possible. type Timestamp struct { time.Time } var _ sql.Scanner = (*Timestamp)(nil) // Scan implements sql.Scanner. func (n *Timestamp) Scan(value interface{}) error { if value == nil { n.Time = time.Unix(0, 0) return nil } switch v := value.(type) { case time.Time: n.Time = v return nil } return fmt.Errorf("can't convert %T to Timestamp", value) } var _ schema.GormDataTypeInterface = Timestamp{} // GormDataType implements schema.GormDataTypeInterface. func (n Timestamp) GormDataType() string { return "TIMESTAMP" } var _ driver.Valuer = Timestamp{} // Value implements driver.Valuer. func (n Timestamp) Value() (driver.Value, error) { return n.Time, nil } var _ json.Marshaler = Timestamp{} // MarshalJSON implements json.Marshaler. func (n Timestamp) MarshalJSON() ([]byte, error) { ts := n.UnixNano() / 1e3 return []byte(strconv.FormatInt(ts, 10)), nil } var _ json.Unmarshaler = (*Timestamp)(nil) // UnmarshalJSON implements json.Unmarshaler. func (n *Timestamp) UnmarshalJSON(data []byte) error { var ts int64 if err := json.Unmarshal(data, &ts); err != nil { return err } n.Time = time.Unix(0, ts*1e3) return nil } ================================================ FILE: util/gormutil/datatype/timestamp_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package datatype import ( "encoding/json" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/testutil" ) func TestTimestampORMType(t *testing.T) { type TestModel struct { Field Timestamp } db := testutil.OpenMockDB(t) defer db.MustClose() db.Mocker(). ExpectExec("CREATE TABLE `test_models` (`field` TIMESTAMP)"). WillReturnResult(sqlmock.NewResult(0, 1)) err := db.Gorm().Migrator().CreateTable(TestModel{}) require.NoError(t, err) db.MustMeetMockExpectation() } func TestTimestampJSON(t *testing.T) { ts := Timestamp{Time: time.Unix(0, 1633880141307801631)} v, err := json.Marshal(ts) require.NoError(t, err) require.Equal(t, string(v), "1633880141307801") ts = Timestamp{Time: time.Unix(0, 500)} v, err = json.Marshal(ts) require.NoError(t, err) require.Equal(t, string(v), "0") st := struct { Foo Timestamp }{ Foo: Timestamp{Time: time.Unix(0, 1633880141307801631)}, } v, err = json.Marshal(st) require.NoError(t, err) require.JSONEq(t, `{"Foo":1633880141307801}`, string(v)) var ts2 Timestamp err = json.Unmarshal([]byte("12345"), &ts2) require.NoError(t, err) require.Equal(t, int64(12345000), ts2.UnixNano()) err = json.Unmarshal([]byte(`{"Foo":12345}`), &st) require.NoError(t, err) require.Equal(t, int64(12345000), st.Foo.UnixNano()) err = json.Unmarshal([]byte(`{"Foo":"54321"}`), &st) require.Error(t, err) err = json.Unmarshal([]byte(`{"Foo":123.45}`), &st) require.Error(t, err) ts3 := Timestamp{Time: time.Unix(0, 1633880141307801000)} v, err = json.Marshal(ts3) require.NoError(t, err) err = json.Unmarshal(v, &ts2) require.NoError(t, err) require.Equal(t, ts2, ts3) } ================================================ FILE: util/gormutil/datatype/timestamp_withdb_test.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. //go:build integration package datatype import ( "fmt" "testing" "time" mysqldriver "github.com/go-sql-driver/mysql" "github.com/stretchr/testify/suite" "gorm.io/gorm" "github.com/pingcap/tidb-dashboard/util/testutil" ) type TimestampORMSuite struct { suite.Suite usePrepareStatement bool db *testutil.TestDB tableName string } func (suite *TimestampORMSuite) SetupSuite() { suite.db = testutil.OpenTestDB(suite.T(), func(_ *mysqldriver.Config, config *gorm.Config) { config.PrepareStmt = suite.usePrepareStatement }) suite.tableName = "`" + suite.db.NewID() + "`" suite.db.MustExec(fmt.Sprintf(`CREATE TABLE %s ( a bigint, b double, c varchar(50), d timestamp(6), e datetime(6) )`, suite.tableName)) suite.db.MustExec(fmt.Sprintf(`INSERT INTO %s VALUES ( 1633803684694123, 1633803684.694123, "2021-10-09 18:21:24.694123", FROM_UNIXTIME("1633803684.694123"), "2021-10-09 18:21:24.694123" )`, suite.tableName)) } func (suite *TimestampORMSuite) TearDownSuite() { suite.db.MustExec(fmt.Sprintf(`DROP TABLE IF EXISTS %s`, suite.tableName)) suite.db.MustClose() } func (suite *TimestampORMSuite) SetupTest() { suite.db.MustExec("SET time_zone = '+00:00'") } func (suite *TimestampORMSuite) TestCheckFixture() { var row struct { C1 float64 C2 float64 } err := suite.db.Gorm().Table(suite.tableName). Select([]string{"UNIX_TIMESTAMP(d) AS C1", "UNIX_TIMESTAMP(e) AS C2"}). Find(&row). Error suite.Require().NoError(err) suite.Require().Equal(1633803684.694123, row.C1) suite.Require().Equal(1633803684.694123, row.C2) } func (suite *TimestampORMSuite) TestScanFromInt() { var r Timestamp err := suite.db.Gorm().Table(suite.tableName).Select("a").Take(&r).Error suite.Require().Error(err) } func (suite *TimestampORMSuite) TestScanFromDouble() { var r Timestamp err := suite.db.Gorm().Table(suite.tableName).Select("b").Take(&r).Error suite.Require().Error(err) } func (suite *TimestampORMSuite) TestScanFromString() { var r Timestamp err := suite.db.Gorm().Table(suite.tableName).Select("c").Take(&r).Error suite.Require().Error(err) } func (suite *TimestampORMSuite) TestScanFromTimestamp() { var r Timestamp // WARN: This test case shows that, even for "TIMESTAMP" field types, MySQL will transmit the value // in civil format (like Y-m-d H:m:s) and drop the timezone part. Then, if the go-mysql driver thinks // it's in UTC time zone then we will get the wrong result. // To ensure the result correctness for "TIMESTAMP" field types, the UTC time_zone must be enforced in both // driver side and database side. suite.db.MustExec("SET time_zone = '+08:00'") err := suite.db.Gorm().Table(suite.tableName).Select("d").Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(int64(1633832484694123000), r.UnixNano()) suite.db.MustExec("SET time_zone = '+00:00'") err = suite.db.Gorm().Table(suite.tableName).Select("d").Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(int64(1633803684694123000), r.UnixNano()) // Another safe way to deal with the TIMESTAMP is to use UNIX_TIMESTAMP function: suite.db.MustExec("SET time_zone = '+03:00'") // Session time zone doesn't matter var r2 float64 err = suite.db.Gorm().Table(suite.tableName).Select("UNIX_TIMESTAMP(d)").Take(&r2).Error suite.Require().NoError(err) suite.Require().Equal(1633803684.694123, r2) } func (suite *TimestampORMSuite) TestScanFromDatetime() { var r Timestamp suite.db.MustExec("SET time_zone = '+08:00'") err := suite.db.Gorm().Table(suite.tableName).Select("e").Take(&r).Error suite.Require().NoError(err) // Note: MySQL return the "Y-m-d H:m:s" as it is, while the driver will treat it as in UTC. suite.Require().Equal(int64(1633803684694123000), r.UnixNano()) suite.db.MustExec("SET time_zone = '+04:00'") // Session time zone doesn't matter. err = suite.db.Gorm().Table(suite.tableName).Select("e").Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(int64(1633803684694123000), r.UnixNano()) } // TestScanToGoTypes verifies different behaviours when scanning a MySQL TIMESTAMP field type into different // Golang types. func (suite *TimestampORMSuite) TestScanToGoTypes() { var r1 int err := suite.db.Gorm().Table(suite.tableName).Select("d").Take(&r1).Error suite.Require().Error(err) var r2 float64 err = suite.db.Gorm().Table(suite.tableName).Select("d").Take(&r2).Error suite.Require().Error(err) // Scanning a TIMESTAMP field type into String is valid. var r3 string suite.db.MustExec("SET time_zone = '+00:00'") err = suite.db.Gorm().Table(suite.tableName).Select("d").Take(&r3).Error suite.Require().NoError(err) suite.Require().Equal("2021-10-09T18:21:24.694123Z", r3) suite.db.MustExec("SET time_zone = '+03:00'") err = suite.db.Gorm().Table(suite.tableName).Select("d").Take(&r3).Error suite.Require().NoError(err) suite.Require().Equal("2021-10-09T21:21:24.694123Z", r3) } func (suite *TimestampORMSuite) TestWhere() { var r Timestamp err := suite.db.Gorm(). Table(suite.tableName). Select("d"). Where("d = ?", Timestamp{Time: time.Unix(0, 1633803684694123000)}). Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(int64(1633803684694123000), r.UnixNano()) err = suite.db.Gorm(). Table(suite.tableName). Select("d"). Where("d > ?", Timestamp{Time: time.Unix(0, 1633803684694000000)}). Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(int64(1633803684694123000), r.UnixNano()) err = suite.db.Gorm(). Table(suite.tableName). Select("d"). Where("d = ?", Timestamp{Time: time.Unix(0, 1633803684694000000)}). Take(&r).Error suite.Require().Error(err) suite.Require().Equal(gorm.ErrRecordNotFound, err) err = suite.db.Gorm(). Table(suite.tableName). Select("d"). Where("d > ?", Timestamp{Time: time.Unix(0, 1633803684694123001)}). Take(&r).Error suite.Require().Error(err) suite.Require().Equal(gorm.ErrRecordNotFound, err) // It is also possible to specify string directly for a TIMESTAMP type. err = suite.db.Gorm(). Table(suite.tableName). Select("d"). Where("d = ?", "2021-10-09 18:21:24.694123"). Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(int64(1633803684694123000), r.UnixNano()) } func (suite *TimestampORMSuite) TestWhereInIndex() { tableName := "`" + suite.db.NewID() + "`" suite.db.MustExec(fmt.Sprintf(`CREATE TABLE %s ( id int, ts timestamp(6), INDEX idx (ts) )`, tableName)) suite.db.MustExec(fmt.Sprintf(`INSERT INTO %s VALUES ( 1, FROM_UNIXTIME(1633880141.307801) )`, tableName)) defer suite.db.MustExec(fmt.Sprintf(`DROP TABLE IF EXISTS %s`, tableName)) var r Timestamp err := suite.db.Gorm(). Table(tableName). Select("ts"). Where("ts = ?", Timestamp{Time: time.Unix(0, 1633880141307801000)}). Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(int64(1633880141307801000), r.UnixNano()) // Verify Plan is Index Scan explain := suite.db. MustExplain(fmt.Sprintf("SELECT ts FROM %s WHERE ts = ?", tableName), Timestamp{Time: time.Unix(0, 1633880141307801000)}) testutil.RequireIndexRangeScan(suite.T(), explain) } func (suite *TimestampORMSuite) TestInsert() { tableName := "`" + suite.db.NewID() + "`" suite.db.MustExec(fmt.Sprintf(`CREATE TABLE %s ( id int, ts timestamp(6) )`, tableName)) defer suite.db.MustExec(fmt.Sprintf(`DROP TABLE IF EXISTS %s`, tableName)) type model struct { ID int Ts Timestamp } err := suite.db.Gorm().Table(tableName).Create(model{ ID: 5, Ts: Timestamp{Time: time.Unix(0, 1633880957785123456)}, }).Error suite.Require().NoError(err) var r model err = suite.db.Gorm().Table(tableName).Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(5, r.ID) suite.Require().Equal(int64(1633880957785123000), r.Ts.UnixNano()) } func (suite *TimestampORMSuite) TestScanNull() { tableName := "`" + suite.db.NewID() + "`" suite.db.MustExec(fmt.Sprintf(`CREATE TABLE %s ( id int, ts timestamp(6) )`, tableName)) suite.db.MustExec(fmt.Sprintf(`INSERT INTO %s VALUES ( 100, null )`, tableName)) defer suite.db.MustExec(fmt.Sprintf(`DROP TABLE IF EXISTS %s`, tableName)) var r Timestamp err := suite.db.Gorm(). Table(tableName). Select("ts"). Take(&r).Error suite.Require().NoError(err) suite.Require().Equal(int64(0), r.UnixNano()) } func TestTimestampInORM(t *testing.T) { suite.Run(t, &TimestampORMSuite{ usePrepareStatement: false, }) } func TestTimestampInORMWithPrepared(t *testing.T) { suite.Run(t, &TimestampORMSuite{ usePrepareStatement: true, }) } ================================================ FILE: util/gormutil/virtualview/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package virtualview import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/gormutil/virtualview/schema.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package virtualview import ( "fmt" "reflect" "sort" "strings" "github.com/antonmedv/expr/ast" "github.com/antonmedv/expr/parser" "github.com/fatih/structtag" "gorm.io/gorm/schema" ) type viewFieldProps struct { jsonNameL string // JSON name in lower case. Possibly empty, means field is JSON unexported. viewExpr string // Possibly empty, means either vexpr tag is not set, or is set to empty columnNameL string // DB column name in lower case. dependOnColumnNamesL []string // Possibly nil when viewExpr is empty. // isInvalid indicates whether this field can be safely used to construct SQL clauses. // Fields are not invalid by default. It can be updated by updateFieldAvailability. isInvalid bool } func decodeField(ft reflect.StructField) (props viewFieldProps, err error) { // parse vexpr tag props.viewExpr = ft.Tag.Get("vexpr") // parse JSON tag { props.jsonNameL = strings.ToLower(ft.Name) tags, _ := structtag.Parse(string(ft.Tag)) if tags != nil { jsonTag, _ := tags.Get("json") if jsonTag != nil && jsonTag.Name != "" { props.jsonNameL = strings.ToLower(jsonTag.Name) } if props.jsonNameL == "-" { props.jsonNameL = "" } } } // parse gorm tag { gormTag := schema.ParseTagSetting(ft.Tag.Get("gorm"), ";") dbName := gormTag["COLUMN"] if dbName == "" { dbName = schema.NamingStrategy{}.ColumnName("", ft.Name) } props.columnNameL = strings.ToLower(dbName) } if props.viewExpr != "" { depFields, err := parseExprDependencies(props.viewExpr) if err != nil { return viewFieldProps{}, err } props.dependOnColumnNamesL = depFields } return } type identVisitor struct { idents []string } func (v *identVisitor) Enter(_ *ast.Node) {} func (v *identVisitor) Exit(node *ast.Node) { if n, ok := (*node).(*ast.IdentifierNode); ok { v.idents = append(v.idents, n.Value) } } // Note: not all SQL statement is supported. // For example, CAST(.. AS SIGNED) ans `COLUMN` is unsupported. func parseExprDependencies(expr string) ([]string, error) { // Parse idents tree, err := parser.Parse(expr) if err != nil { return nil, err } visitor := &identVisitor{} ast.Walk(&tree.Node, visitor) // Normalize and deduplicate idents identMap := map[string]struct{}{} for _, ident := range visitor.idents { identMap[strings.ToLower(ident)] = struct{}{} } idents := make([]string, 0, len(identMap)) for ident := range identMap { idents = append(idents, ident) } sort.Strings(idents) return idents, nil } // updateAvailability updates field's availability according to the `knownColumnNamesL` parameter: // - For calculated fields (vexpr is specified), the field is available when // all of its dependency columns exist in knownColumnNamesL. // - For normal fields (vexpr is not specified), the field is available when // itself exists in knownColumnNamesL. // // If knownColumnNamesL is nil, all fields will be set to available. func (fp *viewFieldProps) updateAvailability(knownColumnNamesL map[string]struct{}) { fp.isInvalid = false if knownColumnNamesL == nil { return } if fp.viewExpr == "" { // This is not a calculated field. if _, ok := knownColumnNamesL[fp.columnNameL]; !ok { fp.isInvalid = true return } } // This is a calculated field. Any missing dependency field result in a invalid status. for _, columnNameL := range fp.dependOnColumnNamesL { if _, ok := knownColumnNamesL[columnNameL]; !ok { fp.isInvalid = true return } } } type viewSchema struct { fields []*viewFieldProps fieldByJSONNameL map[string]*viewFieldProps fieldByColumnNameL map[string]*viewFieldProps } func parseViewModelSchema(model interface{}) (viewSchema, error) { v := reflect.Indirect(reflect.ValueOf(model)) vt := v.Type() fields := make([]*viewFieldProps, 0) for i := 0; i < vt.NumField(); i++ { ft := vt.Field(i) props, err := decodeField(ft) if err != nil { return viewSchema{}, fmt.Errorf("field %s is invalid: %w", ft.Name, err) } if props.jsonNameL == "" { // this field is unexported, skip. continue } fields = append(fields, &props) } vs := viewSchema{ fields: fields, fieldByJSONNameL: map[string]*viewFieldProps{}, fieldByColumnNameL: map[string]*viewFieldProps{}, } for _, field := range fields { vs.fieldByJSONNameL[field.jsonNameL] = field vs.fieldByColumnNameL[field.columnNameL] = field } return vs, nil } func (schema *viewSchema) updateFieldsAvailability(knownColumnNames []string) { if knownColumnNames == nil { for _, field := range schema.fields { field.updateAvailability(nil) } return } columnNamesL := map[string]struct{}{} for _, columnName := range knownColumnNames { columnNamesL[strings.ToLower(columnName)] = struct{}{} } for _, field := range schema.fields { field.updateAvailability(columnNamesL) } } ================================================ FILE: util/gormutil/virtualview/schema_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package virtualview import ( "encoding/json" "reflect" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/testutil" ) func TestParseExprDependencies(t *testing.T) { tests := []struct { want []string args string }{ {[]string{"my_col"}, "my_col"}, {[]string{"col2", "my_col"}, "my_col + 1.0 * col2"}, {[]string{"time"}, "(UNIX_TIMESTAMP(Time) + 0E0)"}, {[]string{"bar", "time"}, "Floor(Foo(Time, 'abc', Def(), Bar))"}, {[]string{"avg_process_time", "exec_count", "sum_cop_task_num"}, "SUM(exec_count * avg_process_time) / SUM(sum_cop_task_num)"}, {[]string{"bar"}, "Def() + Bar"}, {[]string{"col0", "col1"}, "Plus(Col1 * col1) + COL0"}, } for _, tt := range tests { v, err := parseExprDependencies(tt.args) require.NoError(t, err) require.Equal(t, tt.want, v) } failTests := []string{ "CAST(exec_count AS SIGNED)", "Floor(", "Def() + Bar)", "", } for _, tt := range failTests { _, err := parseExprDependencies(tt) require.Error(t, err) } } func TestDecodeFieldAssumptionJSON(t *testing.T) { // For JSON, when it is not specified in the json tag, field name is used: type TestModel struct { QueryValue string JSONUnexported string `json:"-"` JSONSkip string `json:",omitempty"` } val, err := json.Marshal(TestModel{ QueryValue: "a", JSONUnexported: "b", JSONSkip: "c", }) require.NoError(t, err) require.JSONEq(t, `{"QueryValue":"a","JSONSkip":"c"}`, string(val)) } func TestDecodeFieldAssumptionGORM(t *testing.T) { // For GORM, when it is not specified in the tag, default naming strategy will be used: //nolint:govet type TestModel struct { GORMOmit string QueryValue string `gorm:"column:qv"` GORMSkip string `gorm:"primaryKey"` GORMInvalid string `gorm:"foo"` InvalidTag string `abc` } db := testutil.OpenMockDB(t) defer db.MustClose() db.Mocker(). ExpectExec("CREATE TABLE `test_models` (`gorm_omit` longtext,`qv` longtext,`gorm_skip` varchar(191),`gorm_invalid` longtext,`invalid_tag` longtext,PRIMARY KEY (`gorm_skip`))"). WillReturnResult(sqlmock.NewResult(0, 1)) err := db.Gorm().Migrator().CreateTable(TestModel{}) require.NoError(t, err) db.MustMeetMockExpectation() } //nolint:govet type SampleModel struct { Digest string `gorm:"column:MyDigest" json:"digest"` QueryValue string Timestamp float64 `gorm:"column:timestamp" vexpr:"PLUS(a, b)"` JSONUnexported string `json:"-"` JSONSkip string `json:",omitempty"` InvalidTag string `abc` GORMInvalid string `gorm:"foo"` JSONAlias string `vexpr:"SUM(a+1)" json:"foo"` Full string `vexpr:"AVG(Time)" json:"bar" gorm:"column:full_col"` } func TestDecodeField(t *testing.T) { ft := reflect.TypeFor[SampleModel]() f, err := decodeField(ft.Field(0)) require.NoError(t, err) require.Equal(t, "digest", f.jsonNameL) require.Equal(t, "", f.viewExpr) require.Equal(t, "mydigest", f.columnNameL) f, err = decodeField(ft.Field(1)) require.NoError(t, err) require.Equal(t, "queryvalue", f.jsonNameL) require.Equal(t, "", f.viewExpr) require.Equal(t, "query_value", f.columnNameL) f, err = decodeField(ft.Field(2)) require.NoError(t, err) require.Equal(t, "timestamp", f.jsonNameL) require.Equal(t, "PLUS(a, b)", f.viewExpr) require.Equal(t, "timestamp", f.columnNameL) f, err = decodeField(ft.Field(3)) require.NoError(t, err) require.Equal(t, "", f.jsonNameL) require.Equal(t, "", f.viewExpr) require.Equal(t, "json_unexported", f.columnNameL) f, err = decodeField(ft.Field(4)) require.NoError(t, err) require.Equal(t, "jsonskip", f.jsonNameL) require.Equal(t, "", f.viewExpr) require.Equal(t, "json_skip", f.columnNameL) f, err = decodeField(ft.Field(5)) require.NoError(t, err) require.Equal(t, "invalidtag", f.jsonNameL) require.Equal(t, "", f.viewExpr) require.Equal(t, "invalid_tag", f.columnNameL) f, err = decodeField(ft.Field(6)) require.NoError(t, err) require.Equal(t, "gorminvalid", f.jsonNameL) require.Equal(t, "", f.viewExpr) require.Equal(t, "gorm_invalid", f.columnNameL) f, err = decodeField(ft.Field(7)) require.NoError(t, err) require.Equal(t, "foo", f.jsonNameL) require.Equal(t, "SUM(a+1)", f.viewExpr) require.Equal(t, "json_alias", f.columnNameL) f, err = decodeField(ft.Field(8)) require.NoError(t, err) require.Equal(t, "bar", f.jsonNameL) require.Equal(t, "AVG(Time)", f.viewExpr) require.Equal(t, "full_col", f.columnNameL) } func TestParseViewModelSchemaSuccess(t *testing.T) { schema, err := parseViewModelSchema(&SampleModel{}) require.NoError(t, err) require.Equal(t, 8, len(schema.fields)) require.Equal(t, "digest", schema.fields[0].jsonNameL) require.Equal(t, "queryvalue", schema.fields[1].jsonNameL) require.Equal(t, "timestamp", schema.fields[2].jsonNameL) require.Equal(t, "jsonskip", schema.fields[3].jsonNameL) require.Equal(t, "invalidtag", schema.fields[4].jsonNameL) require.Equal(t, "gorminvalid", schema.fields[5].jsonNameL) require.Equal(t, "foo", schema.fields[6].jsonNameL) require.Equal(t, "bar", schema.fields[7].jsonNameL) require.Equal(t, schema.fields[0], schema.fieldByJSONNameL["digest"]) require.Equal(t, schema.fields[1], schema.fieldByJSONNameL["queryvalue"]) require.Equal(t, schema.fields[3], schema.fieldByJSONNameL["jsonskip"]) require.Equal(t, schema.fields[6], schema.fieldByJSONNameL["foo"]) require.Equal(t, schema.fields[7], schema.fieldByJSONNameL["bar"]) require.Equal(t, schema.fields[0], schema.fieldByColumnNameL["mydigest"]) require.Equal(t, schema.fields[1], schema.fieldByColumnNameL["query_value"]) require.Equal(t, schema.fields[4], schema.fieldByColumnNameL["invalid_tag"]) require.Equal(t, schema.fields[6], schema.fieldByColumnNameL["json_alias"]) require.Equal(t, schema.fields[7], schema.fieldByColumnNameL["full_col"]) // Test passing struct directly schema, err = parseViewModelSchema(SampleModel{}) require.NoError(t, err) require.Equal(t, 8, len(schema.fields)) require.Equal(t, "digest", schema.fields[0].jsonNameL) require.Equal(t, "queryvalue", schema.fields[1].jsonNameL) require.Equal(t, "timestamp", schema.fields[2].jsonNameL) require.Equal(t, "jsonskip", schema.fields[3].jsonNameL) require.Equal(t, "invalidtag", schema.fields[4].jsonNameL) require.Equal(t, "gorminvalid", schema.fields[5].jsonNameL) require.Equal(t, "foo", schema.fields[6].jsonNameL) require.Equal(t, "bar", schema.fields[7].jsonNameL) } func TestParseViewModelSchemaFailure(t *testing.T) { type Model struct { Digest string `gorm:"column:MyDigest" json:"digest"` Foo string `vexpr:"invalidExpr(a,"` } _, err := parseViewModelSchema(&Model{}) require.Error(t, err) _, err = parseViewModelSchema(Model{}) require.Error(t, err) } func TestUpdateFieldsAvailability(t *testing.T) { schema, err := parseViewModelSchema(&SampleModel{}) require.NoError(t, err) require.Equal(t, 8, len(schema.fields)) requireFieldsInvalid := func(status ...bool) { require.Equal(t, len(status), len(schema.fields)) for idx, s := range status { require.Equal(t, s, schema.fields[idx].isInvalid, "expect invalid=%v for field[%d], got invalid=%v", s, idx, schema.fields[idx].isInvalid) } } requireFieldsInvalid(false, false, false, false, false, false, false, false) schema.updateFieldsAvailability([]string{}) requireFieldsInvalid(true, true, true, true, true, true, true, true) schema.updateFieldsAvailability(nil) requireFieldsInvalid(false, false, false, false, false, false, false, false) schema.updateFieldsAvailability([]string{"Query_Value"}) requireFieldsInvalid(true, false, true, true, true, true, true, true) schema.updateFieldsAvailability([]string{"c"}) requireFieldsInvalid(true, true, true, true, true, true, true, true) schema.updateFieldsAvailability([]string{"a"}) requireFieldsInvalid(true, true, true, true, true, true, false, true) schema.updateFieldsAvailability([]string{"a", "B"}) requireFieldsInvalid(true, true, false, true, true, true, false, true) schema.updateFieldsAvailability([]string{"query_value", "A", "B"}) requireFieldsInvalid(true, false, false, true, true, true, false, true) schema.updateFieldsAvailability([]string{"queryvalue", "A", "B"}) requireFieldsInvalid(true, true, false, true, true, true, false, true) schema.updateFieldsAvailability([]string{"timestamp"}) requireFieldsInvalid(true, true, true, true, true, true, true, true) schema.updateFieldsAvailability([]string{"query_value", "GORM_INVALID", "A", "B"}) requireFieldsInvalid(true, false, false, true, true, false, false, true) schema.updateFieldsAvailability([]string{"query_value", "digest", "json_skip", "foobar"}) requireFieldsInvalid(true, false, true, false, true, true, true, true) schema.updateFieldsAvailability([]string{"query_value", "digest", "MYDIGEST", "json_skip", "foobar"}) requireFieldsInvalid(false, false, true, false, true, true, true, true) } ================================================ FILE: util/gormutil/virtualview/virtualview.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package virtualview import ( "fmt" "strings" "sync" "gorm.io/gorm/clause" "github.com/pingcap/tidb-dashboard/util/nocopy" ) // VirtualView is a helper to construct SQL clauses for view-like models, based on the view definition in // the `vexpr` tag in the model. // // For a model field like: // // AggLastSeen int `vexpr:"UNIX_TIMESTAMP(MAX(last_seen))"` // // VirtualView can build projections like: // // SELECT UNIX_TIMESTAMP(MAX(last_seen)) AS agg_last_seen .... // ^^^^^^^^^^^^^ This follows the GORM naming strategy and // can be controlled by gorm:"column:xxx". // // Then, when selecting the result of this projection into the same model, fields will be filled out naturally: // // { // AggLastSeen: // } // // If `vexpr` is not specified in the model field, the field can be transparently used. For example: // // FieldFoo int // // The projection will be: // // SELECT field_foo ... // // Callers must specify the fields to be used in clauses. The field can be specified via its JSON name. // // VirtualView is safe to be used concurrently. type VirtualView struct { nocopy.NoCopy fullSchema viewSchema mu sync.Mutex } func New(model interface{}) (*VirtualView, error) { schema, err := parseViewModelSchema(model) if err != nil { return nil, err } return &VirtualView{ fullSchema: schema, }, nil } func MustNew(model interface{}) *VirtualView { vv, err := New(model) if err != nil { panic(err) } return vv } // SetSourceDBColumns restricts SelectClause and OrderByClause to only build clauses over these // specified db columns or fields calculated from these db columns. The restriction will be removed // if nil is given. // This is useful when the source table does not fully match the model, e.g. when there are extra // fields in the model, this can avoid view to be broken. // // This function is concurrent-safe. func (vv *VirtualView) SetSourceDBColumns(dbColumnNames []string) { vv.mu.Lock() vv.fullSchema.updateFieldsAvailability(dbColumnNames) vv.mu.Unlock() } func (vv *VirtualView) Clauses(jsonFieldNames []string) Clauses { m := map[string]struct{}{} for _, name := range jsonFieldNames { m[strings.ToLower(name)] = struct{}{} } return Clauses{ vv: vv, jsonFieldNames: jsonFieldNames, jsonFieldsByNameL: m, } } type Clauses struct { vv *VirtualView jsonFieldNames []string jsonFieldsByNameL map[string]struct{} } // Select builds a Select clause that return all fields specified in Clauses(). // // This function is concurrent-safe. func (vvc Clauses) Select() clause.Expression { vvc.vv.mu.Lock() defer vvc.vv.mu.Unlock() selectColumns := make([]clause.Column, 0, len(vvc.jsonFieldNames)) for _, fieldName := range vvc.jsonFieldNames { fieldNameL := strings.ToLower(fieldName) field := vvc.vv.fullSchema.fieldByJSONNameL[fieldNameL] if field == nil { // The specified JSON field does not exist in the schema, ignoring. continue } if field.isInvalid { // The field has already been filtered out using SetSourceDBColumns continue } if field.viewExpr == "" { // Not a computed field, just use the column name. selectColumns = append(selectColumns, clause.Column{ Name: field.columnNameL, }) } else { // Computed field, build SQL like: // SELECT PLUS(a,b) AS column_name, ... // ^^^^^^^^^^^^^^^^^^^^^^^^ selectColumns = append(selectColumns, clause.Column{ Name: fmt.Sprintf("%s AS %s", field.viewExpr, field.columnNameL), Raw: true, }) // TODO: We'd better quote the alias field name. } } if len(selectColumns) == 0 { // Avoid becoming "SELECT *". selectColumns = append(selectColumns, clause.Column{ Name: "NULL AS __HIDDEN_FIELD__", Raw: true, }) } return clause.Select{ Distinct: false, Columns: selectColumns, } } type OrderByField struct { JSONFieldName string `json:"json_field_name"` IsDesc bool `json:"is_desc"` } // OrderBy builds a Order By clause based on the given fields. Order by fields that do not exist // when calling Clauses() will be ignored. // // This function is concurrent-safe. func (vvc Clauses) OrderBy(fields []OrderByField) clause.Expression { vvc.vv.mu.Lock() defer vvc.vv.mu.Unlock() orderByColumns := make([]clause.OrderByColumn, 0, len(fields)) for _, f := range fields { fieldNameL := strings.ToLower(f.JSONFieldName) if _, ok := vvc.jsonFieldsByNameL[fieldNameL]; !ok { // This field does not exist in the select clause. It should not be allowed to order by. continue } field := vvc.vv.fullSchema.fieldByJSONNameL[fieldNameL] if field == nil { continue } if field.isInvalid { continue } orderByColumns = append(orderByColumns, clause.OrderByColumn{ Column: clause.Column{Name: field.columnNameL}, Desc: f.IsDesc, }) } // If non of the order by field is valid, no ordering will be specified. if len(orderByColumns) == 0 { return nopClause{} } return clause.OrderBy{Columns: orderByColumns} } var ( _ clause.Interface = nopClause{} _ clause.Expression = nopClause{} ) // nopClause is a clause that will be never embedded into the SQL statement. type nopClause struct{} func (c nopClause) Name() string { return "__DUMMY_CLAUSE__" } func (c nopClause) Build(_ clause.Builder) {} func (c nopClause) MergeClause(_ *clause.Clause) {} ================================================ FILE: util/gormutil/virtualview/virtualview_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package virtualview import ( "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/testutil" ) func TestVirtualViewSelect(t *testing.T) { vv := MustNew(SampleModel{}) db := testutil.OpenMockDB(t) defer db.MustClose() var results []SampleModel // No field db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err := db.Gorm(). Clauses(vv.Clauses([]string{}).Select()).Find(&results).Error require.NoError(t, err) // A field with GORM specified column name db.Mocker(). ExpectQuery("SELECT `mydigest` FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"digest"}).Select()).Find(&results).Error require.NoError(t, err) // A field with alternative JSON letter case db.Mocker(). ExpectQuery("SELECT `mydigest` FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"DIgest"}).Select()).Find(&results).Error require.NoError(t, err) // A field without GORM specified column name db.Mocker(). ExpectQuery("SELECT `query_value` FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"QueryValue"}).Select()).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT `query_value` FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"queryVALUE"}).Select()).Find(&results).Error require.NoError(t, err) // Multiple fields db.Mocker(). ExpectQuery("SELECT `query_value`,`mydigest` FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"QueryValue", "digest"}).Select()).Find(&results).Error require.NoError(t, err) // A field with json alias db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"foo"}).Select()).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"Foo"}).Select()).Find(&results).Error require.NoError(t, err) // A field that is not exported db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"jsonunexported"}).Select()).Find(&results).Error require.NoError(t, err) // A field with the original JSON name of the field (which should be failed). db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"JSONAlias"}).Select()).Find(&results).Error require.NoError(t, err) // There is an unknown field db.Mocker(). ExpectQuery("SELECT `mydigest` FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"query_value", "DIGEST"}).Select()).Find(&results).Error require.NoError(t, err) // All fields are unknown db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"query_value", "xyz"}).Select()).Find(&results).Error require.NoError(t, err) // A field with vexpr db.Mocker(). ExpectQuery("SELECT PLUS(a, b) AS timestamp FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"timestamp"}).Select()).Find(&results).Error require.NoError(t, err) // Complex field selection allFieldNames := []string{"invalidtag", "jsonalias", "foo", "gorminvalid", "queryvalue", "full", "timestamp", "bar", "digest", "jsonunexported", "jsonskip"} db.Mocker(). ExpectQuery("SELECT `invalid_tag`,SUM(a+1) AS json_alias,`gorm_invalid`,`query_value`,PLUS(a, b) AS timestamp,AVG(Time) AS full_col,`mydigest`,`json_skip` FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses(allFieldNames).Select()).Find(&results).Error require.NoError(t, err) // Update Source Columns. // All columns are not a used column. vv.SetSourceDBColumns([]string{"abc", "Timestamp", "digest"}) db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses(allFieldNames).Select()).Find(&results).Error require.NoError(t, err) // A computed field is partially matched. vv.SetSourceDBColumns([]string{"a"}) db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"TimeStamp"}).Select()).Find(&results).Error require.NoError(t, err) // A computed field is fully matched. vv.SetSourceDBColumns([]string{"a", "b"}) db.Mocker(). ExpectQuery("SELECT PLUS(a, b) AS timestamp FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"TimeStamp"}).Select()).Find(&results).Error require.NoError(t, err) // A computed field itself is given as the source column -- should be treated as missing. vv.SetSourceDBColumns([]string{"timestamp"}) db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"TimeStamp"}).Select()).Find(&results).Error require.NoError(t, err) // When a computed field itself is given, and all of its base columns are also given, we should // always build a computed expression. vv.SetSourceDBColumns([]string{"timestamp", "a", "b"}) db.Mocker(). ExpectQuery("SELECT PLUS(a, b) AS timestamp FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"TIMESTAMP"}).Select()).Find(&results).Error require.NoError(t, err) // Selecting base columns of a computed expression should not match any columns. vv.SetSourceDBColumns([]string{"timestamp", "a", "b"}) db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"a", "b"}).Select()).Find(&results).Error require.NoError(t, err) // A non-computed field is given, using its GORM column name. vv.SetSourceDBColumns([]string{"mydigest"}) db.Mocker(). ExpectQuery("SELECT `mydigest` FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"digest"}).Select()).Find(&results).Error require.NoError(t, err) // If a GORM column name is specified but is not used, then it should be regarded as missing. vv.SetSourceDBColumns([]string{"digest"}) db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"digest"}).Select()).Find(&results).Error require.NoError(t, err) // When a field contains both GORM column name and vexpr, column name should not be not a filter. vv.SetSourceDBColumns([]string{"time"}) db.Mocker(). ExpectQuery("SELECT AVG(Time) AS full_col FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"bar"}).Select()).Find(&results).Error require.NoError(t, err) vv.SetSourceDBColumns([]string{"full_col"}) db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"bar"}).Select()).Find(&results).Error require.NoError(t, err) // A base column is used in multiple computed columns. vv.SetSourceDBColumns([]string{"a", "b"}) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias,PLUS(a, b) AS timestamp FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"foo", "timestamp", "digest"}).Select()).Find(&results).Error require.NoError(t, err) // Remove Source column filter. vv.SetSourceDBColumns(nil) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias,PLUS(a, b) AS timestamp,`mydigest` FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses(vv.Clauses([]string{"foo", "timestamp", "digest"}).Select()).Find(&results).Error require.NoError(t, err) db.MustMeetMockExpectation() } func TestVirtualViewOrderBy(t *testing.T) { vv := MustNew(SampleModel{}) db := testutil.OpenMockDB(t) defer db.MustClose() var results []SampleModel // No field in Clauses(). c := vv.Clauses([]string{}) db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err := db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"foo", false}}), ).Find(&results).Error require.NoError(t, err) // Field is specified in Clauses(), but no field in OrderBy(). c = vv.Clauses([]string{"foo"}) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{}), ).Find(&results).Error require.NoError(t, err) // A field not in Clauses() will be ignored. c = vv.Clauses([]string{"digest"}) db.Mocker(). ExpectQuery("SELECT `mydigest` FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"foo", false}}), ). Find(&results).Error require.NoError(t, err) // A field does not really exist will be ignored. c = vv.Clauses([]string{"xyz"}) db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"xyz", false}}), ). Find(&results).Error require.NoError(t, err) // A field with GORM specified column name c = vv.Clauses([]string{"digest"}) db.Mocker(). ExpectQuery("SELECT `mydigest` FROM `sample_models` ORDER BY `mydigest` DESC"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"DIGEST", true}}), ).Find(&results).Error require.NoError(t, err) // A field without GORM specified column name c = vv.Clauses([]string{"QueryValue"}) db.Mocker(). ExpectQuery("SELECT `query_value` FROM `sample_models` ORDER BY `query_value`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"queryValue", false}}), ).Find(&results).Error require.NoError(t, err) c = vv.Clauses([]string{"queryVALUE"}) db.Mocker(). ExpectQuery("SELECT `query_value` FROM `sample_models` ORDER BY `query_value`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"QUERYVALUE", false}}), ).Find(&results).Error require.NoError(t, err) // Multiple fields in clauses, use some of it c = vv.Clauses([]string{"queryvalue", "digest"}) db.Mocker(). ExpectQuery("SELECT `query_value`,`mydigest` FROM `sample_models` ORDER BY `query_value`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"queryValue", false}}), ).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT `query_value`,`mydigest` FROM `sample_models` ORDER BY `mydigest`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"digest", false}}), ).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT `query_value`,`mydigest` FROM `sample_models` ORDER BY `mydigest` DESC,`query_value`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{ {"digest", true}, {"queryValue", false}, }), ).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT `query_value`,`mydigest` FROM `sample_models` ORDER BY `mydigest` DESC,`query_value`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{ {"digest", true}, {"timestamp", false}, // This field is not in the Clauses() {"queryValue", false}, }), ).Find(&results).Error require.NoError(t, err) // A field with json alias c = vv.Clauses([]string{"foo"}) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias FROM `sample_models` ORDER BY `json_alias`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"foo", false}}), ).Find(&results).Error require.NoError(t, err) c = vv.Clauses([]string{"FOO"}) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias FROM `sample_models` ORDER BY `json_alias`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"Foo", false}}), ).Find(&results).Error require.NoError(t, err) // A field with the original JSON name of the field should be ignored c = vv.Clauses([]string{"foo"}) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"json_alias", false}}), ).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"jsonalias", false}}), ).Find(&results).Error require.NoError(t, err) c = vv.Clauses([]string{"JSONAlias"}) db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"jsonalias", false}}), ).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT NULL AS __HIDDEN_FIELD__ FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"foo", false}}), ).Find(&results).Error require.NoError(t, err) // Update Source Columns. // Fully match some computed fields. vv.SetSourceDBColumns([]string{"a", "b"}) c = vv.Clauses([]string{"foo", "timestamp", "digest"}) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias,PLUS(a, b) AS timestamp FROM `sample_models` ORDER BY `json_alias`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"foo", false}}), ).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias,PLUS(a, b) AS timestamp FROM `sample_models` ORDER BY `timestamp`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"timestamp", false}}), ).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias,PLUS(a, b) AS timestamp FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"digest", false}}), // ignored ).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias,PLUS(a, b) AS timestamp FROM `sample_models` ORDER BY `json_alias`,`timestamp`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{ {"foo", false}, {"digest", false}, // ignored {"timestamp", false}, }), ).Find(&results).Error require.NoError(t, err) // Partially match vv.SetSourceDBColumns([]string{"a"}) c = vv.Clauses([]string{"foo", "timestamp", "digest"}) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias FROM `sample_models` ORDER BY `json_alias`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"foo", false}}), ).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"timestamp", false}}), // ignored ).Find(&results).Error require.NoError(t, err) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias FROM `sample_models`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{{"digest", false}}), // ignored ).Find(&results).Error require.NoError(t, err) // Match a normal field. vv.SetSourceDBColumns([]string{"a", "b", "mydigest"}) c = vv.Clauses([]string{"foo", "timestamp", "digest"}) db.Mocker(). ExpectQuery("SELECT SUM(a+1) AS json_alias,PLUS(a, b) AS timestamp,`mydigest` FROM `sample_models` ORDER BY `json_alias`,`mydigest`,`timestamp`"). WillReturnRows(sqlmock.NewRows([]string{"some_column"})) err = db.Gorm(). Clauses( c.Select(), c.OrderBy([]OrderByField{ {"foo", false}, {"digest", false}, {"timestamp", false}, }), ).Find(&results).Error require.NoError(t, err) db.MustMeetMockExpectation() } ================================================ FILE: util/israce/no_race.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. //go:build !race // Package israce reports if the Go race detector is enabled. package israce // Enabled reports if the race detector is enabled. const Enabled = false ================================================ FILE: util/israce/race.go ================================================ // Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0. //go:build race // Package israce reports if the Go race detector is enabled. package israce // Enabled reports if the race detector is enabled. const Enabled = true ================================================ FILE: util/jsonserde/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package jsonserde import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/jsonserde/convert.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Copyright (c) 2017 Eason Lin package jsonserde import ( "unicode" ) func swagToSnakeCase(in string) string { // Copied from https://github.com/swaggo/swag/blob/8ffc6c29c01a13fb01183ee91d0fcc5fc586b431/field_parser.go#L81 // so that the serialized value can be aligned with the swagger spec. runes := []rune(in) length := len(runes) out := make([]rune, 0, length) for i := range length { if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < length && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) { out = append(out, '_') } out = append(out, unicode.ToLower(runes[i])) } return string(out) } ================================================ FILE: util/jsonserde/default.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package jsonserde sets default config for json-iterator. // // If this package is imported, json-iterator will: // - encode time as millisecond timestamps // - encode field names as lower_snake_case package jsonserde import ( "time" jsoniter "github.com/json-iterator/go" "github.com/json-iterator/go/extra" ) func init() { extra.RegisterTimeAsInt64Codec(time.Millisecond) extra.SetNamingStrategy(swagToSnakeCase) } var Default = jsoniter.ConfigCompatibleWithStandardLibrary ================================================ FILE: util/jsonserde/default_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package jsonserde import ( "testing" "time" "github.com/stretchr/testify/require" ) type testStruct struct { FooBar string SQLTime time.Time } func TestMarshal(t *testing.T) { data := testStruct{ FooBar: "zoo", SQLTime: time.Unix(0, 1641733771580123000), } val, err := Default.Marshal(data) require.NoError(t, err) require.Equal(t, `{"foo_bar":"zoo","sql_time":1641733771580}`, string(val)) } func TestUnmarshal(t *testing.T) { var data testStruct err := Default.Unmarshal([]byte(`{"foo_bar":"zoo","sql_time":1641733771580}`), &data) require.NoError(t, err) require.Equal(t, "zoo", data.FooBar) require.Equal(t, time.Unix(0, 1641733771580000000), data.SQLTime) } ================================================ FILE: util/jsonserde/ginadapter/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package ginadapter import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/jsonserde/ginadapter/binding.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Copyright 2014 Manu Martinez-Almeida. package ginadapter import ( "bytes" "fmt" "io" "net/http" "github.com/gin-gonic/gin/binding" "github.com/pingcap/tidb-dashboard/util/jsonserde" ) var Binding = jsonBinding{} type jsonBinding struct{} func (jsonBinding) Name() string { return "json" } func (jsonBinding) Bind(req *http.Request, obj interface{}) error { if req == nil || req.Body == nil { return fmt.Errorf("invalid request") } return decodeJSON(req.Body, obj) } func (jsonBinding) BindBody(body []byte, obj interface{}) error { return decodeJSON(bytes.NewReader(body), obj) } func decodeJSON(r io.Reader, obj interface{}) error { decoder := jsonserde.Default.NewDecoder(r) if err := decoder.Decode(obj); err != nil { return err } return validate(obj) } func validate(obj interface{}) error { if binding.Validator == nil { return nil } return binding.Validator.ValidateStruct(obj) } ================================================ FILE: util/jsonserde/ginadapter/binding_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package ginadapter import ( "testing" "github.com/stretchr/testify/require" ) func TestJSONBindingBindBody(t *testing.T) { type sampleStruct struct { ABCFoo string Bar string Box string `json:"_box"` } var s sampleStruct err := jsonBinding{}.BindBody([]byte(`{"Abc_foo": "FOO", "Box": "zzz", "Bar": "xyz"}`), &s) require.NoError(t, err) require.Equal(t, "FOO", s.ABCFoo) require.Equal(t, "xyz", s.Bar) require.Equal(t, "", s.Box) s = sampleStruct{} err = jsonBinding{}.BindBody([]byte(`{"ABCFoo": "z", "_Box": "yo"}`), &s) require.NoError(t, err) require.Equal(t, "", s.ABCFoo) require.Equal(t, "", s.Bar) require.Equal(t, "yo", s.Box) s = sampleStruct{} err = jsonBinding{}.BindBody([]byte(`{"abc_foo": "x", "_box": "yoo", "bar": "jojo"}`), &s) require.NoError(t, err) require.Equal(t, "x", s.ABCFoo) require.Equal(t, "jojo", s.Bar) require.Equal(t, "yoo", s.Box) } func TestJSONBindingBindBodyMap(t *testing.T) { s := make(map[string]string) err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO","Hello":"world"}`), &s) require.NoError(t, err) require.Len(t, s, 2) require.Equal(t, "FOO", s["foo"]) require.Equal(t, "world", s["Hello"]) } ================================================ FILE: util/jsonserde/ginadapter/render.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Copyright 2014 Manu Martinez-Almeida. package ginadapter import ( "net/http" "github.com/pingcap/tidb-dashboard/util/jsonserde" ) var jsonContentType = []string{"application/json; charset=utf-8"} func writeContentType(w http.ResponseWriter, value []string) { header := w.Header() if val := header["Content-Type"]; len(val) == 0 { header["Content-Type"] = value } } type Renderer struct { Data interface{} } func (j Renderer) Render(w http.ResponseWriter) error { writeContentType(w, jsonContentType) jsonBytes, err := jsonserde.Default.Marshal(j.Data) if err != nil { return err } _, err = w.Write(jsonBytes) return err } func (j Renderer) WriteContentType(w http.ResponseWriter) { writeContentType(w, jsonContentType) } ================================================ FILE: util/jsonserde/ginadapter/render_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package ginadapter import ( "net/http/httptest" "testing" "github.com/stretchr/testify/require" ) func TestRenderer(t *testing.T) { w := httptest.NewRecorder() type User struct { FullName string Age int } data := User{ FullName: "zoo", Age: 18, } err := (Renderer{data}).Render(w) require.NoError(t, err) require.Equal(t, `{"full_name":"zoo","age":18}`, w.Body.String()) require.Equal(t, `application/json; charset=utf-8`, w.Header().Get("Content-Type")) } func TestRendererError(t *testing.T) { w := httptest.NewRecorder() data := make(chan int) err := (Renderer{data}).Render(w) require.EqualError(t, err, "chan int is unsupported type") } ================================================ FILE: util/lifecyclectx/fx.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package lifecyclectx import ( "context" "sync/atomic" "go.uber.org/fx" ) // Provider is an easy way to access a lifecycle context on-demand. type Provider interface { GetLifecycleCtx() context.Context } // FxCtx provide the lifecycle from Fx App Lifecycle. type FxCtx struct { ctx atomic.Value } func NewFxCtx(lc fx.Lifecycle) Provider { p := &FxCtx{ ctx: atomic.Value{}, } lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { if ctx != nil { p.ctx.Store(ctx) } return nil }, }) return p } func (p FxCtx) GetLifecycleCtx() context.Context { ctx := p.ctx.Load() if ctx == nil { panic("lifecyclectx.FxCtx is not registered or not initialized yet") } return ctx.(context.Context) } ================================================ FILE: util/lifecyclectx/lifecyclectxtest/test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package lifecyclectxtest import ( "context" "github.com/pingcap/tidb-dashboard/util/lifecyclectx" ) type TestCtx struct { ctx context.Context } func BgCtx() lifecyclectx.Provider { return TestCtx{ ctx: context.Background(), } } func (p TestCtx) GetLifecycleCtx() context.Context { return p.ctx } ================================================ FILE: util/netutil/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package netutil import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/netutil/parse.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package netutil import ( "net" "net/url" "strconv" "github.com/joomcode/errorx" ) var ( ErrNS = errorx.NewNamespace("net_util") ErrInvalidAddress = ErrNS.NewType("invalid_address") ) // ParseHostAndPortFromAddress parse an address in format `host:port` like `127.0.0.1:2379`. // Returns error if parse failed. func ParseHostAndPortFromAddress(address string) (string, uint, error) { host, port, err := net.SplitHostPort(address) if err != nil { return "", 0, ErrInvalidAddress.New("Invalid address `%s`, expect format `host:port`", address) } portNumeric, err := strconv.Atoi(port) if err != nil || portNumeric == 0 { return "", 0, ErrInvalidAddress.New("Invalid address `%s`, expect port to be numeric", address) } return host, uint(portNumeric), nil } // ParseHostAndPortFromAddressURL parse an address in format `protocol://ip:port/...` like `http://127.0.0.1:2379`. // Returns error if parse failed. func ParseHostAndPortFromAddressURL(urlString string) (string, uint, error) { u, err := url.Parse(urlString) if err != nil { return "", 0, ErrInvalidAddress.New("Invalid address `%s`, expect format `http(s)://host:port/...`", urlString) } host, port, err := ParseHostAndPortFromAddress(u.Host) if err != nil { return "", 0, err } return host, port, nil } ================================================ FILE: util/netutil/parse_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package netutil import ( "testing" "github.com/stretchr/testify/require" ) func TestParseHostAndPortFromAddress(t *testing.T) { host, port, err := ParseHostAndPortFromAddress("abc.com:123") require.NoError(t, err) require.Equal(t, "abc.com", host) require.Equal(t, uint(123), port) host, port, err = ParseHostAndPortFromAddress("192.168.31.1:1234") require.NoError(t, err) require.Equal(t, "192.168.31.1", host) require.Equal(t, uint(1234), port) host, port, err = ParseHostAndPortFromAddress("[::ffff:1.2.3.4]:10023") require.NoError(t, err) require.Equal(t, "::ffff:1.2.3.4", host) require.Equal(t, uint(10023), port) host, port, err = ParseHostAndPortFromAddress("[2001:0db8::1428:57ab]:80") require.NoError(t, err) require.Equal(t, "2001:0db8::1428:57ab", host) require.Equal(t, uint(80), port) _, _, err = ParseHostAndPortFromAddress("http://abc.com:123") require.Error(t, err) _, _, err = ParseHostAndPortFromAddress("abc.com") require.Error(t, err) _, _, err = ParseHostAndPortFromAddress("localhost") require.Error(t, err) _, _, err = ParseHostAndPortFromAddress("abc.com:def") require.Error(t, err) _, _, err = ParseHostAndPortFromAddress("::ffff:1.2.3.4:10023") require.Error(t, err) _, _, err = ParseHostAndPortFromAddress("2001:0db8::1428:57ab:80") require.Error(t, err) } func TestParseHostAndPortFromAddressURL(t *testing.T) { host, port, err := ParseHostAndPortFromAddressURL("http://abc.com:123") require.NoError(t, err) require.Equal(t, "abc.com", host) require.Equal(t, uint(123), port) host, port, err = ParseHostAndPortFromAddressURL("https://192.168.31.1:1234") require.NoError(t, err) require.Equal(t, "192.168.31.1", host) require.Equal(t, uint(1234), port) host, port, err = ParseHostAndPortFromAddressURL("abc://[::ffff:1.2.3.4]:10023") require.NoError(t, err) require.Equal(t, "::ffff:1.2.3.4", host) require.Equal(t, uint(10023), port) host, port, err = ParseHostAndPortFromAddressURL("foo://[2001:0db8::1428:57ab]:80") require.NoError(t, err) require.Equal(t, "2001:0db8::1428:57ab", host) require.Equal(t, uint(80), port) _, _, err = ParseHostAndPortFromAddressURL("http://abc.com") require.Error(t, err) _, _, err = ParseHostAndPortFromAddressURL("abc.com:345") require.Error(t, err) _, _, err = ParseHostAndPortFromAddressURL("http://localhost") require.Error(t, err) _, _, err = ParseHostAndPortFromAddressURL("http://abc.com:def") require.Error(t, err) _, _, err = ParseHostAndPortFromAddressURL("http://::ffff:1.2.3.4:10023") require.Error(t, err) _, _, err = ParseHostAndPortFromAddressURL("http://2001:0db8::1428:57ab:80") require.Error(t, err) } ================================================ FILE: util/nocopy/nocopy.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package nocopy // NoCopy may be embedded into structs which must not be copied // after the first use. // // See https://github.com/golang/go/issues/8005 type NoCopy struct{} // Lock is a no-op used by -copylocks checker from `go vet`. func (*NoCopy) Lock() {} func (*NoCopy) UnLock() {} ================================================ FILE: util/proxy/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package proxy import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/proxy/proxy.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. // Package proxy provides a TCP reverse proxy. Unlike normal reverse proxy, the upstream is intentionally fixed. // A new upstream will be selected if the current upstream is down. package proxy import ( "context" "io" "net" "sync" "time" "github.com/pingcap/log" "go.uber.org/atomic" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/nocopy" ) type Proxy struct { nocopy.NoCopy config Config listener net.Listener ctx context.Context ctxCancel context.CancelFunc upstreams sync.Map // The key is the address in string type. The value is in type *upstream. noActiveUpstream *atomic.Bool currentUpstreamAddr *atomic.String } type Config struct { KindTag string UpstreamProbeInterval time.Duration DialTimeout time.Duration } const ( DefaultUpstreamProbeInterval = 2 * time.Second DefaultDialTimeout = 3 * time.Second ) func New(config Config) (*Proxy, error) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, err } ctx, cancel := context.WithCancel(context.Background()) if config.UpstreamProbeInterval <= 0 { config.UpstreamProbeInterval = DefaultUpstreamProbeInterval } if config.DialTimeout <= 0 { config.DialTimeout = DefaultDialTimeout } p := &Proxy{ config: config, listener: l, ctx: ctx, ctxCancel: cancel, upstreams: sync.Map{}, noActiveUpstream: atomic.NewBool(true), currentUpstreamAddr: atomic.NewString(""), } go p.runListenerLoop() go p.runProbeLoop() return p, nil } // HasActiveUpstream returns whether there is an active upstream. func (p *Proxy) HasActiveUpstream() bool { return !p.noActiveUpstream.Load() } // Close stops any running loops and stops the listener. // This function is concurrent-safe. func (p *Proxy) Close() { p.ctxCancel() _ = p.listener.Close() } // Port returns the actual listening port. func (p *Proxy) Port() int { return p.listener.Addr().(*net.TCPAddr).Port } // SetUpstreams sets the upstream address list. // This function is concurrent-safe. func (p *Proxy) SetUpstreams(addresses []string) { if len(addresses) == 0 { log.Debug("All endpoints are removed from the upstream list", zap.String("kindTag", p.config.KindTag)) p.upstreams.Range(func(key, _ interface{}) bool { p.upstreams.Delete(key) return true }) p.currentUpstreamAddr.Store("") p.noActiveUpstream.Store(true) return } // Add new upstreams for _, addr := range addresses { if _, ok := p.upstreams.Load(addr); !ok { log.Debug("An endpoint is added to the upstream list", zap.String("kindTag", p.config.KindTag), zap.String("addr", addr)) p.upstreams.Store(addr, newUpstream(p.config.KindTag, addr)) } } // Remove old upstreams addrSet := make(map[string]struct{}) for _, addr := range addresses { addrSet[addr] = struct{}{} } p.upstreams.Range(func(key, _ interface{}) bool { addr := key.(string) if _, ok := addrSet[addr]; !ok { log.Debug("An endpoint is removed from the upstream list", zap.String("kindTag", p.config.KindTag), zap.String("addr", addr)) p.upstreams.Delete(key) } return true }) } func (p *Proxy) serveConnection(in net.Conn) { out := p.pickActiveUpstreamAndConnect() if out == nil { _ = in.Close() return } // bidirectional copy go func() { _, _ = io.Copy(in, out) _ = in.Close() _ = out.Close() }() _, _ = io.Copy(out, in) _ = out.Close() _ = in.Close() } func (p *Proxy) pickActiveUpstreamAndConnect() net.Conn { for { picked := p.pickOneLastActiveUpstream() if picked == nil { // There is no active upstream for now. // We stop and wait the prober to discover an active upstreams later. { currentAddr := p.currentUpstreamAddr.Load() if currentAddr != "" { log.Warn("No upstream is active", zap.String("kindTag", p.config.KindTag), zap.String("lastUpstreamAddr", currentAddr)) } } p.currentUpstreamAddr.Store("") p.noActiveUpstream.Store(true) return nil } conn, err := picked.Connect(p.config.DialTimeout) if err == nil { // Connect is successful, memorize this upstream for future connection. { currentAddr := p.currentUpstreamAddr.Load() if currentAddr != picked.addr { log.Info("Using new upstream", zap.String("kindTag", p.config.KindTag), zap.String("lastUpstreamAddr", currentAddr), zap.String("newUpstreamAddr", picked.addr)) } } p.currentUpstreamAddr.Store(picked.addr) p.noActiveUpstream.Store(false) return conn } // We know that this upstream doesn't look good. Update its status. p.currentUpstreamAddr.Store("") } } // pickOneLastActiveUpstream returns an upstream which is last known as active. // If there is no active upstream, nil will be returned. func (p *Proxy) pickOneLastActiveUpstream() *upstream { currentUpstream := p.currentUpstreamAddr.Load() if currentUpstream != "" { // It is possible that the upstream list has been changed. r, ok := p.upstreams.Load(currentUpstream) if ok { picked := r.(*upstream) if picked.IsActive() { return picked } } } var picked *upstream p.upstreams.Range(func(_, value interface{}) bool { r := value.(*upstream) if r.IsActive() { picked = r return false } return true }) return picked } // probeActiveUpstreams iterates all inactive upstreams and try to update its status to active. func (p *Proxy) probeActiveUpstreams() { activeUpstreams := 0 p.upstreams.Range(func(_, value interface{}) bool { r := value.(*upstream) if r.IsActive() { activeUpstreams++ } else { r.TryProbeAsync(p.config.DialTimeout) } return true }) if activeUpstreams > 0 { // As long as there is any upstream recognized as active, there is active upstream. p.noActiveUpstream.Store(false) } } func (p *Proxy) runProbeLoop() { for { select { case <-p.ctx.Done(): return case <-time.After(p.config.UpstreamProbeInterval): p.probeActiveUpstreams() } } } func (p *Proxy) runListenerLoop() { for { select { case <-p.ctx.Done(): return default: conn, err := p.listener.Accept() if err != nil { // Ignore listener close error // TODO: Use `if !errors.Is(err, net.ErrClosed)` for higher Golang compilers. log.Warn("Accept incoming connection failed", zap.String("remoteAddr", p.listener.Addr().String()), zap.Error(err)) } else { go p.serveConnection(conn) } } } } ================================================ FILE: util/proxy/proxy_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package proxy import ( "fmt" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/go-resty/resty/v2" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/testutil" ) var configForTest = Config{ UpstreamProbeInterval: time.Millisecond * 200, } const probeWait = time.Millisecond * 500 // UpstreamProbeInterval*2.5 func sendGetToProxy(proxy *Proxy) (*resty.Response, error) { url := fmt.Sprintf("http://127.0.0.1:%d", proxy.Port()) return resty.New().SetTimeout(time.Millisecond * 500).R().Get(url) } func TestNoUpstream(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) } func TestAddUpstream(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() server := testutil.NewHTTPServer("foo") defer server.Close() p.SetUpstreams([]string{testutil.GetHTTPServerHost(server)}) require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) // Incoming connection will not be established until a probe interval. require.Error(t, err) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) } func TestAddMultipleUpstream(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() servers := testutil.NewMultiServer(5, "foo#%d") defer servers.CloseAll() require.False(t, p.HasActiveUpstream()) p.SetUpstreams(servers.GetEndpoints()) _, err = sendGetToProxy(p) require.Error(t, err) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Contains(t, resp.String(), "foo#") require.Equal(t, servers.LastResp(), resp.String()) } func TestRemoveAllUpstreams(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() server := testutil.NewHTTPServer("foo") defer server.Close() p.SetUpstreams([]string{testutil.GetHTTPServerHost(server)}) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) p.SetUpstreams([]string{}) require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) } func TestRemoveOneUpstream(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() server1 := testutil.NewHTTPServer("foo") defer server1.Close() server2 := testutil.NewHTTPServer("bar") defer server2.Close() p.SetUpstreams([]string{testutil.GetHTTPServerHost(server1)}) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) p.SetUpstreams([]string{testutil.GetHTTPServerHost(server1), testutil.GetHTTPServerHost(server2)}) require.True(t, p.HasActiveUpstream()) time.Sleep(probeWait) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) require.True(t, p.HasActiveUpstream()) // The active upstream is removed, another upstream should be used. p.SetUpstreams([]string{testutil.GetHTTPServerHost(server2)}) require.True(t, p.HasActiveUpstream()) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "bar", resp.String()) // Add upstream back p.SetUpstreams([]string{testutil.GetHTTPServerHost(server1), testutil.GetHTTPServerHost(server2)}) require.True(t, p.HasActiveUpstream()) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "bar", resp.String()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "bar", resp.String()) } func TestPickLastActiveUpstream(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() server1 := testutil.NewHTTPServer("foo") defer server1.Close() server2 := testutil.NewHTTPServer("bar") defer server2.Close() p.SetUpstreams([]string{testutil.GetHTTPServerHost(server1)}) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) // Even if SetUpstreams is called, the active upstream should be unchanged. p.SetUpstreams([]string{testutil.GetHTTPServerHost(server1), testutil.GetHTTPServerHost(server2)}) require.True(t, p.HasActiveUpstream()) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) time.Sleep(probeWait) for range 5 { // Let's try multiple times! We should always get "foo". require.True(t, p.HasActiveUpstream()) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) } } func TestAllUpstreamDown(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() servers := testutil.NewMultiServer(3, "foo#%d") defer servers.CloseAll() p.SetUpstreams(servers.GetEndpoints()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Contains(t, resp.String(), "foo#") require.Equal(t, servers.LastResp(), resp.String()) servers.CloseAll() require.True(t, p.HasActiveUpstream()) time.Sleep(probeWait) // Since we only set inactive when new connection is established (lazily), HasActiveUpstream is still true here. require.True(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) require.False(t, p.HasActiveUpstream()) } func TestActiveUpstreamDown(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() servers := testutil.NewMultiServer(5, "foo#%d") defer servers.CloseAll() p.SetUpstreams(servers.GetEndpoints()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Contains(t, resp.String(), "foo#") require.Equal(t, servers.LastResp(), resp.String()) require.Equal(t, fmt.Sprintf("foo#%d", servers.LastID()), resp.String()) // Close the last accessed server servers.Servers[servers.LastID()].Close() // The connection is still succeeded, but forwarded to another upstream. resp2, err := sendGetToProxy(p) require.NoError(t, err) require.Contains(t, resp2.String(), "foo#") require.Equal(t, servers.LastResp(), resp2.String()) require.NotEqual(t, resp.String(), resp2.String()) // Check upstream has changed time.Sleep(probeWait) resp3, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, resp3.String(), resp2.String()) // Unchanged } func TestNonActiveUpstreamDown(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() servers := testutil.NewMultiServer(5, "foo#%d") defer servers.CloseAll() p.SetUpstreams(servers.GetEndpoints()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, fmt.Sprintf("foo#%d", servers.LastID()), resp.String()) // Close other non active servers for i := range 5 { if i != servers.LastID() { servers.Servers[i].Close() } } resp2, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, resp.String(), resp2.String()) // Unchanged time.Sleep(probeWait) resp3, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, resp.String(), resp3.String()) // Unchanged } func TestBrokenServer(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprintln(w, "foo") })) defer server.Close() p.SetUpstreams([]string{testutil.GetHTTPServerHost(server)}) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) require.True(t, os.IsTimeout(err)) require.True(t, p.HasActiveUpstream()) // In this case, proxy will not switch the upstream by design. Let's check it is still // connecting the original "broken" upstream. server2 := testutil.NewHTTPServer("foo") defer server2.Close() p.SetUpstreams([]string{testutil.GetHTTPServerHost(server), testutil.GetHTTPServerHost(server2)}) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) require.True(t, os.IsTimeout(err)) // Let's remove the first upstream! We should get success response immediately without waiting probe. p.SetUpstreams([]string{testutil.GetHTTPServerHost(server2)}) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) } func TestUpstreamBack(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() server := testutil.NewHTTPServer("foo") defer server.Close() host := testutil.GetHTTPServerHost(server) p.SetUpstreams([]string{host}) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) // Close the upstream server server.Close() _, err = sendGetToProxy(p) require.Error(t, err) require.False(t, p.HasActiveUpstream()) // Start the upstream server again at the original listen address server2 := testutil.NewHTTPServerAtHost("bar", host) defer server2.Close() // We will still get failure here, even if the upstream is back. It will recover at next probe round. require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "bar", resp.String()) } func TestUpstreamSwitchComplex(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() server := testutil.NewHTTPServer("foo") defer server.Close() p.SetUpstreams([]string{testutil.GetHTTPServerHost(server)}) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) server2 := testutil.NewHTTPServer("bar") defer server2.Close() // Let's close the current upstream server.Close() require.True(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) require.False(t, p.HasActiveUpstream()) // Wait one round probe, nothing is changed time.Sleep(probeWait) require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) // Add a new alive upstream p.SetUpstreams([]string{testutil.GetHTTPServerHost(server), testutil.GetHTTPServerHost(server2)}) require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "bar", resp.String()) // Bring down the new upstream again! server2.Close() require.True(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) server3 := testutil.NewHTTPServer("box") defer server3.Close() host3 := testutil.GetHTTPServerHost(server3) // Add a new alive upstream p.SetUpstreams([]string{testutil.GetHTTPServerHost(server), testutil.GetHTTPServerHost(server2), host3}) require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "box", resp.String()) server3.Close() server4 := testutil.NewHTTPServer("car") host4 := testutil.GetHTTPServerHost(server4) server4.Close() // Add a bad upstream p.SetUpstreams([]string{testutil.GetHTTPServerHost(server), testutil.GetHTTPServerHost(server2), host3, host4}) require.True(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) // Bring back server3 server3New := testutil.NewHTTPServerAtHost("newBox", host3) defer server3New.Close() require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "newBox", resp.String()) // Remove server3 p.SetUpstreams([]string{testutil.GetHTTPServerHost(server), testutil.GetHTTPServerHost(server2), host4}) require.True(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) // Remove server4 p.SetUpstreams([]string{testutil.GetHTTPServerHost(server), testutil.GetHTTPServerHost(server2)}) _, err = sendGetToProxy(p) require.Error(t, err) require.False(t, p.HasActiveUpstream()) // Start server4 again, nothing should be changed (keep failure). server4New := testutil.NewHTTPServerAtHost("newCar", host4) defer server4New.Close() _, err = sendGetToProxy(p) require.Error(t, err) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) // Add server3 back to the upstream p.SetUpstreams([]string{testutil.GetHTTPServerHost(server2), host3}) require.False(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) require.Error(t, err) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "newBox", resp.String()) require.True(t, p.HasActiveUpstream()) // Change upstream to host4 p.SetUpstreams([]string{host4}) require.True(t, p.HasActiveUpstream()) _, err = sendGetToProxy(p) // At this time, active upstream is host3, and host4 is not recognized as alive, so it should fail require.Error(t, err) require.False(t, p.HasActiveUpstream()) time.Sleep(probeWait) resp, err = sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "newCar", resp.String()) require.True(t, p.HasActiveUpstream()) } func TestClose(t *testing.T) { p, err := New(configForTest) require.NoError(t, err) defer p.Close() server := testutil.NewHTTPServer("foo") defer server.Close() p.SetUpstreams([]string{testutil.GetHTTPServerHost(server)}) time.Sleep(probeWait) require.True(t, p.HasActiveUpstream()) resp, err := sendGetToProxy(p) require.NoError(t, err) require.Equal(t, "foo", resp.String()) p.Close() require.True(t, p.HasActiveUpstream()) // TODO: Should we fix this behaviour? _, err = sendGetToProxy(p) require.Error(t, err) p.Close() // Close again should be fine! } ================================================ FILE: util/proxy/upstream.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package proxy import ( "net" "time" "github.com/pingcap/log" "go.uber.org/atomic" "go.uber.org/zap" ) type upstream struct { kindTag string addr string active *atomic.Bool } func newUpstream(kindTag, addr string) *upstream { return &upstream{ kindTag: kindTag, addr: addr, active: atomic.NewBool(false), } } // IsActive is concurrent-safe. func (r *upstream) IsActive() bool { return r.active.Load() } // setInactive is concurrent-safe. func (r *upstream) setInactive() { lastIsActive := r.active.Swap(false) if lastIsActive { log.Info("An upstream becomes inactive", zap.String("kindTag", r.kindTag), zap.String("addr", r.addr)) } } // setActive is concurrent-safe. func (r *upstream) setActive() { lastIsActive := r.active.Swap(true) if !lastIsActive { log.Debug("An upstream becomes active", zap.String("kindTag", r.kindTag), zap.String("addr", r.addr)) } } // Connect connects to the upstream no matter it is active or not, and update the active status according to connect status. // When connect failed, the error will be returned and the status will be updated to inactive. // When connect succeeded, the connection will be returned and the status will be updated to active. // This function is concurrent-safe. func (r *upstream) Connect(dialTimeout time.Duration) (net.Conn, error) { log.Debug("Trying to connect to upstream", zap.String("kindTag", r.kindTag), zap.Bool("lastIsActive", r.active.Load()), zap.String("addr", r.addr)) conn, err := net.DialTimeout("tcp", r.addr, dialTimeout) if err != nil { r.setInactive() return nil, err } r.setActive() return conn, nil } // TryProbeAsync probes the upstream if it is not last known as active. func (r *upstream) TryProbeAsync(dialTimeout time.Duration) { if r.IsActive() { return } go func() { conn, err := r.Connect(dialTimeout) if err != nil { // TODO: Reduce number of logs log.Debug("The upstream is still inactive, will be probed again later.", zap.String("kindTag", r.kindTag), zap.String("addr", r.addr), zap.Error(err)) return } // Connect is succeeded, close the connection. _ = conn.Close() }() } ================================================ FILE: util/reflectutil/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package reflectutil import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/reflectutil/field.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package reflectutil import "reflect" // See https://cs.opensource.google/go/go/+/refs/tags/go1.17.1:src/reflect/type.go;l=619 func IsFieldExported(field reflect.StructField) bool { return field.PkgPath == "" } ================================================ FILE: util/reflectutil/field_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package reflectutil import ( "reflect" "testing" "github.com/stretchr/testify/require" ) func TestIsFieldExported(t *testing.T) { type f struct { a string //nolint:unused B string ab string //nolint:unused aBC string //nolint:unused } rt := reflect.TypeFor[f]() require.Equal(t, 4, rt.NumField()) require.False(t, IsFieldExported(rt.Field(0))) require.Equal(t, "a", rt.Field(0).Name) require.True(t, IsFieldExported(rt.Field(1))) require.Equal(t, "B", rt.Field(1).Name) require.False(t, IsFieldExported(rt.Field(2))) require.Equal(t, "ab", rt.Field(2).Name) require.False(t, IsFieldExported(rt.Field(3))) require.Equal(t, "aBC", rt.Field(3).Name) type F2 struct { f } rt = reflect.TypeFor[F2]() require.Equal(t, 1, rt.NumField()) require.False(t, IsFieldExported(rt.Field(0))) require.Equal(t, "f", rt.Field(0).Name) type F3 struct { F2 } rt = reflect.TypeFor[F3]() require.Equal(t, 1, rt.NumField()) require.True(t, IsFieldExported(rt.Field(0))) require.Equal(t, "F2", rt.Field(0).Name) } ================================================ FILE: util/reflectutil/tag.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package reflectutil import ( "reflect" ) // GetFieldsAndTags return fields' tags assign by `tags` parameter. func GetFieldsAndTags(obj interface{}, tags []string) []Field { t := reflect.TypeOf(obj) fNum := t.NumField() fieldTags := make([]Field, 0, fNum) for i := range fNum { f := Field{Tags: map[string]string{}} structField := t.Field(i) f.Name = structField.Name for _, tagName := range tags { f.Tags[tagName] = structField.Tag.Get(tagName) } fieldTags = append(fieldTags, f) } return fieldTags } type Field struct { Name string Tags map[string]string } ================================================ FILE: util/reflectutil/tag_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package reflectutil import ( "testing" "github.com/stretchr/testify/require" ) type MyStruct struct { FirstField string `matched:"first tag" value:"whatever"` SecondField string `matched:"second tag" value:"another whatever"` } func TestGetFieldTags(t *testing.T) { rst := GetFieldsAndTags(MyStruct{}, []string{"matched", "value"}) require.Equal(t, rst, []Field{ { Name: "FirstField", Tags: map[string]string{ "matched": "first tag", "value": "whatever", }, }, { Name: "SecondField", Tags: map[string]string{ "matched": "second tag", "value": "another whatever", }, }, }) } // // TODO: support nested struct // func TestGetFieldTags_with_nested_struct(t *testing.T) {} ================================================ FILE: util/rest/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package rest import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/rest/context_helpers.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package rest import ( "net/http" "github.com/gin-gonic/gin" "github.com/pingcap/tidb-dashboard/util/jsonserde/ginadapter" ) // Error appends an error to the context, which will later becomes an error message returned to the client. // You should not write any other body to the client before or after calling this function. // Otherwise there will be no error message written to the client. // See `ErrorHandlerFn` for more details. func Error(c *gin.Context, err error) { // For security reasons, we need to hide detailed stacktrace info. _ = c.Error(err) // before: c.Error(errorx.EnsureStackTrace(err)) } // JSON writes a JSON string to the client with the given status code. // The key of te `obj` will be serialized in snake_case by default (see `jsonserde` package). func JSON(c *gin.Context, code int, obj interface{}) { c.Render(code, ginadapter.Renderer{Data: obj}) } // OK writes a JSON string to the client with the status code 200. // The key of te `obj` will be serialized in snake_case by default (see `jsonserde` package). func OK(c *gin.Context, obj interface{}) { JSON(c, http.StatusOK, obj) } // MustBind decodes the request body to the passed struct pointer. // If error occurs, `ErrBadRequest` will be recorded in the context and `false` will be returned. You should early // return the handler in this case. func MustBind(c *gin.Context, obj interface{}) bool { if err := c.ShouldBindWith(obj, ginadapter.Binding); err != nil { Error(c, ErrBadRequest.WrapWithNoMessage(err)) return false } return true } ================================================ FILE: util/rest/context_helpers_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package rest import ( "fmt" "net/http" "testing" "github.com/joomcode/errorx" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/testutil/gintest" ) func TestError(t *testing.T) { c, r := gintest.CtxGet(nil) Error(c, fmt.Errorf("my error")) require.Len(t, c.Errors, 1) require.EqualError(t, c.Errors[0].Err, "my error") require.Empty(t, r.Body.String()) } func TestJSON(t *testing.T) { c, r := gintest.CtxGet(nil) JSON(c, http.StatusBadRequest, "foo") require.Empty(t, c.Errors) require.Equal(t, http.StatusBadRequest, r.Code) require.Equal(t, `"foo"`, r.Body.String()) type example struct { FooBar string } c, r = gintest.CtxGet(nil) JSON(c, http.StatusOK, example{FooBar: "value"}) require.Empty(t, c.Errors) require.Equal(t, http.StatusOK, r.Code) require.Equal(t, `{"foo_bar":"value"}`, r.Body.String()) } func TestMustBind(t *testing.T) { c, r := gintest.CtxPost(nil, `"abc"`) var v string bindResult := MustBind(c, &v) require.True(t, bindResult) require.Equal(t, "abc", v) require.Empty(t, c.Errors) require.Empty(t, r.Body.String()) c, r = gintest.CtxPost(nil, `123`) bindResult = MustBind(c, &v) require.False(t, bindResult) require.Len(t, c.Errors, 1) require.Error(t, c.Errors[0].Err) require.True(t, errorx.IsOfType(c.Errors[0].Err, ErrBadRequest)) require.Empty(t, r.Body.String()) } func TestOK(t *testing.T) { c, r := gintest.CtxGet(nil) OK(c, "xyz") require.Empty(t, c.Errors) require.Equal(t, http.StatusOK, r.Code) require.Equal(t, `"xyz"`, r.Body.String()) } ================================================ FILE: util/rest/empty_resp.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package rest type EmptyResponse struct{} ================================================ FILE: util/rest/error.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package rest import ( "net/http" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/pingcap/log" "go.uber.org/zap" ) var ( ErrUnauthenticated = errorx.CommonErrors.NewType("unauthenticated") ErrForbidden = errorx.CommonErrors.NewType("forbidden") ErrBadRequest = errorx.CommonErrors.NewType("bad_request") ErrNotFound = errorx.CommonErrors.NewType("not_found") errInternal = errorx.CommonErrors.NewType("internal") propHTTPCode = errorx.RegisterProperty("http_code") ) func HTTPCodeProperty(code int) (errorx.Property, int) { return propHTTPCode, code } func extractHTTPCodeFromError(err error) int { if err == nil { return http.StatusOK } ex := errorx.Cast(err) if ex == nil { return http.StatusInternalServerError } // If there is a Status Code property inside, take it. v, ok := ex.Property(propHTTPCode) if ok { return v.(int) } // Is it a well-known error type? if ex.IsOfType(ErrUnauthenticated) { // See https://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses // for why StatusUnauthorized comes from ErrUnauthenticated return http.StatusUnauthorized } if ex.IsOfType(ErrForbidden) { return http.StatusForbidden } if ex.IsOfType(ErrBadRequest) { return http.StatusBadRequest } if ex.IsOfType(ErrNotFound) { return http.StatusNotFound } return http.StatusInternalServerError } // ErrorHandlerFn creates a handler func that turns (last) error in the context into an APIError json response. // In handlers, `rest.Error(c, err)` can be used to attach the error to the context. // When error is attached in the context: // - The handler can optionally assign the HTTP status code. // - The handler must not self-generate a response body. func ErrorHandlerFn() gin.HandlerFunc { return func(c *gin.Context) { c.Next() err := c.Errors.Last() if err == nil { return } if c.Writer.Size() > 0 { return } statusCode := c.Writer.Status() if statusCode == http.StatusOK { // Change the status code if it is not specified. statusCode = extractHTTPCodeFromError(err.Err) } errResponse := NewErrorResponse(err.Err) log.Warn("Error when handling request", zap.String("uri", c.Request.RequestURI), zap.String("remoteAddr", c.Request.RemoteAddr), zap.String("errorCode", errResponse.Code), zap.String("errorMessage", errResponse.Message), zap.String("errorFullText", errResponse.FullText), // empty unless on debug level ) c.AbortWithStatusJSON(statusCode, errResponse) } } ================================================ FILE: util/rest/error_resp.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package rest import ( "fmt" "strings" "github.com/joomcode/errorx" "github.com/pingcap/log" "go.uber.org/zap/zapcore" ) type ErrorResponse struct { Error bool `json:"error"` Message string `json:"message"` Code string `json:"code"` FullText string `json:"full_text"` } // buildSimpleMessage traverses through the error chain and builds a simple error message. func buildSimpleMessage(err error) string { if err == nil { return "" } mb := strings.Builder{} isFirstMsg := true cause := err for cause != nil { causeEx := errorx.Cast(cause) var msg string if causeEx == nil { // cause exists, but is not an errorx type msg = cause.Error() } else { msg = causeEx.Message() } if len(msg) > 0 { if !isFirstMsg { mb.WriteString(", caused by: ") } mb.WriteString(msg) isFirstMsg = false } if causeEx == nil { // This is already an error interface. It is not possible to get cause any more. break } cause = causeEx.Cause() } if isFirstMsg { // No message is successfully extracted return err.Error() } return mb.String() } func buildCode(err error) string { if err == nil { return "" } cause := err for cause != nil { causeEx := errorx.Cast(cause) if causeEx == nil { break } if causeEx.Type().RootNamespace().FullName() == "synthetic" { // Ignore standard transparent types. // User-defined transparent types are not detectable, however. cause = causeEx.Cause() } else { return causeEx.Type().FullName() } } return errInternal.FullName() } // Note: This function only exists for compatibility during the refactoring. Before refactoring, // all error codes begin with "error.". We will migrate more and more error codes to not begin with "error.". // Finally, after all error codes are migrated, this function is no longer needed. func removeErrorPrefix(code string) string { return strings.TrimPrefix(code, "error.") } func buildDetailMessage(err error) string { if err == nil { return "" } return fmt.Sprintf("%+v", errorx.EnsureStackTrace(err)) } func NewErrorResponse(err error) ErrorResponse { logLevel := log.GetLevel() fullText := "" if logLevel == zapcore.DebugLevel { fullText = buildDetailMessage(err) } return ErrorResponse{ Error: true, Message: buildSimpleMessage(err), Code: removeErrorPrefix(buildCode(err)), // For security reasons, we need to hide detailed stacktrace info in prod. FullText: fullText, } } ================================================ FILE: util/rest/error_resp_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package rest import ( "fmt" "os" "regexp" "strings" "testing" "github.com/joomcode/errorx" "github.com/stretchr/testify/require" ) func TestErrors(t *testing.T) { ns := errorx.NewNamespace("ns") errTypeInner := ns.NewType("errInner") errTypeOuter := ns.NewType("errOuter") tests := []struct { err error expectCode string expectSimpleMessage string expectDetailMessage string }{ { fmt.Errorf(""), "common.internal", "", "", }, { fmt.Errorf("foo"), "common.internal", "foo", "foo", }, { os.ErrNotExist, "common.internal", "file does not exist", "file does not exist", }, { fmt.Errorf("internal error: %w", os.ErrNotExist), "common.internal", "internal error: file does not exist", "internal error: file does not exist", }, { errTypeInner.NewWithNoMessage(), "ns.errInner", "ns.errInner", "ns.errInner", }, { errTypeInner.New("foo"), "ns.errInner", "foo", "ns.errInner: foo", }, { errTypeOuter.WrapWithNoMessage(os.ErrNotExist), "ns.errOuter", "file does not exist", "ns.errOuter: file does not exist", }, { errTypeOuter.Wrap(os.ErrNotExist, "internal error"), "ns.errOuter", "internal error, caused by: file does not exist", "ns.errOuter: internal error, cause: file does not exist", }, { errTypeOuter.WrapWithNoMessage(errTypeInner.NewWithNoMessage()), "ns.errOuter", "ns.errOuter: ns.errInner", "ns.errOuter: ns.errInner", }, { errTypeOuter.WrapWithNoMessage(errTypeInner.New("foo")), "ns.errOuter", "foo", "ns.errOuter: ns.errInner: foo", }, { errTypeOuter.WrapWithNoMessage(errTypeInner.WrapWithNoMessage(os.ErrNotExist)), "ns.errOuter", "file does not exist", "ns.errOuter: ns.errInner: file does not exist", }, { errTypeOuter.WrapWithNoMessage(errTypeInner.WrapWithNoMessage(fmt.Errorf(""))), "ns.errOuter", "ns.errOuter: ns.errInner", "ns.errOuter: ns.errInner", }, { errTypeOuter.WrapWithNoMessage(errTypeInner.Wrap(os.ErrNotExist, "internal error")), "ns.errOuter", "internal error, caused by: file does not exist", "ns.errOuter: ns.errInner: internal error, cause: file does not exist", }, { errTypeOuter.Wrap(errTypeInner.NewWithNoMessage(), "gateway error"), "ns.errOuter", "gateway error", "ns.errOuter: gateway error, cause: ns.errInner", }, { errTypeOuter.Wrap(errTypeInner.New("foo"), "gateway error"), "ns.errOuter", "gateway error, caused by: foo", "ns.errOuter: gateway error, cause: ns.errInner: foo", }, { errTypeOuter.Wrap(errTypeInner.WrapWithNoMessage(os.ErrNotExist), "gateway error"), "ns.errOuter", "gateway error, caused by: file does not exist", "ns.errOuter: gateway error, cause: ns.errInner: file does not exist", }, { errTypeOuter.Wrap(errTypeInner.Wrap(os.ErrNotExist, "internal error"), "gateway error"), "ns.errOuter", "gateway error, caused by: internal error, caused by: file does not exist", "ns.errOuter: gateway error, cause: ns.errInner: internal error, cause: file does not exist", }, { errorx.Decorate(errorx.IllegalState.New("unfortunate"), "this could be so much better"), "common.illegal_state", "this could be so much better, caused by: unfortunate", "this could be so much better, cause: common.illegal_state: unfortunate", }, { errorx.Decorate(os.ErrNotExist, "this could be so much better"), "common.internal", "this could be so much better, caused by: file does not exist", "this could be so much better, cause: file does not exist", }, } for idx, tt := range tests { t.Run(fmt.Sprintf("Case #%d", idx), func(t *testing.T) { require.Equal(t, tt.expectSimpleMessage, buildSimpleMessage(tt.err)) require.Equal(t, tt.expectCode, buildCode(tt.err)) requireErrorAndStack(t, buildDetailMessage(tt.err), tt.expectDetailMessage) }) } require.Equal(t, "", buildSimpleMessage(nil)) require.Equal(t, "", buildCode(nil)) require.Equal(t, "", buildDetailMessage(nil)) } func requireErrorAndStack(t *testing.T, src string, errMessage string) { lines := strings.SplitN(src, "\n", 2) require.Equal(t, 2, len(lines)) require.Equal(t, errMessage, lines[0]) require.NotEmpty(t, lines[1]) stacks := strings.Split(lines[1], "\n") require.GreaterOrEqual(t, len(stacks), 2) require.True(t, regexp.MustCompile(`^\s*at github\.com/.*?\(\)`).MatchString(stacks[0])) require.True(t, regexp.MustCompile(`\.go:\d+$`).MatchString(stacks[1])) } ================================================ FILE: util/rest/error_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package rest import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "go.uber.org/atomic" "github.com/pingcap/tidb-dashboard/util/assertutil" ) func TestExtractHTTPCodeFromError(t *testing.T) { ns := errorx.NewNamespace("ns") et := ns.NewType("err1") tests := []struct { want int args error }{ {http.StatusOK, nil}, {http.StatusInternalServerError, fmt.Errorf("foo")}, {http.StatusBadRequest, ErrBadRequest.NewWithNoMessage()}, {http.StatusBadRequest, ErrBadRequest.WrapWithNoMessage(fmt.Errorf("foo"))}, {http.StatusBadRequest, errorx.Decorate(ErrBadRequest.NewWithNoMessage(), "parameter foo is invalid")}, {http.StatusInternalServerError, et.NewWithNoMessage()}, {http.StatusInternalServerError, et.WrapWithNoMessage(ErrBadRequest.NewWithNoMessage())}, {http.StatusBadGateway, et.NewWithNoMessage().WithProperty(HTTPCodeProperty(http.StatusBadGateway))}, {http.StatusConflict, ErrBadRequest.NewWithNoMessage().WithProperty(HTTPCodeProperty(http.StatusConflict))}, } for _, tt := range tests { require.Equal(t, tt.want, extractHTTPCodeFromError(tt.args)) } } type ErrorHandlerFnTestSuite struct { suite.Suite } func (suite *ErrorHandlerFnTestSuite) TestNoError() { engine := gin.New() engine.Use(ErrorHandlerFn()) engine.GET("/test", func(c *gin.Context) { OK(c, gin.H{ "foo": "bar", }) }) r := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/foo/", nil) engine.ServeHTTP(r, req) suite.Require().Equal(http.StatusNotFound, r.Code) r = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/test", nil) engine.ServeHTTP(r, req) suite.Require().Equal(http.StatusOK, r.Code) suite.Require().JSONEq(`{"foo":"bar"}`, r.Body.String()) } func (suite *ErrorHandlerFnTestSuite) TestNormalError() { engine := gin.New() engine.Use(ErrorHandlerFn()) engine.GET("/test", func(c *gin.Context) { Error(c, fmt.Errorf("some error")) }) r := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/test", nil) engine.ServeHTTP(r, req) suite.Require().Equal(http.StatusInternalServerError, r.Code) assertutil.RequireJSONContains(suite.T(), r.Body.String(), `{"error":true,"message":"some error","code":"common.internal"}`) } func (suite *ErrorHandlerFnTestSuite) TestBuiltinError() { engine := gin.New() engine.Use(ErrorHandlerFn()) engine.GET("/test", func(c *gin.Context) { Error(c, ErrBadRequest.NewWithNoMessage()) }) r := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/test", nil) engine.ServeHTTP(r, req) suite.Require().Equal(http.StatusBadRequest, r.Code) assertutil.RequireJSONContains(suite.T(), r.Body.String(), `{"error":true,"message":"common.bad_request","code":"common.bad_request"}`) } func (suite *ErrorHandlerFnTestSuite) TestOverrideStatusCode() { engine := gin.New() engine.Use(ErrorHandlerFn()) engine.GET("/test", func(c *gin.Context) { Error(c, ErrBadRequest.NewWithNoMessage()) c.Status(http.StatusBadGateway) }) r := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/test", nil) engine.ServeHTTP(r, req) suite.Require().Equal(http.StatusBadGateway, r.Code) assertutil.RequireJSONContains(suite.T(), r.Body.String(), `{"error":true,"message":"common.bad_request","code":"common.bad_request"}`) } func (suite *ErrorHandlerFnTestSuite) TestResponseAfterError() { engine := gin.New() engine.Use(ErrorHandlerFn()) engine.GET("/test", func(c *gin.Context) { Error(c, ErrBadRequest.NewWithNoMessage()) // If normal response is returned, no error message will be generated c.String(http.StatusNotFound, "foobar") }) r := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/test", nil) engine.ServeHTTP(r, req) suite.Require().Equal(http.StatusNotFound, r.Code) suite.Require().Equal(`foobar`, r.Body.String()) } func (suite *ErrorHandlerFnTestSuite) TestNextMiddleware() { middlewareCalled := atomic.NewBool(false) engine := gin.New() engine.Use(ErrorHandlerFn()) engine.Use(func(c *gin.Context) { // Middleware after the ErrorHandlerFn is called even if error is returned, // as ErrorHandlerFn handles errors after processing the request c.Next() middlewareCalled.Store(true) }) engine.GET("/test", func(c *gin.Context) { Error(c, ErrBadRequest.NewWithNoMessage()) }) r := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/test", nil) engine.ServeHTTP(r, req) suite.Require().Equal(http.StatusBadRequest, r.Code) suite.Require().True(middlewareCalled.Load()) } // When panic happened, ErrorHandlerFn will not be invoked. func (suite *ErrorHandlerFnTestSuite) TestWithRecoveryMiddleware() { engine := gin.New() engine.Use(gin.Recovery()) engine.Use(ErrorHandlerFn()) engine.GET("/test", func(_ *gin.Context) { panic("some panic") }) r := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/test", nil) engine.ServeHTTP(r, req) suite.Require().Equal(http.StatusInternalServerError, r.Code) suite.Require().Equal("", r.Body.String()) } func TestErrorHandlerFn(t *testing.T) { suite.Run(t, &ErrorHandlerFnTestSuite{}) } ================================================ FILE: util/rest/fileswap/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package fileswap import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/rest/fileswap/server.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package fileswap import ( "errors" "fmt" "io" "os" "time" "github.com/gin-gonic/gin" jwt "github.com/golang-jwt/jwt/v4" "github.com/gtank/cryptopasta" "github.com/joomcode/errorx" "github.com/minio/sio" "github.com/pingcap/tidb-dashboard/util/nocopy" "github.com/pingcap/tidb-dashboard/util/rest" ) // Handler provides a file-based data serving HTTP handler. // Arbitrary data stream can be stored in the file in encrypted form temporarily, and then downloaded by the user later. // As data is stored in the file, large chunk of data is supported. // // Note: the download token cannot be mixed in different Handler instances. type Handler struct { nocopy.NoCopy // The secret is used to sign the download token as well as encrypting the file in the FS. secret []byte } func New() *Handler { return &Handler{ secret: cryptopasta.NewEncryptionKey()[:], } } // NewFileWriter creates a writer for storing data into FS. A download token can be generated from the writer // for downloading later. The downloading can be handled by the HandleDownloadRequest. func (s *Handler) NewFileWriter(tempFilePattern string) (*FileWriter, error) { file, err := os.CreateTemp("", tempFilePattern) if err != nil { return nil, err } w, err := sio.EncryptWriter(file, sio.Config{Key: s.secret}) if err != nil { _ = file.Close() _ = os.Remove(file.Name()) return nil, err } return &FileWriter{ WriteCloser: w, secret: s.secret, filePath: file.Name(), }, nil } type downloadTokenClaims struct { jwt.StandardClaims TempFileName string DownloadFileName string } func (s *Handler) parseClaimsFromToken(tokenString string) (*downloadTokenClaims, error) { token, err := jwt.ParseWithClaims( tokenString, &downloadTokenClaims{}, func(_ *jwt.Token) (interface{}, error) { return s.secret, nil }) if token != nil { if claims, ok := token.Claims.(*downloadTokenClaims); ok && token.Valid { return claims, nil } } var ve *jwt.ValidationError if errors.As(err, &ve) && ve.Errors&jwt.ValidationErrorExpired != 0 { return nil, errorx.Decorate(err, "download token is expired") } return nil, errorx.Decorate(err, "download token is invalid") } // HandleDownloadRequest handles a gin Request for serving the file in the FS by using a download token. // The file will be removed after it is successfully served to the user. func (s *Handler) HandleDownloadRequest(c *gin.Context) { claims, err := s.parseClaimsFromToken(c.Query("token")) if err != nil { rest.Error(c, rest.ErrBadRequest.Wrap(err, "Invalid download request")) return } file, err := os.Open(claims.TempFileName) if err != nil { if os.IsNotExist(err) { // It is possible that token is reused. In this case, raise invalid request error. rest.Error(c, rest.ErrBadRequest.Wrap(err, "Download file not found. Please retry.")) } else { rest.Error(c, err) } return } defer func() { _ = file.Close() _ = os.Remove(claims.TempFileName) }() c.Writer.Header().Set("Content-type", "application/octet-stream") c.Writer.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, claims.DownloadFileName)) _, err = sio.Decrypt(c.Writer, file, sio.Config{ Key: s.secret, }) if err != nil { rest.Error(c, err) return } } type FileWriter struct { nocopy.NoCopy io.WriteCloser secret []byte filePath string } func (fw *FileWriter) Remove() { _ = fw.Close() _ = os.Remove(fw.filePath) } // GetDownloadToken generates a download token for downloading this file later. // The downloading can be handled by the Handler.HandleDownloadRequest. func (fw *FileWriter) GetDownloadToken(downloadFileName string, expireIn time.Duration) (string, error) { claims := downloadTokenClaims{ TempFileName: fw.filePath, DownloadFileName: downloadFileName, StandardClaims: jwt.StandardClaims{ //nolint:staticcheck // StandardClaims is deprecated, but we use it here temporarily ExpiresAt: time.Now().Add(expireIn).Unix(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenSigned, err := token.SignedString(fw.secret) if err != nil { return "", err } return tokenSigned, nil } ================================================ FILE: util/rest/fileswap/server_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package fileswap import ( "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/assertutil" "github.com/pingcap/tidb-dashboard/util/rest" ) func (s *Handler) mustGetDownloadToken(t *testing.T, fileContent string, downloadFileName string, expireIn time.Duration) string { fw, err := s.NewFileWriter("test") require.NoError(t, err) _, err = fmt.Fprint(fw, fileContent) require.NoError(t, err) err = fw.Close() require.NoError(t, err) token, err := fw.GetDownloadToken(downloadFileName, expireIn) require.NoError(t, err) return token } func TestDownload(t *testing.T) { handler := New() token := handler.mustGetDownloadToken(t, "foobar", "file.txt", time.Second*5) r := httptest.NewRecorder() c, _ := gin.CreateTestContext(r) c.Request, _ = http.NewRequest(http.MethodGet, "/download?token="+token, nil) handler.HandleDownloadRequest(c) require.Len(t, c.Errors, 0) require.Equal(t, http.StatusOK, r.Code) require.Equal(t, `attachment; filename="file.txt"`, r.Header().Get("Content-Disposition")) require.Equal(t, `application/octet-stream`, r.Header().Get("Content-Type")) require.Equal(t, "foobar", r.Body.String()) // Download again r = httptest.NewRecorder() c, _ = gin.CreateTestContext(r) c.Request, _ = http.NewRequest(http.MethodGet, "/download?token="+token, nil) handler.HandleDownloadRequest(c) require.Len(t, c.Errors, 1) require.True(t, errorx.IsOfType(c.Errors[0].Err, rest.ErrBadRequest)) require.Contains(t, c.Errors[0].Error(), "Download file not found") } func TestDownloadAnotherInstance(t *testing.T) { handler := New() token := handler.mustGetDownloadToken(t, "foobar", "file.txt", time.Second*5) handler2 := New() r := httptest.NewRecorder() c, _ := gin.CreateTestContext(r) c.Request, _ = http.NewRequest(http.MethodGet, "/download?token="+token, nil) handler2.HandleDownloadRequest(c) require.Len(t, c.Errors, 1) require.True(t, errorx.IsOfType(c.Errors[0].Err, rest.ErrBadRequest)) require.Contains(t, c.Errors[0].Error(), "Invalid download request") require.Contains(t, c.Errors[0].Error(), "download token is invalid") } func TestExpiredToken(t *testing.T) { handler := New() token := handler.mustGetDownloadToken(t, "foobar", "file.txt", 0) // Note: token expiration precision is 1sec. time.Sleep(time.Millisecond * 1100) r := httptest.NewRecorder() c, _ := gin.CreateTestContext(r) c.Request, _ = http.NewRequest(http.MethodGet, "/download?token="+token, nil) handler.HandleDownloadRequest(c) require.Len(t, c.Errors, 1) require.True(t, errorx.IsOfType(c.Errors[0].Err, rest.ErrBadRequest)) require.Contains(t, c.Errors[0].Error(), "Invalid download request") require.Contains(t, c.Errors[0].Error(), "download token is expired") } func TestNotAToken(t *testing.T) { handler := New() r := httptest.NewRecorder() c, _ := gin.CreateTestContext(r) c.Request, _ = http.NewRequest(http.MethodGet, "/download?token=abc", nil) handler.HandleDownloadRequest(c) require.Len(t, c.Errors, 1) require.True(t, errorx.IsOfType(c.Errors[0].Err, rest.ErrBadRequest)) require.Contains(t, c.Errors[0].Error(), "Invalid download request") require.Contains(t, c.Errors[0].Error(), "download token is invalid") } func TestDownloadInMiddleware(t *testing.T) { handler := New() token := handler.mustGetDownloadToken(t, "abc", "myfile.bin", time.Second*5) engine := gin.New() engine.Use(rest.ErrorHandlerFn()) engine.GET("/download", handler.HandleDownloadRequest) // A normal request r := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/download?token="+token, nil) engine.ServeHTTP(r, req) require.Equal(t, http.StatusOK, r.Code) require.Equal(t, `attachment; filename="myfile.bin"`, r.Header().Get("Content-Disposition")) require.Equal(t, `application/octet-stream`, r.Header().Get("Content-Type")) require.Equal(t, "abc", r.Body.String()) // A request without token r = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/download", nil) engine.ServeHTTP(r, req) require.Equal(t, http.StatusBadRequest, r.Code) require.Equal(t, "", r.Header().Get("Content-Disposition")) require.Equal(t, "application/json; charset=utf-8", r.Header().Get("Content-Type")) assertutil.RequireJSONContains(t, r.Body.String(), `{"code":"common.bad_request", "error":true}`) } ================================================ FILE: util/sqlitestore/sqlitestore.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package dbstore import ( "context" "os" "path" "github.com/pingcap/log" "go.uber.org/fx" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" "moul.io/zapgorm2" ) type SqliteDB struct { *gorm.DB } type Config struct { DbFilePath string } // NewSqliteStore creates a new SQLite storage. When lifecycle is ended, the storage will be closed. func NewSqliteStore(lc fx.Lifecycle, config Config) (*SqliteDB, error) { dataDir := path.Dir(config.DbFilePath) err := os.MkdirAll(dataDir, 0o700) if err != nil { log.Error("Failed to create Dashboard storage directory", zap.Error(err)) return nil, err } log.Info("Dashboard initializing local storage file", zap.String("path", config.DbFilePath)) gormDB, err := gorm.Open(sqlite.Open(config.DbFilePath), &gorm.Config{ Logger: zapgorm2.New(log.L()), }) if err != nil { log.Error("Failed to open Dashboard storage file", zap.Error(err)) return nil, err } db := &SqliteDB{gormDB} lc.Append(fx.Hook{ OnStop: func(context.Context) error { sqlDB, err := db.DB.DB() if err != nil { return err } return sqlDB.Close() }, }) return db, nil } ================================================ FILE: util/testutil/db.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package testutil import ( "encoding/hex" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" mysqldriver "github.com/go-sql-driver/mysql" "github.com/google/uuid" "github.com/pingcap/log" "github.com/stretchr/testify/require" "gorm.io/driver/mysql" "gorm.io/gorm" "moul.io/zapgorm2" ) type TestDB struct { inner *gorm.DB require *require.Assertions isUnderlyingMocked bool mock sqlmock.Sqlmock } func OpenTestDB(t *testing.T, configModifier ...func(*mysqldriver.Config, *gorm.Config)) *TestDB { r := require.New(t) dsn := mysqldriver.NewConfig() dsn.Net = "tcp" dsn.Addr = "127.0.0.1:4000" dsn.Params = map[string]string{"time_zone": "'+00:00'"} dsn.ParseTime = true dsn.Loc = time.UTC dsn.User = "root" dsn.DBName = "test" config := &gorm.Config{ Logger: zapgorm2.New(log.L()), } for _, m := range configModifier { m(dsn, config) } db, err := gorm.Open(mysql.Open(dsn.FormatDSN()), config) r.NoError(err) return &TestDB{ inner: db.Debug(), require: r, } } func OpenMockDB(t *testing.T, configModifier ...func(*gorm.Config)) *TestDB { r := require.New(t) sqlDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) require.NoError(t, err) config := &gorm.Config{ Logger: zapgorm2.New(log.L()), } for _, m := range configModifier { m(config) } db, err := gorm.Open(mysql.New(mysql.Config{ Conn: sqlDB, SkipInitializeWithVersion: true, }), config) r.NoError(err) return &TestDB{ inner: db.Debug(), require: r, isUnderlyingMocked: true, mock: mock, } } func (db *TestDB) MustClose() { if db.isUnderlyingMocked { db.mock.ExpectClose() } d, err := db.inner.DB() db.require.NoError(err) err = d.Close() db.require.NoError(err) } func (db *TestDB) NewID() string { id := uuid.New() return hex.EncodeToString(id[:]) } func (db *TestDB) Gorm() *gorm.DB { return db.inner } func (db *TestDB) MustExec(sql string, values ...interface{}) { err := db.inner.Exec(sql, values...).Error db.require.NoError(err) } type ExplainRow struct { ID string `gorm:"column:id"` } func (db *TestDB) MustExplain(sql string, values ...interface{}) []ExplainRow { var rows []ExplainRow err := db.Gorm().Raw("EXPLAIN "+sql, values...).Scan(&rows).Error db.require.NoError(err) return rows } func (db *TestDB) Mocker() sqlmock.Sqlmock { db.require.True(db.isUnderlyingMocked) return db.mock } func (db *TestDB) MustMeetMockExpectation() { db.require.Nil(db.Mocker().ExpectationsWereMet()) } func RequireIndexRangeScan(t *testing.T, explain []ExplainRow) { hasIndexRange := false for _, r := range explain { if strings.Contains(r.ID, "IndexRangeScan") { hasIndexRange = true break } } require.True(t, hasIndexRange, "IndexRangeScan is not contained in the explain result", explain) } ================================================ FILE: util/testutil/gintest/context.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package gintest import ( "bytes" "net/http" "net/http/httptest" "net/url" "github.com/gin-gonic/gin" ) func CtxGet(queryParams url.Values) (c *gin.Context, r *httptest.ResponseRecorder) { r = httptest.NewRecorder() c, _ = gin.CreateTestContext(r) u := "/" if queryParams != nil { u = "/?" + queryParams.Encode() } c.Request, _ = http.NewRequest(http.MethodGet, u, nil) return } func CtxPost(queryParams url.Values, postBody string) (c *gin.Context, r *httptest.ResponseRecorder) { r = httptest.NewRecorder() c, _ = gin.CreateTestContext(r) u := "/" if queryParams != nil { u = "/?" + queryParams.Encode() } c.Request, _ = http.NewRequest(http.MethodPost, u, bytes.NewBuffer([]byte(postBody))) return } ================================================ FILE: util/testutil/http_server.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package testutil import ( "fmt" "net" "net/http" "net/http/httptest" "go.uber.org/atomic" ) func GetHTTPServerHost(server *httptest.Server) string { return server.Listener.Addr().String() } func NewHTTPServer(response string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprintln(w, response) })) } func NewHTTPServerAtHost(response string, host string) *httptest.Server { l, err := net.Listen("tcp", host) if err != nil { panic(err) } server := &httptest.Server{ Listener: l, Config: &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { // nolint:gosec _, _ = fmt.Fprintln(w, response) })}, } server.Start() return server } type MultiServerHelper struct { Servers []*httptest.Server lastActiveID *atomic.Int32 lastResponse *atomic.String } func NewMultiServer(n int, responsePattern string) *MultiServerHelper { servers := make([]*httptest.Server, 0) lastActiveID := atomic.NewInt32(-1) lastResponse := atomic.NewString("") for i := range n { func(i int) { s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { resp := fmt.Sprintf(responsePattern, i) lastActiveID.Store(int32(i)) lastResponse.Store(resp) _, _ = fmt.Fprintln(w, resp) })) servers = append(servers, s) }(i) } return &MultiServerHelper{ Servers: servers, lastActiveID: lastActiveID, lastResponse: lastResponse, } } func (m *MultiServerHelper) LastResp() string { return m.lastResponse.Load() } func (m *MultiServerHelper) LastID() int { return int(m.lastActiveID.Load()) } func (m *MultiServerHelper) CloseAll() { for _, s := range m.Servers { s.Close() } } func (m *MultiServerHelper) GetEndpoints() []string { l := make([]string, 0, len(m.Servers)) for _, s := range m.Servers { l = append(l, GetHTTPServerHost(s)) } return l } ================================================ FILE: util/testutil/httpmockutil/responder.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package httpmockutil import ( "net/http" "strings" "github.com/jarcoal/httpmock" ) func StringResponder(body string) httpmock.Responder { return httpmock.NewStringResponder(200, strings.TrimSpace(body)) } func ChanStringResponder(ch chan string) httpmock.Responder { return func(*http.Request) (*http.Response, error) { v := <-ch return httpmock.NewStringResponse(200, v), nil } } ================================================ FILE: util/testutil/testdefault/main.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package testdefault import ( "runtime" "testing" "github.com/gin-gonic/gin" "github.com/pingcap/log" "go.uber.org/goleak" ) func enableDebugLog() { logger, prop, err := log.InitLogger(&log.Config{ Level: "debug", }) if err != nil { panic(err) } log.ReplaceGlobals(logger, prop) } func TestMain(m *testing.M) { enableDebugLog() gin.SetMode(gin.TestMode) opts := []goleak.Option{ goleak.IgnoreTopFunction("github.com/ReneKroon/ttlcache/v2.(*Cache).startExpirationProcessing"), } goleak.VerifyTestMain(m, opts...) runtime.GC() } ================================================ FILE: util/timeutil/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package timeutil import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/timeutil/format.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package timeutil import "time" const ( DateTimeFormat = "2006-01-02 15:04:05 MST" ) func FormatInUTC(t time.Time) string { return t.UTC().Format(DateTimeFormat) } ================================================ FILE: util/timeutil/format_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package timeutil import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestFormatInUTC(t *testing.T) { require.Equal(t, "2021-10-01 16:53:55 UTC", FormatInUTC(time.Unix(1633107235, 0))) } ================================================ FILE: util/tlsutil/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tlsutil import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/tlsutil/config_ext.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tlsutil import ( "crypto/tls" "strings" ) func GetHTTPScheme(c *tls.Config) string { if c == nil { return "http" } return "https" } func NormalizeURL(c *tls.Config, url string) string { const httpPrefix = "http://" const httpsPrefix = "https://" const httpPrefixLen = len(httpPrefix) isLeadingHTTP := strings.HasPrefix(url, httpPrefix) isLeadingHTTPS := strings.HasPrefix(url, httpsPrefix) if !isLeadingHTTP && !isLeadingHTTPS { return GetHTTPScheme(c) + "://" + url } if c != nil && isLeadingHTTP { return httpsPrefix + url[httpPrefixLen:] } return url } ================================================ FILE: util/tlsutil/config_ext_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package tlsutil import ( "crypto/tls" "testing" "github.com/stretchr/testify/require" ) func TestNormalizeURL(t *testing.T) { var c *tls.Config c = nil require.Equal(t, "http://foo", NormalizeURL(c, "foo")) require.Equal(t, "http://foo", NormalizeURL(c, "http://foo")) require.Equal(t, "https://foo", NormalizeURL(c, "https://foo")) require.Equal(t, "http://ftp://foo", NormalizeURL(c, "ftp://foo")) c = &tls.Config{} // #nosec G402 require.Equal(t, "https://foo", NormalizeURL(c, "foo")) require.Equal(t, "https://foo", NormalizeURL(c, "http://foo")) require.Equal(t, "https://foo", NormalizeURL(c, "https://foo")) require.Equal(t, "https://ftp://foo", NormalizeURL(c, "ftp://foo")) } ================================================ FILE: util/topo/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topo import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/topo/mock_TopologyProvider.go ================================================ // Code generated by mockery v2.40.3. DO NOT EDIT. package topo import ( context "context" mock "github.com/stretchr/testify/mock" ) // MockTopologyProvider is an autogenerated mock type for the TopologyProvider type type MockTopologyProvider struct { mock.Mock } // GetAlertManager provides a mock function with given fields: ctx func (_m *MockTopologyProvider) GetAlertManager(ctx context.Context) (*AlertManagerInfo, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetAlertManager") } var r0 *AlertManagerInfo var r1 error if rf, ok := ret.Get(0).(func(context.Context) (*AlertManagerInfo, error)); ok { return rf(ctx) } if rf, ok := ret.Get(0).(func(context.Context) *AlertManagerInfo); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*AlertManagerInfo) } } if rf, ok := ret.Get(1).(func(context.Context) error); ok { r1 = rf(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // GetGrafana provides a mock function with given fields: ctx func (_m *MockTopologyProvider) GetGrafana(ctx context.Context) (*GrafanaInfo, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetGrafana") } var r0 *GrafanaInfo var r1 error if rf, ok := ret.Get(0).(func(context.Context) (*GrafanaInfo, error)); ok { return rf(ctx) } if rf, ok := ret.Get(0).(func(context.Context) *GrafanaInfo); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*GrafanaInfo) } } if rf, ok := ret.Get(1).(func(context.Context) error); ok { r1 = rf(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // GetPD provides a mock function with given fields: ctx func (_m *MockTopologyProvider) GetPD(ctx context.Context) ([]PDInfo, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetPD") } var r0 []PDInfo var r1 error if rf, ok := ret.Get(0).(func(context.Context) ([]PDInfo, error)); ok { return rf(ctx) } if rf, ok := ret.Get(0).(func(context.Context) []PDInfo); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]PDInfo) } } if rf, ok := ret.Get(1).(func(context.Context) error); ok { r1 = rf(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // GetPrometheus provides a mock function with given fields: ctx func (_m *MockTopologyProvider) GetPrometheus(ctx context.Context) (*PrometheusInfo, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetPrometheus") } var r0 *PrometheusInfo var r1 error if rf, ok := ret.Get(0).(func(context.Context) (*PrometheusInfo, error)); ok { return rf(ctx) } if rf, ok := ret.Get(0).(func(context.Context) *PrometheusInfo); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*PrometheusInfo) } } if rf, ok := ret.Get(1).(func(context.Context) error); ok { r1 = rf(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // GetTiDB provides a mock function with given fields: ctx func (_m *MockTopologyProvider) GetTiDB(ctx context.Context) ([]TiDBInfo, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetTiDB") } var r0 []TiDBInfo var r1 error if rf, ok := ret.Get(0).(func(context.Context) ([]TiDBInfo, error)); ok { return rf(ctx) } if rf, ok := ret.Get(0).(func(context.Context) []TiDBInfo); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]TiDBInfo) } } if rf, ok := ret.Get(1).(func(context.Context) error); ok { r1 = rf(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // GetTiFlash provides a mock function with given fields: ctx func (_m *MockTopologyProvider) GetTiFlash(ctx context.Context) ([]TiFlashStoreInfo, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetTiFlash") } var r0 []TiFlashStoreInfo var r1 error if rf, ok := ret.Get(0).(func(context.Context) ([]TiFlashStoreInfo, error)); ok { return rf(ctx) } if rf, ok := ret.Get(0).(func(context.Context) []TiFlashStoreInfo); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]TiFlashStoreInfo) } } if rf, ok := ret.Get(1).(func(context.Context) error); ok { r1 = rf(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // GetTiKV provides a mock function with given fields: ctx func (_m *MockTopologyProvider) GetTiKV(ctx context.Context) ([]TiKVStoreInfo, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetTiKV") } var r0 []TiKVStoreInfo var r1 error if rf, ok := ret.Get(0).(func(context.Context) ([]TiKVStoreInfo, error)); ok { return rf(ctx) } if rf, ok := ret.Get(0).(func(context.Context) []TiKVStoreInfo); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]TiKVStoreInfo) } } if rf, ok := ret.Get(1).(func(context.Context) error); ok { r1 = rf(ctx) } else { r1 = ret.Error(1) } return r0, r1 } // NewMockTopologyProvider creates a new instance of MockTopologyProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockTopologyProvider(t interface { mock.TestingT Cleanup(func()) }) *MockTopologyProvider { mock := &MockTopologyProvider{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: util/topo/model.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topo type CompStatus string const ( CompStatusUnknown CompStatus = "unknown" CompStatusUnreachable CompStatus = "unreachable" CompStatusUp CompStatus = "up" CompStatusTombstone CompStatus = "tombstone" CompStatusLeaving CompStatus = "leaving" CompStatusDown CompStatus = "down" ) type Kind string const ( KindTiDB Kind = "tidb" KindTiKV Kind = "tikv" KindPD Kind = "pd" KindTiFlash Kind = "tiflash" KindTiCDC Kind = "ticdc" KindTiProxy Kind = "tiproxy" KindTSO Kind = "tso" KindScheduling Kind = "scheduling" KindAlertManager Kind = "alert_manager" KindGrafana Kind = "grafana" KindPrometheus Kind = "prometheus" ) type PDInfo struct { GitHash string Version string IP string Port uint DeployPath string Status CompStatus StartTimestamp int64 // Ts = 0 means unknown } var _ Info = &PDInfo{} func (i *PDInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, Kind: KindPD, }, Version: i.Version, Status: i.Status, } } type TiDBInfo struct { GitHash string Version string IP string Port uint DeployPath string Status CompStatus StatusPort uint StartTimestamp int64 } var _ Info = &TiDBInfo{} func (i *TiDBInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, StatusPort: i.StatusPort, Kind: KindTiDB, }, Version: i.Version, Status: i.Status, } } // StoreInfo may be either a TiKV store info or a TiFlash store info. type StoreInfo struct { GitHash string Version string IP string Port uint DeployPath string Status CompStatus StatusPort uint Labels map[string]string StartTimestamp int64 } type TiKVStoreInfo StoreInfo var _ Info = &TiKVStoreInfo{} func (i *TiKVStoreInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, StatusPort: i.StatusPort, Kind: KindTiKV, }, Version: i.Version, Status: i.Status, } } type TiFlashStoreInfo StoreInfo var _ Info = &TiFlashStoreInfo{} func (i *TiFlashStoreInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, StatusPort: i.StatusPort, Kind: KindTiFlash, }, Version: i.Version, Status: i.Status, } } type TiCDCInfo struct { ClusterName string GitHash string Version string IP string Port uint DeployPath string Status CompStatus StatusPort uint StartTimestamp int64 } var _ Info = &TiCDCInfo{} func (i *TiCDCInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, StatusPort: i.StatusPort, Kind: KindTiCDC, }, Version: i.Version, Status: i.Status, } } type TiProxyInfo struct { GitHash string Version string IP string Port uint DeployPath string Status CompStatus StatusPort uint StartTimestamp int64 } var _ Info = &TiProxyInfo{} func (i *TiProxyInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, StatusPort: i.StatusPort, Kind: KindTiProxy, }, Version: i.Version, Status: i.Status, } } type TSOInfo struct { GitHash string Version string IP string Port uint DeployPath string Status CompStatus StartTimestamp int64 } var _ Info = &TSOInfo{} func (i *TSOInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, Kind: KindTSO, }, Version: i.Version, Status: i.Status, } } type SchedulingInfo struct { GitHash string Version string IP string Port uint DeployPath string Status CompStatus StartTimestamp int64 } var _ Info = &SchedulingInfo{} func (i *SchedulingInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, Kind: KindScheduling, }, Version: i.Version, Status: i.Status, } } type StandardDeployInfo struct { IP string Port uint } type AlertManagerInfo StandardDeployInfo var _ Info = &AlertManagerInfo{} func (i *AlertManagerInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, Kind: KindAlertManager, }, Version: "", Status: CompStatusUnknown, } } type GrafanaInfo StandardDeployInfo var _ Info = &GrafanaInfo{} func (i *GrafanaInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, Kind: KindGrafana, }, Version: "", Status: CompStatusUnknown, } } type PrometheusInfo StandardDeployInfo var _ Info = &PrometheusInfo{} func (i *PrometheusInfo) Info() CompInfo { return CompInfo{ CompDescriptor: CompDescriptor{ IP: i.IP, Port: i.Port, Kind: KindPrometheus, }, Version: "", Status: CompStatusUnknown, } } ================================================ FILE: util/topo/model_desc.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topo // CompDescriptor (Component Desc) is a unique identifier for a component. // It is secure to be persisted, but is not secure to be accepted from the user input. // To securely accept a Comp from user input, see SignedCompDescriptor. type CompDescriptor struct { IP string Port uint StatusPort uint Kind Kind // WARN: Extreme care should be taken when adding more fields here, // as this struct is widely used or persisted. } ================================================ FILE: util/topo/model_info.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topo import ( "context" "fmt" ) // CompInfo provides common information for a component. // It must not be persisted, as the runtime status may change at any time. // It must not be accepted directly from the user input. See SignedCompDescriptor. // The contained descriptor is unsigned, which means it may not be very useful to be passed to users. // Call WithSignature() if you want to pass to users. type CompInfo struct { CompDescriptor Version string Status CompStatus } // Info is an interface implemented by all component info structures. type Info interface { Info() CompInfo } func GetInfoByKind(ctx context.Context, p TopologyProvider, kind Kind) ([]CompInfo, error) { switch kind { case KindTiDB: v, err := p.GetTiDB(ctx) if err != nil { return nil, err } result := make([]CompInfo, 0, len(v)) for _, info := range v { result = append(result, info.Info()) } return result, nil case KindTiKV: v, err := p.GetTiKV(ctx) if err != nil { return nil, err } result := make([]CompInfo, 0, len(v)) for _, info := range v { result = append(result, info.Info()) } return result, nil case KindPD: v, err := p.GetPD(ctx) if err != nil { return nil, err } result := make([]CompInfo, 0, len(v)) for _, info := range v { result = append(result, info.Info()) } return result, nil case KindTiFlash: v, err := p.GetTiFlash(ctx) if err != nil { return nil, err } result := make([]CompInfo, 0, len(v)) for _, info := range v { result = append(result, info.Info()) } return result, nil case KindAlertManager: v, err := p.GetAlertManager(ctx) if err != nil { return nil, err } return []CompInfo{v.Info()}, nil case KindGrafana: v, err := p.GetGrafana(ctx) if err != nil { return nil, err } return []CompInfo{v.Info()}, nil case KindPrometheus: v, err := p.GetPrometheus(ctx) if err != nil { return nil, err } return []CompInfo{v.Info()}, nil default: return nil, fmt.Errorf("unsupported component %s", kind) } } ================================================ FILE: util/topo/pdtopo/1_main_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdtopo_test import ( "testing" "github.com/pingcap/tidb-dashboard/util/testutil/testdefault" ) func TestMain(m *testing.M) { testdefault.TestMain(m) } ================================================ FILE: util/topo/pdtopo/monitor.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdtopo import ( "context" clientv3 "go.etcd.io/etcd/client/v3" "github.com/pingcap/tidb-dashboard/util/topo" ) func GetAlertManagerInstance(ctx context.Context, etcdClient *clientv3.Client) (*topo.AlertManagerInfo, error) { i, err := fetchStandardComponentTopology(ctx, "alertmanager", etcdClient) if err != nil { return nil, err } if i == nil { return nil, nil } return (*topo.AlertManagerInfo)(i), nil } func GetGrafanaInstance(ctx context.Context, etcdClient *clientv3.Client) (*topo.GrafanaInfo, error) { i, err := fetchStandardComponentTopology(ctx, "grafana", etcdClient) if err != nil { return nil, err } if i == nil { return nil, nil } return (*topo.GrafanaInfo)(i), nil } func GetPrometheusInstance(ctx context.Context, etcdClient *clientv3.Client) (*topo.PrometheusInfo, error) { i, err := fetchStandardComponentTopology(ctx, "prometheus", etcdClient) if err != nil { return nil, err } if i == nil { return nil, nil } return (*topo.PrometheusInfo)(i), nil } ================================================ FILE: util/topo/pdtopo/pd.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdtopo import ( "context" "sort" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/client/pdclient" "github.com/pingcap/tidb-dashboard/util/distro" "github.com/pingcap/tidb-dashboard/util/netutil" "github.com/pingcap/tidb-dashboard/util/topo" ) func GetPDInstances(ctx context.Context, pdAPI *pdclient.APIClient) ([]topo.PDInfo, error) { ds, err := pdAPI.GetMembers(ctx) if err != nil { return nil, err } health, err := pdAPI.GetHealth(ctx) if err != nil { return nil, err } healthMap := map[uint64]struct{}{} for _, v := range *health { if v.Health { healthMap[v.MemberID] = struct{}{} } } nodes := make([]topo.PDInfo, 0) for _, ds := range ds.Members { u := ds.ClientUrls[0] hostname, port, err := netutil.ParseHostAndPortFromAddressURL(u) if err != nil { continue } tsResp, err := pdAPI.GetStatus(ctx) if err != nil { log.Warn("Failed to fetch start timestamp", zap.String("component", distro.R().PD), zap.String("targetNode", u), zap.Error(err)) tsResp = &pdclient.GetStatusResponse{} } storeStatus := topo.CompStatusUnreachable if _, ok := healthMap[ds.MemberID]; ok { storeStatus = topo.CompStatusUp } nodes = append(nodes, topo.PDInfo{ GitHash: ds.GitHash, Version: ds.BinaryVersion, IP: hostname, Port: port, DeployPath: ds.DeployPath, Status: storeStatus, StartTimestamp: tsResp.StartTimestamp, }) } sort.Slice(nodes, func(i, j int) bool { if nodes[i].IP < nodes[j].IP { return true } if nodes[i].IP > nodes[j].IP { return false } return nodes[i].Port < nodes[j].Port }) return nodes, nil } ================================================ FILE: util/topo/pdtopo/pd_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdtopo_test import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/client/pdclient/fixture" "github.com/pingcap/tidb-dashboard/util/topo" "github.com/pingcap/tidb-dashboard/util/topo/pdtopo" ) func TestGetPDInstances(t *testing.T) { apiClient := fixture.NewAPIClientFixture() resp, err := pdtopo.GetPDInstances(context.Background(), apiClient) require.NoError(t, err) require.Equal(t, []topo.PDInfo{ { GitHash: "0c1246dd219fd16b4b2ff5108941e5d3e958922d", Version: "v4.0.14", IP: "172.16.6.169", Port: 2379, DeployPath: "/home/tidb/tidb-deploy/pd-2379/bin", Status: topo.CompStatusUp, StartTimestamp: 1635762685, }, { GitHash: "0c1246dd219fd16b4b2ff5108941e5d3e958922d", Version: "v4.0.14", IP: "172.16.6.170", Port: 2379, DeployPath: "/home/tidb/tidb-deploy/pd-2379/bin", Status: topo.CompStatusUp, StartTimestamp: 1635762685, }, { GitHash: "0c1246dd219fd16b4b2ff5108941e5d3e958922d", Version: "v4.0.14", IP: "172.16.6.171", Port: 2379, DeployPath: "/home/tidb/tidb-deploy/pd-2379/bin", Status: topo.CompStatusUp, StartTimestamp: 1635762685, }, }, resp) } ================================================ FILE: util/topo/pdtopo/provider_pd.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdtopo import ( "context" clientv3 "go.etcd.io/etcd/client/v3" "github.com/pingcap/tidb-dashboard/util/client/pdclient" "github.com/pingcap/tidb-dashboard/util/topo" ) // TopologyFromPD provides the topology information from PD. type TopologyFromPD struct { etcdClient *clientv3.Client pdAPI *pdclient.APIClient } var _ topo.TopologyProvider = (*TopologyFromPD)(nil) // NewTopologyProviderFromPD creates a provider that gets topology information from PD. // The base URL of the PD API client must be correctly set. func NewTopologyProviderFromPD(etcdClient *clientv3.Client, pdAPI *pdclient.APIClient) topo.TopologyProvider { return &TopologyFromPD{ etcdClient: etcdClient, pdAPI: pdAPI, } } func (p *TopologyFromPD) GetPD(ctx context.Context) ([]topo.PDInfo, error) { return GetPDInstances(ctx, p.pdAPI) } func (p *TopologyFromPD) GetTiDB(ctx context.Context) ([]topo.TiDBInfo, error) { return GetTiDBInstances(ctx, p.etcdClient) } func (p *TopologyFromPD) GetTiKV(ctx context.Context) ([]topo.TiKVStoreInfo, error) { tikvStores, _, err := GetStoreInstances(ctx, p.pdAPI) if err != nil { return nil, err } return tikvStores, nil } func (p *TopologyFromPD) GetTiFlash(ctx context.Context) ([]topo.TiFlashStoreInfo, error) { _, tiFlashStores, err := GetStoreInstances(ctx, p.pdAPI) if err != nil { return nil, err } return tiFlashStores, nil } func (p *TopologyFromPD) GetPrometheus(ctx context.Context) (*topo.PrometheusInfo, error) { return GetPrometheusInstance(ctx, p.etcdClient) } func (p *TopologyFromPD) GetGrafana(ctx context.Context) (*topo.GrafanaInfo, error) { return GetGrafanaInstance(ctx, p.etcdClient) } func (p *TopologyFromPD) GetAlertManager(ctx context.Context) (*topo.AlertManagerInfo, error) { return GetAlertManagerInstance(ctx, p.etcdClient) } ================================================ FILE: util/topo/pdtopo/std_comp.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdtopo import ( "context" "encoding/json" "github.com/joomcode/errorx" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/topo" ) var ( ErrNS = errorx.NewNamespace("topo.pd") ErrEtcdRequestFailed = ErrNS.NewType("etcd_request_failed") ErrInvalidTopologyData = ErrNS.NewType("invalid_topology_data") ) func fetchStandardComponentTopology(ctx context.Context, componentName string, etcdClient *clientv3.Client) (*topo.StandardDeployInfo, error) { key := "/topology/" + componentName resp, err := etcdClient.Get(ctx, key, clientv3.WithPrefix()) if err != nil { return nil, ErrEtcdRequestFailed.Wrap(err, "Failed to read topology from etcd key `%s`", key) } if resp.Count == 0 { return nil, nil } info := topo.StandardDeployInfo{} kv := resp.Kvs[0] if err = json.Unmarshal(kv.Value, &info); err != nil { log.Warn("Failed to unmarshal topology value", zap.String("key", string(kv.Key)), zap.String("value", string(kv.Value))) return nil, nil } return &info, nil } ================================================ FILE: util/topo/pdtopo/store.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdtopo import ( "context" "strings" "github.com/pingcap/log" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/client/pdclient" "github.com/pingcap/tidb-dashboard/util/netutil" "github.com/pingcap/tidb-dashboard/util/topo" ) // GetStoreInstances returns TiKV info and TiFlash info. func GetStoreInstances(ctx context.Context, pdAPI *pdclient.APIClient) ([]topo.TiKVStoreInfo, []topo.TiFlashStoreInfo, error) { stores, err := pdAPI.HLGetStores(ctx) if err != nil { return nil, nil, err } tiKVStores := make([]pdclient.GetStoresResponseStore, 0, len(stores)) tiFlashStores := make([]pdclient.GetStoresResponseStore, 0, len(stores)) for _, store := range stores { isTiFlash := false for _, label := range store.Labels { if label.Key == "engine" && label.Value == "tiflash" { isTiFlash = true } } if isTiFlash { tiFlashStores = append(tiFlashStores, store) } else { tiKVStores = append(tiKVStores, store) } } siTiKV := buildStoreTopology(tiKVStores) storesTiKV := make([]topo.TiKVStoreInfo, 0, len(siTiKV)) for _, si := range siTiKV { storesTiKV = append(storesTiKV, topo.TiKVStoreInfo(si)) } siTiFlash := buildStoreTopology(tiFlashStores) storesTiFlash := make([]topo.TiFlashStoreInfo, 0, len(siTiFlash)) for _, si := range siTiFlash { storesTiFlash = append(storesTiFlash, topo.TiFlashStoreInfo(si)) } return storesTiKV, storesTiFlash, nil } func buildStoreTopology(stores []pdclient.GetStoresResponseStore) []topo.StoreInfo { nodes := make([]topo.StoreInfo, 0, len(stores)) for _, v := range stores { hostname, port, err := netutil.ParseHostAndPortFromAddress(v.Address) if err != nil { log.Warn("Failed to parse store address", zap.Any("store", v)) continue } _, statusPort, err := netutil.ParseHostAndPortFromAddress(v.StatusAddress) if err != nil { log.Warn("Failed to parse store status address", zap.Any("store", v)) continue } // In current TiKV, it's version may not start with 'v', // so we may need to add a prefix 'v' for it. version := strings.Trim(v.Version, "\n ") if !strings.HasPrefix(version, "v") { version = "v" + version } node := topo.StoreInfo{ Version: version, IP: hostname, Port: port, GitHash: v.GitHash, DeployPath: v.DeployPath, Status: parseStoreState(v.StateName), StatusPort: statusPort, Labels: map[string]string{}, StartTimestamp: v.StartTimestamp, } for _, v := range v.Labels { node.Labels[v.Key] = v.Value } nodes = append(nodes, node) } return nodes } func parseStoreState(state string) topo.CompStatus { state = strings.Trim(strings.ToLower(state), "\n ") switch state { case "up": return topo.CompStatusUp case "tombstone": return topo.CompStatusTombstone case "offline": return topo.CompStatusLeaving case "down": return topo.CompStatusDown case "disconnected": return topo.CompStatusUnreachable default: return topo.CompStatusUnreachable } } ================================================ FILE: util/topo/pdtopo/store_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdtopo_test import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/pingcap/tidb-dashboard/util/client/pdclient/fixture" "github.com/pingcap/tidb-dashboard/util/topo" "github.com/pingcap/tidb-dashboard/util/topo/pdtopo" ) func TestGetStoreInstances(t *testing.T) { apiClient := fixture.NewAPIClientFixture() tiKvStores, tiFlashStores, err := pdtopo.GetStoreInstances(context.Background(), apiClient) require.NoError(t, err) require.Equal(t, []topo.TiKVStoreInfo{ { GitHash: "d7dc4fff51ca71c76a928a0780a069efaaeaae70", Version: "v4.0.14", IP: "172.16.5.141", Port: 20160, DeployPath: "/home/tidb/tidb-deploy/tikv-20160/bin", Status: topo.CompStatusUp, StatusPort: 20180, Labels: map[string]string{}, StartTimestamp: 1636421301, }, { GitHash: "d7dc4fff51ca71c76a928a0780a069efaaeaae70", Version: "v4.0.14", IP: "172.16.5.218", Port: 20160, DeployPath: "/home/tidb/tidb-deploy/tikv-20160/bin", Status: topo.CompStatusUp, StatusPort: 20180, Labels: map[string]string{}, StartTimestamp: 1636421304, }, { GitHash: "d7dc4fff51ca71c76a928a0780a069efaaeaae70", Version: "v4.0.14", IP: "172.16.6.168", Port: 20160, DeployPath: "/home/tidb/tidb-deploy/tikv-20160/bin", Status: topo.CompStatusUp, StatusPort: 20180, Labels: map[string]string{}, StartTimestamp: 1636421304, }, }, tiKvStores) require.Equal(t, []topo.TiFlashStoreInfo{}, tiFlashStores) } ================================================ FILE: util/topo/pdtopo/tidb.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package pdtopo import ( "context" "encoding/json" "sort" "strconv" "strings" "time" "github.com/pingcap/log" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" "github.com/pingcap/tidb-dashboard/util/distro" "github.com/pingcap/tidb-dashboard/util/netutil" "github.com/pingcap/tidb-dashboard/util/topo" ) const tidbTopologyKeyPrefix = "/topology/tidb/" func GetTiDBInstances(ctx context.Context, etcdClient *clientv3.Client) ([]topo.TiDBInfo, error) { resp, err := etcdClient.Get(ctx, tidbTopologyKeyPrefix, clientv3.WithPrefix()) if err != nil { return nil, ErrEtcdRequestFailed.Wrap(err, "Failed to read topology from etcd key `%s`", tidbTopologyKeyPrefix) } nodesAlive := make(map[string]struct{}, len(resp.Kvs)) nodesInfo := make(map[string]*topo.TiDBInfo, len(resp.Kvs)) for _, kv := range resp.Kvs { key := string(kv.Key) if !strings.HasPrefix(key, tidbTopologyKeyPrefix) { continue } // remainingKey looks like `ip:port/info` or `ip:port/ttl`. remainingKey := key[len(tidbTopologyKeyPrefix):] keyParts := strings.Split(remainingKey, "/") if len(keyParts) != 2 { log.Warn("Ignored invalid topology key", zap.String("component", distro.R().TiDB), zap.String("key", key)) continue } switch keyParts[1] { case "info": node, err := parseTiDBInfo(keyParts[0], kv.Value) if err == nil { nodesInfo[keyParts[0]] = node } else { log.Warn("Ignored invalid topology info entry", zap.String("component", distro.R().TiDB), zap.String("key", key), zap.String("value", string(kv.Value)), zap.Error(err)) } case "ttl": alive, err := parseTiDBAliveness(kv.Value) if err == nil { nodesAlive[keyParts[0]] = struct{}{} if !alive { log.Warn("Component alive TTL has expired (maybe local time are not synchronized)", zap.String("component", distro.R().TiDB), zap.String("key", key), zap.String("value", string(kv.Value))) } } else { log.Warn("Ignored invalid topology TTL entry", zap.String("component", distro.R().TiDB), zap.String("key", key), zap.String("value", string(kv.Value)), zap.Error(err)) } } } nodes := make([]topo.TiDBInfo, 0) for addr, info := range nodesInfo { if _, ok := nodesAlive[addr]; ok { info.Status = topo.CompStatusUp } nodes = append(nodes, *info) } sort.Slice(nodes, func(i, j int) bool { if nodes[i].IP < nodes[j].IP { return true } if nodes[i].IP > nodes[j].IP { return false } return nodes[i].Port < nodes[j].Port }) return nodes, nil } func parseTiDBInfo(address string, value []byte) (*topo.TiDBInfo, error) { ds := struct { Version string `json:"version"` GitHash string `json:"git_hash"` StatusPort uint `json:"status_port"` DeployPath string `json:"deploy_path"` StartTimestamp int64 `json:"start_timestamp"` }{} err := json.Unmarshal(value, &ds) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "Read topology value failed") } hostname, port, err := netutil.ParseHostAndPortFromAddress(address) if err != nil { return nil, ErrInvalidTopologyData.Wrap(err, "Read topology address failed") } return &topo.TiDBInfo{ GitHash: ds.GitHash, Version: ds.Version, IP: hostname, Port: port, DeployPath: ds.DeployPath, Status: topo.CompStatusUnreachable, StatusPort: ds.StatusPort, StartTimestamp: ds.StartTimestamp, }, nil } func parseTiDBAliveness(value []byte) (bool, error) { unixTimestampNano, err := strconv.ParseUint(string(value), 10, 64) if err != nil { return false, ErrInvalidTopologyData.Wrap(err, "Parse topology TTL info failed") } t := time.Unix(0, int64(unixTimestampNano)) if time.Since(t) > time.Second*45 { return false, nil } return true, nil } ================================================ FILE: util/topo/provider.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topo import ( "context" ) // TopologyProvider provides the topology information for different components. type TopologyProvider interface { GetPD(ctx context.Context) ([]PDInfo, error) GetTiDB(ctx context.Context) ([]TiDBInfo, error) GetTiKV(ctx context.Context) ([]TiKVStoreInfo, error) GetTiFlash(ctx context.Context) ([]TiFlashStoreInfo, error) GetPrometheus(ctx context.Context) (*PrometheusInfo, error) GetGrafana(ctx context.Context) (*GrafanaInfo, error) GetAlertManager(ctx context.Context) (*AlertManagerInfo, error) } //go:generate mockery --name TopologyProvider --inpackage var _ TopologyProvider = (*MockTopologyProvider)(nil) ================================================ FILE: util/topo/provider_cached.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topo import ( "context" "runtime" "time" "github.com/ReneKroon/ttlcache/v2" ) // CachedTopology provides topology over an underlying topology provider with a TTL cache. // This struct is concurrent-safe. type CachedTopology struct { p TopologyProvider cache *ttlcache.Cache } var _ TopologyProvider = (*CachedTopology)(nil) func NewCachedTopology(p TopologyProvider, ttl time.Duration) TopologyProvider { cache := ttlcache.NewCache() cache.SkipTTLExtensionOnHit(true) _ = cache.SetTTL(ttl) ct := &CachedTopology{ p: p, cache: cache, } // Destroy the internal cache goroutine when no one is referencing this struct anymore. runtime.SetFinalizer(ct, (*CachedTopology).finalize) return ct } func (c *CachedTopology) getOrFillCache(key string, backSource func() (interface{}, error)) (interface{}, error) { if data, err := c.cache.Get(key); err == nil { return data, nil } // TODO: use singleflight. src, err := backSource() if err != nil { // Error is never cached. return nil, err } _ = c.cache.Set(key, src) runtime.KeepAlive(c) return src, nil } func (c *CachedTopology) GetPD(ctx context.Context) ([]PDInfo, error) { v, err := c.getOrFillCache("pd", func() (interface{}, error) { return c.p.GetPD(ctx) }) if err != nil { return nil, err } return v.([]PDInfo), nil } func (c *CachedTopology) GetTiDB(ctx context.Context) ([]TiDBInfo, error) { v, err := c.getOrFillCache("tidb", func() (interface{}, error) { return c.p.GetTiDB(ctx) }) if err != nil { return nil, err } return v.([]TiDBInfo), nil } func (c *CachedTopology) GetTiKV(ctx context.Context) ([]TiKVStoreInfo, error) { v, err := c.getOrFillCache("tikv", func() (interface{}, error) { return c.p.GetTiKV(ctx) }) if err != nil { return nil, err } return v.([]TiKVStoreInfo), nil } func (c *CachedTopology) GetTiFlash(ctx context.Context) ([]TiFlashStoreInfo, error) { v, err := c.getOrFillCache("tiflash", func() (interface{}, error) { return c.p.GetTiFlash(ctx) }) if err != nil { return nil, err } return v.([]TiFlashStoreInfo), nil } func (c *CachedTopology) GetPrometheus(ctx context.Context) (*PrometheusInfo, error) { v, err := c.getOrFillCache("prometheus", func() (interface{}, error) { return c.p.GetPrometheus(ctx) }) if err != nil { return nil, err } return v.(*PrometheusInfo), nil } func (c *CachedTopology) GetGrafana(ctx context.Context) (*GrafanaInfo, error) { v, err := c.getOrFillCache("grafana", func() (interface{}, error) { return c.p.GetGrafana(ctx) }) if err != nil { return nil, err } return v.(*GrafanaInfo), nil } func (c *CachedTopology) GetAlertManager(ctx context.Context) (*AlertManagerInfo, error) { v, err := c.getOrFillCache("alert_manager", func() (interface{}, error) { return c.p.GetAlertManager(ctx) }) if err != nil { return nil, err } return v.(*AlertManagerInfo), nil } func (c *CachedTopology) finalize() { _ = c.cache.Close() } ================================================ FILE: util/topo/provider_cached_test.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package topo import ( "context" "fmt" "sync" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) func TestCachedTopologyCacheValue(t *testing.T) { type mockKey string const mockKey1 = mockKey("useMock1") const mockKey2 = mockKey("useMock2") mp := new(MockTopologyProvider) mp. On("GetPrometheus", mock.MatchedBy(func(ctx context.Context) bool { ctxV := ctx.Value(mockKey1) return ctxV != nil && ctxV.(bool) == true })). Return(&PrometheusInfo{ IP: "192.168.35.10", Port: 1234, }, nil). On("GetPrometheus", mock.MatchedBy(func(ctx context.Context) bool { ctxV := ctx.Value(mockKey2) return ctxV != nil && ctxV.(bool) == true })). Return(&PrometheusInfo{ IP: "192.168.100.5", Port: 5414, }, nil). On("GetPrometheus", mock.Anything). Return((*PrometheusInfo)(nil), fmt.Errorf("some error")) cp := NewCachedTopology(mp, time.Millisecond*500) // Error response should not be cached v, err := cp.GetPrometheus(context.Background()) require.Error(t, err) require.Equal(t, err.Error(), "some error") require.Nil(t, v) mp.AssertNumberOfCalls(t, "GetPrometheus", 1) v, err = cp.GetPrometheus(context.Background()) require.Error(t, err) require.Equal(t, err.Error(), "some error") require.Nil(t, v) mp.AssertNumberOfCalls(t, "GetPrometheus", 2) // Non error response should be cached v, err = cp.GetPrometheus(context.WithValue(context.Background(), mockKey1, true)) require.NoError(t, err) require.Equal(t, "192.168.35.10", v.IP) require.Equal(t, uint(1234), v.Port) mp.AssertNumberOfCalls(t, "GetPrometheus", 3) v, err = cp.GetPrometheus(context.WithValue(context.Background(), mockKey1, true)) require.NoError(t, err) require.Equal(t, "192.168.35.10", v.IP) require.Equal(t, uint(1234), v.Port) mp.AssertNumberOfCalls(t, "GetPrometheus", 3) v, err = cp.GetPrometheus(context.WithValue(context.Background(), mockKey2, true)) require.NoError(t, err) require.Equal(t, "192.168.35.10", v.IP) // Unchanged since it is cached require.Equal(t, uint(1234), v.Port) mp.AssertNumberOfCalls(t, "GetPrometheus", 3) // Wait until expired time.Sleep(time.Millisecond * 550) v, err = cp.GetPrometheus(context.WithValue(context.Background(), mockKey2, true)) require.NoError(t, err) require.Equal(t, "192.168.100.5", v.IP) require.Equal(t, uint(5414), v.Port) mp.AssertNumberOfCalls(t, "GetPrometheus", 4) v, err = cp.GetPrometheus(context.Background()) require.NoError(t, err) require.Equal(t, "192.168.100.5", v.IP) require.Equal(t, uint(5414), v.Port) mp.AssertNumberOfCalls(t, "GetPrometheus", 4) // Wait until expired time.Sleep(time.Millisecond * 550) v, err = cp.GetPrometheus(context.Background()) require.Error(t, err) require.Equal(t, err.Error(), "some error") require.Nil(t, v) mp.AssertNumberOfCalls(t, "GetPrometheus", 5) v, err = cp.GetPrometheus(context.Background()) require.Error(t, err) require.Equal(t, err.Error(), "some error") require.Nil(t, v) mp.AssertNumberOfCalls(t, "GetPrometheus", 6) v, err = cp.GetPrometheus(context.WithValue(context.Background(), mockKey1, true)) require.NoError(t, err) require.Equal(t, "192.168.35.10", v.IP) require.Equal(t, uint(1234), v.Port) mp.AssertNumberOfCalls(t, "GetPrometheus", 7) v, err = cp.GetPrometheus(context.Background()) require.NoError(t, err) require.Equal(t, "192.168.35.10", v.IP) require.Equal(t, uint(1234), v.Port) mp.AssertNumberOfCalls(t, "GetPrometheus", 7) // Read should not extend TTL time.Sleep(time.Millisecond * 550) tBegin := time.Now() v, err = cp.GetPrometheus(context.WithValue(context.Background(), mockKey1, true)) require.NoError(t, err) require.Equal(t, "192.168.35.10", v.IP) require.Equal(t, uint(1234), v.Port) mp.AssertNumberOfCalls(t, "GetPrometheus", 8) time.Sleep(time.Millisecond * 400) v, err = cp.GetPrometheus(context.Background()) require.NoError(t, err) require.Equal(t, "192.168.35.10", v.IP) require.Equal(t, uint(1234), v.Port) mp.AssertNumberOfCalls(t, "GetPrometheus", 8) time.Sleep(time.Millisecond * 150) // 550ms has passed since first put, so we should expect cache to expire v, err = cp.GetPrometheus(context.Background()) require.Error(t, err) require.Equal(t, err.Error(), "some error") require.Nil(t, v) mp.AssertNumberOfCalls(t, "GetPrometheus", 9) // Let's see that the expiration is not caused by 400ms+500ms. require.True(t, tBegin.Add(time.Millisecond*800).After(time.Now())) mp.AssertExpectations(t) } func TestCachedTopologyCacheNil(t *testing.T) { // No prometheus exists. mp := new(MockTopologyProvider) mp. On("GetPrometheus", mock.Anything). Return((*PrometheusInfo)(nil), nil) cp := NewCachedTopology(mp, time.Millisecond*500) v, err := cp.GetPrometheus(context.Background()) require.NoError(t, err) require.Nil(t, v) mp.AssertNumberOfCalls(t, "GetPrometheus", 1) // Nil (but success) result is cached. v, err = cp.GetPrometheus(context.Background()) require.NoError(t, err) require.Nil(t, v) mp.AssertNumberOfCalls(t, "GetPrometheus", 1) mp.AssertExpectations(t) } func TestCachedTopologyConcurrentGet(t *testing.T) { mp := new(MockTopologyProvider) mp. On("GetPrometheus", mock.Anything). After(time.Second). Return((*PrometheusInfo)(nil), nil) cp := NewCachedTopology(mp, time.Millisecond*500) var wg sync.WaitGroup for range 5 { wg.Go(func() { v, err := cp.GetPrometheus(context.Background()) require.NoError(t, err) require.Nil(t, v) }) } wg.Wait() // There is no singleflight behavior. mp.AssertNumberOfCalls(t, "GetPrometheus", 5) v, err := cp.GetPrometheus(context.Background()) require.NoError(t, err) require.Nil(t, v) mp.AssertNumberOfCalls(t, "GetPrometheus", 5) mp.AssertExpectations(t) } func TestCachedTopologyAllMethods(t *testing.T) { // Hopefully we can find cache key is not mixed via this test. mp := new(MockTopologyProvider) mp. On("GetPD", mock.Anything).Return([]PDInfo{{IP: "addr-pd.internal"}}, nil). On("GetTiDB", mock.Anything).Return([]TiDBInfo{{IP: "addr-tidb-2.internal"}, {IP: "addr-tidb-1.internal"}}, nil). On("GetTiKV", mock.Anything).Return([]TiKVStoreInfo{{IP: "addr-tikv-3.internal"}}, nil). On("GetTiFlash", mock.Anything).Return([]TiFlashStoreInfo{}, nil). On("GetPrometheus", mock.Anything).Return(nil, nil). On("GetGrafana", mock.Anything).Return(&GrafanaInfo{IP: "addr-grafana.internal"}, nil). On("GetAlertManager", mock.Anything).Return(&AlertManagerInfo{IP: "addr-am-x.internal"}, nil) cp := NewCachedTopology(mp, time.Millisecond*500) { v, err := cp.GetPD(context.Background()) require.NoError(t, err) require.NotNil(t, v) require.Equal(t, 1, len(v)) require.Equal(t, "addr-pd.internal", v[0].IP) } { v, err := cp.GetTiDB(context.Background()) require.NoError(t, err) require.NotNil(t, v) require.Equal(t, 2, len(v)) require.Equal(t, "addr-tidb-2.internal", v[0].IP) require.Equal(t, "addr-tidb-1.internal", v[1].IP) } { v, err := cp.GetTiKV(context.Background()) require.NoError(t, err) require.NotNil(t, v) require.Equal(t, 1, len(v)) require.Equal(t, "addr-tikv-3.internal", v[0].IP) } { v, err := cp.GetTiFlash(context.Background()) require.NoError(t, err) require.NotNil(t, v) require.Equal(t, 0, len(v)) } { v, err := cp.GetPrometheus(context.Background()) require.NoError(t, err) require.Nil(t, v) } { v, err := cp.GetGrafana(context.Background()) require.NoError(t, err) require.NotNil(t, v) require.Equal(t, "addr-grafana.internal", v.IP) } { v, err := cp.GetAlertManager(context.Background()) require.NoError(t, err) require.NotNil(t, v) require.Equal(t, "addr-am-x.internal", v.IP) } mp.AssertExpectations(t) } ================================================ FILE: util/ziputil/zip_writer.go ================================================ // Copyright 2026 PingCAP, Inc. Licensed under Apache-2.0. package ziputil import ( "archive/zip" "io" "os" "path/filepath" "time" ) // WriteZipFromFiles compresses `files` using zip and write the zip in a streaming way to the io Writer `w`. // The files will be flattened in the zip file, i.e. `/a/b/c.txt` becomes `c.txt`. // FIXME: This function does not handle with encrypted files on the disk. func WriteZipFromFiles(w io.Writer, files []string, compress bool) error { zw := zip.NewWriter(w) defer func() { _ = zw.Close() }() // TODO: Handle with duplicate file names. for _, file := range files { err := writeZipFromFile(zw, file, compress) if err != nil { return err } } return nil } func writeZipFromFile(zw *zip.Writer, file string, compress bool) error { f, err := os.Open(filepath.Clean(file)) if err != nil { return err } defer func() { _ = f.Close() }() fileInfo, err := f.Stat() if err != nil { return err } zipMethod := zip.Store // no compress if compress { zipMethod = zip.Deflate // compress } zipFile, err := zw.CreateHeader(&zip.FileHeader{ Name: fileInfo.Name(), Method: zipMethod, Modified: time.Now(), }) if err != nil { return err } _, err = io.Copy(zipFile, f) if err != nil { return err } return nil }