From c51e47044cdd93bf63964285dfb5b427430be802 Mon Sep 17 00:00:00 2001 From: Rohit Nayak Date: Thu, 5 Dec 2024 19:10:14 +0100 Subject: [PATCH 01/15] Remove old comments Signed-off-by: Rohit Nayak --- go/summarize/force-graph.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/go/summarize/force-graph.go b/go/summarize/force-graph.go index 2feec79..2ddd9bf 100644 --- a/go/summarize/force-graph.go +++ b/go/summarize/force-graph.go @@ -232,9 +232,3 @@ func serveIndex(w http.ResponseWriter, data forceGraphData) error { return nil } - -/* -TODO: - - New relationship: FKs - - Different sizes of nodes and links based on table size and relationship occurrences -*/ From 0dda1646b9e71c3ca6f3a677809c205f7b8f1ace Mon Sep 17 00:00:00 2001 From: Rohit Nayak Date: Thu, 5 Dec 2024 23:17:15 +0100 Subject: [PATCH 02/15] Add gin based webserver framework. Currently only showing static pages Signed-off-by: Rohit Nayak --- go.mod | 20 ++++ go.sum | 45 +++++++ go/cmd/root.go | 2 + go/vt/main.go | 16 +++ go/web/templates/about.html | 151 ++++++++++++++++++++++++ go/web/templates/css/styles.css | 83 +++++++++++++ go/web/templates/footer.html | 5 + go/web/templates/header.html | 11 ++ go/web/templates/images/vitess-logo.png | Bin 0 -> 20545 bytes go/web/templates/index.html | 16 +++ go/web/templates/index2.html | 10 ++ go/web/templates/layout.html | 14 +++ go/web/web.go | 129 ++++++++++++++++++++ 13 files changed, 502 insertions(+) create mode 100644 go/web/templates/about.html create mode 100644 go/web/templates/css/styles.css create mode 100644 go/web/templates/footer.html create mode 100644 go/web/templates/header.html create mode 100644 go/web/templates/images/vitess-logo.png create mode 100644 go/web/templates/index.html create mode 100644 go/web/templates/index2.html create mode 100644 go/web/templates/layout.html create mode 100644 go/web/web.go diff --git a/go.mod b/go.mod index a4ef421..f2c9e79 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/alecthomas/chroma v0.10.0 github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.18.0 + github.com/gin-gonic/gin v1.10.0 github.com/jstemmer/go-junit-report/v2 v2.1.0 github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.1 @@ -33,7 +34,11 @@ require ( github.com/armon/go-metrics v0.4.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -41,6 +46,12 @@ require ( github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect github.com/ebitengine/purego v0.8.1 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // 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.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.3 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -65,14 +76,19 @@ require ( github.com/hashicorp/hcl v1.0.1-vault-7 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/pgzip v1.2.6 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/magiconair/properties v1.8.7 // 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.16 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opentracing-contrib/go-grpc v0.1.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect @@ -103,8 +119,10 @@ require ( github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tinylib/msgp v1.2.4 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + github.com/ugorji/go/codec v1.2.12 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/z-division/go-zookeeper v1.0.0 // indirect @@ -115,6 +133,8 @@ require ( go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.31.0 // indirect diff --git a/go.sum b/go.sum index 702c12e..7a3b768 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,10 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -55,6 +59,10 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= @@ -90,17 +98,33 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -199,6 +223,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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/v2 v2.1.0 h1:X3+hPYlSczH9IMIpSC9CQSZA0L+BipYafciZUWHEmsc= github.com/jstemmer/go-junit-report/v2 v2.1.0/go.mod h1:mgHVr7VUo5Tn8OLVr1cKnLuEy0M92wdRntM99h7RkgQ= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -206,8 +232,12 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -217,6 +247,8 @@ 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/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -249,9 +281,12 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F 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-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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -379,10 +414,14 @@ github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU= github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +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/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= @@ -411,6 +450,9 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= @@ -484,6 +526,7 @@ golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -572,6 +615,8 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= vitess.io/vitess v0.10.3-0.20241204125051-ab7b5169eb79 h1:RjMt2HXW71yyMEXoOY131LAC5SP1HZuz7z6hYa9imsI= diff --git a/go/cmd/root.go b/go/cmd/root.go index 29d6190..1d2b377 100644 --- a/go/cmd/root.go +++ b/go/cmd/root.go @@ -17,6 +17,7 @@ limitations under the License. package cmd import ( + "fmt" "os" "github.com/spf13/cobra" @@ -43,6 +44,7 @@ func Execute() { err := root.Execute() if err != nil { + fmt.Printf("Error: %v\n", err) os.Exit(1) } } diff --git a/go/vt/main.go b/go/vt/main.go index 31349bf..f72f6f4 100644 --- a/go/vt/main.go +++ b/go/vt/main.go @@ -17,9 +17,25 @@ limitations under the License. package main import ( + "os" + "github.com/vitessio/vt/go/cmd" + web2 "github.com/vitessio/vt/go/web" ) func main() { + ch := make(chan int, 2) + launchWebServer(ch) cmd.Execute() + if os.WriteFile("/dev/stderr", []byte("Command executed, web server is still running, use Ctrl-C to exit\n"), 0o600) != nil { + panic("Failed to write to /dev/stderr") + } + <-ch +} + +func launchWebServer(ch chan int) { + go func() { + web2.Run() + ch <- 1 + }() } diff --git a/go/web/templates/about.html b/go/web/templates/about.html new file mode 100644 index 0000000..6e5e953 --- /dev/null +++ b/go/web/templates/about.html @@ -0,0 +1,151 @@ +{{define "content"}} + +

VT Utilities

+

The vt binary encapsulates several utility tools for Vitess, providing a comprehensive suite for + testing, summarizing, and query analysis.

+ +

Tools Included

+
    +
  • vt test: A testing utility using the same test files as the MySQL Test Framework. It compares the + results of identical queries executed on both MySQL and Vitess (vtgate), helping to ensure compatibility. +
  • +
  • vt keys: A utility that analyzes query logs and provides information about + queries, tables, joins, and column usage. +
  • +
  • vt transactions: A tool that analyzes query logs to identify transaction patterns + and outputs a JSON report detailing these patterns. +
  • +
  • vt trace: A tool that generates execution traces for queries without comparing + against MySQL. It helps analyze query behavior and performance in Vitess environments. +
  • +
  • vt summarize: A tool used to summarize or compare trace logs or key logs for + easier human consumption. +
  • +
  • vt dbinfo: A tool that provides information about the database schema, including + row counts, useful column attributes and relevant subset of global variables. +
  • +
+ +

Installation

+

You can install vt using the following command:

+
go install github.com/vitessio/vt/go/vt@latest
+ +

Testing Methodology

+

To verify compatibility and correctness, the testing strategy involves running identical queries on both MySQL and + vtgate, followed by a comparison of results. The process includes:

+
    +
  1. Query Execution: Each test query is executed on both MySQL and vtgate.
  2. +
  3. Result Comparison: The returned data, result set structure (column types, order), and errors + are compared. +
  4. +
  5. Error Handling: Any errors are checked to ensure vtgate produces the same error types as MySQL. +
  6. +
+

This dual-testing strategy ensures high confidence in vtgate's compatibility with MySQL.

+ +

Sharded Testing Strategy

+

Vitess operates in a sharded environment, presenting unique challenges, especially during schema changes (DDL). The + vt test tool handles these by converting DDL statements into VSchema commands.

+

Here's an example of running vt test:

+
vt test --sharded t/basic.test  # Runs tests on a sharded database
+

Custom schemas and configurations can be applied using directives. Run vt test --help, and check out + directives.test for more examples.

+ +

Tracing and Query Analysis

+

Comparative Tracing with vt test

+

vt test can generate traces while comparing behavior with MySQL using the --trace-file + flag:

+
vt test --sharded --trace-file=trace-log.json t/tpch.test
+ +

Standalone Tracing with vt trace

+

vt trace focuses solely on analyzing query execution in Vitess without MySQL comparison:

+
# With VSchema and backup initialization
+vt trace --vschema=t/vschema.json --backup-path=/path/to/backup --number-of-shards=4 t/tpch.test > trace-log.json
+

vt trace accepts most of the same configuration flags as vt test, including:

+
    +
  • --sharded: Enable auto-sharded mode - uses primary keys as sharding keys. Not a good idea for a + production environment, but can be used to ensure that all queries work in a sharded environment. +
  • +
  • --vschema: Specify the VSchema configuration
  • +
  • --backup-path: Initialize from a backup
  • +
  • --number-of-shards: Specify the number of shards to bring up
  • +
  • Other database configuration flags
  • +
+

Both vt trace and vt keys support different input file formats through the --input-type + flag:

+

Example using different input types:

+
# Analyze SQL file or slow query log
+vt trace slow-query.log > trace-log.json
+
+# Analyze MySQL general query log
+vt trace --input-type=mysql-log general-query.log > trace-log.json
+
+# Analyze VTGate query log
+vt trace --input-type=vtgate-log vtgate-querylog.log > trace-log.json
+

Both types of trace logs can be analyzed using vt summarize:

+
vt summarize trace-log.json  # Summarize a single trace
+vt summarize trace-log1.json trace-log2.json  # Compare two traces
+ +

Key Analysis Workflow

+

vt keys analyzes query logs and outputs detailed information about tables, column usage, and joins in + queries. This data can be summarized using vt summarize.

+

Here's a typical workflow:

+
    +
  1. Run vt keys to analyze queries:
  2. +
+
# Analyze an SQL file or slow query log
+vt keys slow-query.log > keys-log.json
+
+# Analyze a MySQL general query log
+vt keys --input-type=mysql-log general-query.log > keys-log.json
+
+# Analyze VTGate query log
+vt trace --input-type=vtgate-log vtgate-querylog.log > trace-log.json
+

This command generates a keys-log.json file that contains a detailed analysis of table and column usage + from the queries.

+
    +
  1. Summarize the keys-log using vt summarize:
  2. +
+
vt summarize keys-log.json
+

This command summarizes the key analysis, providing insight into which tables and columns are used across queries, + and how frequently they are involved in filters, groupings, and joins.

+

If you have access to the running database, you can use vt dbinfo > dbinfo.json and pass it to summarize + so that the analysis can take into account the additional information from the database schema and configuration: +

+
vt summarize keys-log.json dbinfo.json
+ +

Transaction Analysis with vt transactions

+

The vt transactions command is designed to analyze query logs and identify patterns of transactional + queries. It processes the logs to find sequences of queries that form transactions and outputs a JSON report + summarizing these patterns. Read more about how to use and how to read the output in the vt transactions documentation.

+ +

Using --backup-path Flag

+

The --backup-path flag allows vt test and vt trace to initialize tests from a + database backup rather than an empty database. This is particularly helpful when verifying compatibility during + version upgrades or testing stateful operations.

+
vt test --backup-path /path/to/backup -vschema t/vschema.json t/basic.test
+ +

Contributing

+

We welcome contributions in the following areas:

+
    +
  • Writing documentation on how to use the framework
  • +
  • Triaging issues
  • +
  • Submitting new test cases
  • +
  • Fixing bugs in the test framework
  • +
  • Adding features from the MySQL test framework that are missing in this implementation
  • +
+

After cloning the repo, make sure to run:

+
make install-hooks
+

to install the pre-commit hooks.

+ +

License

+

Vitess Tester is licensed under the Apache 2.0 license. See the LICENSE file for more + information.

+ +

Acknowledgments

+

Vitess Tester started as a fork from pingcap/mysql-tester. We + thank the original authors for their foundational work.

+ +{{end}} diff --git a/go/web/templates/css/styles.css b/go/web/templates/css/styles.css new file mode 100644 index 0000000..a86fd98 --- /dev/null +++ b/go/web/templates/css/styles.css @@ -0,0 +1,83 @@ + +/* General Styles */ +html, body { + height: 100%; /* Ensure the entire viewport is used */ + margin: 0; + padding: 0; + font-family: Arial, sans-serif; + background-color: #f9f9f9; + color: #333; + overflow: hidden; /* Prevent scrollbars for the whole page if not needed */ + width: 100%; + box-sizing: border-box; +} + +a { + text-decoration: none; + color: #007BFF; +} + +a:hover { + text-decoration: underline; +} + +/* Header Styles */ +header { + background-color: black; + color: white; + display: flex; + justify-content: space-between; + align-items: center; + height: 60px; /* Fixed height */ + width: 100%; + margin: 0; + padding: 10px; + box-sizing: border-box; +} + +header h1 { + margin: 0; + font-size: 24px; +} + +header nav a { + margin-left: 15px; + font-size: 16px; + font-weight: bold; + color: white; +} + +header nav a:hover { + color: #FFD700; +} + +/* Main Content Styles */ +main { + height: calc(100% - 150px); /* Adjusted for header and footer heights */ + overflow-y: auto; /* Enable vertical scrolling if content overflows */ + overflow-x: hidden; /* Prevent horizontal scrolling */ + padding: 20px; + background: white; + margin: 0 auto; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + width: 100%; +} + +/* Footer Styles */ +footer { + background-color: orange; + color: black; + text-align: center; + height: 40px; /* Fixed height */ + line-height: 40px; /* Vertically center content */ + width: 100%; + position: absolute; + bottom: 0; +} + +footer p { + margin: 0; + padding: 0; + font-size: 14px; +} \ No newline at end of file diff --git a/go/web/templates/footer.html b/go/web/templates/footer.html new file mode 100644 index 0000000..7b80456 --- /dev/null +++ b/go/web/templates/footer.html @@ -0,0 +1,5 @@ +{{define "footer"}} +
+

© 2024 Vitess

+
+{{end}} diff --git a/go/web/templates/header.html b/go/web/templates/header.html new file mode 100644 index 0000000..905dd2b --- /dev/null +++ b/go/web/templates/header.html @@ -0,0 +1,11 @@ +{{define "header"}} +
+ VT Logo +

vt utilities

+ +
+{{end}} + diff --git a/go/web/templates/images/vitess-logo.png b/go/web/templates/images/vitess-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d20b7faf50d7ee4d9284ff75a986e3c3e075037c GIT binary patch literal 20545 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 BUxxqy literal 0 HcmV?d00001 diff --git a/go/web/templates/index.html b/go/web/templates/index.html new file mode 100644 index 0000000..9297581 --- /dev/null +++ b/go/web/templates/index.html @@ -0,0 +1,16 @@ +{{define "content"}} + +

VT Utilities

+

VT Utilities is a powerful suite of tools designed for testing, summarizing, and analyzing queries in Vitess. It ensures compatibility with MySQL and provides insights into query behavior, schema information, transaction patterns, and more.

+

Key Features

+
    +
  • Testing: Compare query results between MySQL and Vitess to ensure compatibility.
  • +
  • Query Analysis: Analyze query logs for table, column, and join usage.
  • +
  • Tracing: Generate execution traces to debug and optimize queries.
  • +
  • Summarization: Simplify trace logs and key analyses for easy review.
  • +
  • Database Insights: Retrieve schema details and relevant configurations.
  • +
+

With VT Utilities, developers and database administrators can confidently analyze and optimize Vitess deployments in sharded or unsharded environments.

+ + +{{end}} diff --git a/go/web/templates/index2.html b/go/web/templates/index2.html new file mode 100644 index 0000000..566549b --- /dev/null +++ b/go/web/templates/index2.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/go/web/templates/layout.html b/go/web/templates/layout.html new file mode 100644 index 0000000..6cea825 --- /dev/null +++ b/go/web/templates/layout.html @@ -0,0 +1,14 @@ + + + + {{.Title}} + + + +{{template "header" .}} +
+ {{template "content" .}} +
+{{template "footer" .}} + + diff --git a/go/web/web.go b/go/web/web.go new file mode 100644 index 0000000..5c10fbe --- /dev/null +++ b/go/web/web.go @@ -0,0 +1,129 @@ +package web + +import ( + "bytes" + "fmt" + "html/template" + "io" + "log" + "net/http" + "os" + + "github.com/gin-gonic/gin" +) + +func renderFile(fileName string, c *gin.Context) { + tmpl := template.Must(template.ParseFiles( + "go/web/templates/layout.html", + "go/web/templates/footer.html", + "go/web/templates/header.html", + fmt.Sprintf("go/web/templates/%s", fileName), + )) + + var buf bytes.Buffer + if err := tmpl.ExecuteTemplate(&buf, "layout.html", nil); err != nil { + // Return an error response if template execution fails + c.String(http.StatusInternalServerError, err.Error()) + return + } + + // Set the Content-Type to text/html and write the rendered content + c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes()) +} + +func Run() { + gin.SetMode(gin.ReleaseMode) + gin.DefaultWriter = io.Discard // Disable logging + r := gin.Default() + + r.LoadHTMLGlob("go/web/templates/*.html") + r.Static("/css", "go/web/templates/css") + r.Static("/images", "go/web/templates/images") + + r.GET("/", func(c *gin.Context) { + renderFile("index.html", c) + }) + + r.GET("/about", func(c *gin.Context) { + renderFile("about.html", c) + }) + + r.GET("/render", func(c *gin.Context) { + action := c.Query("action") + param := c.Query("param") + + // Call the handler to process the action + tmpl := template.Must(template.ParseFiles("go/web/templates/layout.html", "go/web/templates/index.html", "go/web/templates/footer.html", "go/web/templates/header.html")) + + data, err := handleRenderAction(tmpl, action, param) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) + return + } + + // Return the rendered HTML + c.Data(http.StatusOK, "text/html; charset=utf-8", data) + }) + + r.POST("/render", func(c *gin.Context) { + var input struct { + Action string `json:"action"` + Param string `json:"param"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"}) + return + } + tmpl := template.Must(template.ParseFiles("go/web/templates/layout.html", "go/web/templates/index.html", "go/web/templates/footer.html", "go/web/templates/header.html")) + + data, err := handleRenderAction(tmpl, input.Action, input.Param) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) + return + } + + c.Data(http.StatusOK, "text/html; charset=utf-8", data) + }) + if os.WriteFile("/dev/stderr", []byte("Starting web server on http://localhost:8080\n"), 0o600) != nil { + panic("Failed to write to /dev/stderr") + } + if err := r.Run(":8080"); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} + +func handleRenderAction(tmpl *template.Template, action, param string) ([]byte, error) { + var data struct { + Title string + Body string + } + + switch action { + case "home": + data.Title = "Home Page" + data.Body = "Welcome to the homepage!" + case "about": + data.Title = "About Page" + data.Body = fmt.Sprintf("About us: %s", param) + case "dynamic": + data.Title = "Dynamic Content" + data.Body = generateDynamicContent(param) + default: + return nil, fmt.Errorf("invalid action: %s", action) + } + + var buf bytes.Buffer + if err := tmpl.ExecuteTemplate(&buf, "layout", data); err != nil { + return nil, fmt.Errorf("failed to render template: %v", err) + } + + return buf.Bytes(), nil +} + +func generateDynamicContent(param string) string { + if param == "" { + return "No parameter provided for dynamic content." + } + return fmt.Sprintf("Generated dynamic content with param: %s", param) +} From 3cc9e4570cfc8495ae67e8294526c0ead53e190c Mon Sep 17 00:00:00 2001 From: Rohit Nayak Date: Sun, 8 Dec 2024 15:30:25 +0100 Subject: [PATCH 03/15] Make summary attributes public so that they will be part of the marshalled json. Change map key from ColumnInformation to a string representation so it can be marshalled. Fix tests. Code for launching summary is WIP Signed-off-by: Rohit Nayak --- Makefile | 4 +- go/summarize/force-graph.go | 8 +- go/summarize/markdown.go | 4 +- go/summarize/reading.go | 8 +- go/summarize/summarize-keys.go | 68 ++++++++++++---- go/summarize/summarize-keys_test.go | 46 +++++------ go/summarize/summarize-transactions.go | 2 +- go/summarize/summarize.go | 43 +++++++++- go/summarize/summary.go | 32 ++++---- go/web/web.go | 104 +++++++------------------ 10 files changed, 175 insertions(+), 144 deletions(-) diff --git a/Makefile b/Makefile index f2bdf42..47828e0 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ build: @go build -o vt ./go/vt test: - go test -count=1 ./go/... + go test -v -count=1 ./go/... tidy: go mod tidy @@ -80,4 +80,4 @@ install-hooks: install: build @install -m 0755 vt $(GOBIN_DIR)/vt - @echo "vt installed successfully to $(GOBIN_DIR)." \ No newline at end of file + @echo "vt installed successfully to $(GOBIN_DIR)." diff --git a/go/summarize/force-graph.go b/go/summarize/force-graph.go index 2ddd9bf..21e0c2a 100644 --- a/go/summarize/force-graph.go +++ b/go/summarize/force-graph.go @@ -60,7 +60,7 @@ func createForceGraphData(s *Summary) forceGraphData { idxTableNode := make(map[string]int) result.maxNumRows = 0 - for _, table := range s.tables { + for _, table := range s.Tables { result.Nodes = append(result.Nodes, node{ID: table.Table, RowCount: table.RowCount}) idxTableNode[table.Table] = len(result.Nodes) - 1 if table.RowCount > result.maxNumRows { @@ -97,7 +97,7 @@ func createForceGraphData(s *Summary) forceGraphData { } func addForeignKeys(s *Summary, result *forceGraphData, idxTableNode map[string]int) { - for _, ts := range s.tables { + for _, ts := range s.Tables { for _, fk := range ts.ReferencedTables { if t := s.GetTable(ts.Table); t == nil { s.AddTable(&TableSummary{Table: ts.Table}) @@ -119,7 +119,7 @@ func addForeignKeys(s *Summary, result *forceGraphData, idxTableNode map[string] func addTransactions(s *Summary, result *forceGraphData, idxTableNode map[string]int) { txTablesMap := make(map[graphKey]int) - for _, transaction := range s.transactions { + for _, transaction := range s.Transactions { var tables []string for _, query := range transaction.Queries { tables = append(tables, query.Table) @@ -148,7 +148,7 @@ func addTransactions(s *Summary, result *forceGraphData, idxTableNode map[string } func addJoins(s *Summary, result *forceGraphData, idxTableNode map[string]int) { - for _, join := range s.joins { + for _, join := range s.Joins { var preds []string for _, predicate := range join.predicates { preds = append(preds, predicate.String()) diff --git a/go/summarize/markdown.go b/go/summarize/markdown.go index 56bcfd9..8914d5e 100644 --- a/go/summarize/markdown.go +++ b/go/summarize/markdown.go @@ -155,7 +155,7 @@ func renderColumnUsageTable(md *markdown.MarkDown, summary *TableSummary) { } func renderTablesJoined(md *markdown.MarkDown, summary *Summary) { - if len(summary.joins) == 0 { + if len(summary.Joins) == 0 { return } @@ -164,7 +164,7 @@ func renderTablesJoined(md *markdown.MarkDown, summary *Summary) { } md.Println("```") - for _, join := range summary.joins { + for _, join := range summary.Joins { md.Printf("%s ↔ %s (Occurrences: %d)\n", join.Tbl1, join.Tbl2, join.Occurrences) for i, pred := range join.predicates { var s string diff --git a/go/summarize/reading.go b/go/summarize/reading.go index c265da0..ff287d5 100644 --- a/go/summarize/reading.go +++ b/go/summarize/reading.go @@ -82,7 +82,7 @@ func readTransactionFile(fileName string) (summarizer, error) { return nil, fmt.Errorf("error parsing json: %w", err) } return func(s *Summary) error { - s.analyzedFiles = append(s.analyzedFiles, fileName) + s.AnalyzedFiles = append(s.AnalyzedFiles, fileName) return summarizeTransactions(s, to.Signatures) }, nil } @@ -94,7 +94,7 @@ func readKeysFile(fileName string) (summarizer, error) { } return func(s *Summary) error { - s.analyzedFiles = append(s.analyzedFiles, fileName) + s.AnalyzedFiles = append(s.AnalyzedFiles, fileName) return summarizeKeysQueries(s, &ko) }, nil } @@ -106,8 +106,8 @@ func readDBInfoFile(fileName string) (summarizer, error) { } return func(s *Summary) error { - s.analyzedFiles = append(s.analyzedFiles, fileName) - s.hasRowCount = true + s.AnalyzedFiles = append(s.AnalyzedFiles, fileName) + s.HasRowCount = true for _, ti := range schemaInfo.Tables { table := s.GetTable(ti.Name) if table == nil { diff --git a/go/summarize/summarize-keys.go b/go/summarize/summarize-keys.go index 80a5add..0cb0d9a 100644 --- a/go/summarize/summarize-keys.go +++ b/go/summarize/summarize-keys.go @@ -64,6 +64,25 @@ type ( queryGraph map[graphKey]map[operators.JoinPredicate]int ) +func (ci *ColumnInformation) String() string { + return fmt.Sprintf("%s/%s", ci.Name, ci.Pos) +} + +func ColumnInfoFromString(s string) (*ColumnInformation, error) { + ci := ColumnInformation{} + parts := strings.Split(s, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid column information: %s", s) + } + ci.Name = parts[0] + pos, err := PositionFromString(parts[1]) + if err != nil { + return nil, fmt.Errorf("invalid column position: %s", parts[1]) + } + ci.Pos = pos + return &ci, nil +} + const ( Join Position = iota JoinRange @@ -89,8 +108,21 @@ func (p Position) String() string { return "UNKNOWN" } -func (ci ColumnInformation) String() string { - return fmt.Sprintf("%s %s", ci.Name, ci.Pos) +func PositionFromString(s string) (Position, error) { + switch s { + case "JOIN": + return Join, nil + case "JOIN RANGE": + return JoinRange, nil + case "WHERE": + return Where, nil + case "WHERE RANGE": + return WhereRange, nil + case "GROUP": + return Grouping, nil + } + + return 0, fmt.Errorf("invalid position: %s", s) } func (ts TableSummary) GetColumns() iter.Seq2[ColumnInformation, ColumnUsage] { @@ -100,8 +132,12 @@ func (ts TableSummary) GetColumns() iter.Seq2[ColumnInformation, ColumnUsage] { } columns := make([]colDetails, 0, len(ts.ColumnUses)) maxColUse := make(map[string]float64) - for colInfo, usage := range ts.ColumnUses { - columns = append(columns, colDetails{ci: colInfo, cu: usage}) + for colInfoKey, usage := range ts.ColumnUses { + colInfo, err := ColumnInfoFromString(colInfoKey) + if err != nil { + panic(err) + } + columns = append(columns, colDetails{ci: *colInfo, cu: usage}) if maxColUse[colInfo.Name] < usage.Percentage { maxColUse[colInfo.Name] = usage.Percentage } @@ -195,7 +231,7 @@ func summarizeKeysQueries(summary *Summary, queries *keys.Output) error { // First pass: collect all graphData and count occurrences for _, query := range queries.Queries { gatherTableInfo(query, tableSummaries, tableUsageWriteCounts, tableUsageReadCounts) - checkQueryForHotness(&summary.hotQueries, query, summary.hotQueryFn) + checkQueryForHotness(&summary.HotQueries, query, summary.hotQueryFn) } // Second pass: calculate percentages @@ -237,7 +273,7 @@ func summarizeKeysQueries(summary *Summary, queries *keys.Output) error { Count: len(query.LineNumbers), }) } - summary.failures = failures + summary.Failures = failures for _, query := range queries.Queries { for _, pred := range query.JoinPredicates { @@ -255,21 +291,21 @@ func summarizeKeysQueries(summary *Summary, queries *keys.Output) error { sort.Slice(joinPredicates, func(i, j int) bool { return joinPredicates[i].String() < joinPredicates[j].String() }) - summary.joins = append(summary.joins, joinDetails{ + summary.Joins = append(summary.Joins, joinDetails{ Tbl1: tables.Tbl1, Tbl2: tables.Tbl2, Occurrences: occurrences, predicates: joinPredicates, }) } - sort.Slice(summary.joins, func(i, j int) bool { - if summary.joins[i].Occurrences != summary.joins[j].Occurrences { - return summary.joins[i].Occurrences > summary.joins[j].Occurrences + sort.Slice(summary.Joins, func(i, j int) bool { + if summary.Joins[i].Occurrences != summary.Joins[j].Occurrences { + return summary.Joins[i].Occurrences > summary.Joins[j].Occurrences } - if summary.joins[i].Tbl1 != summary.joins[j].Tbl1 { - return summary.joins[i].Tbl1 < summary.joins[j].Tbl1 + if summary.Joins[i].Tbl1 != summary.Joins[j].Tbl1 { + return summary.Joins[i].Tbl1 < summary.Joins[j].Tbl1 } - return summary.joins[i].Tbl2 < summary.joins[j].Tbl2 + return summary.Joins[i].Tbl2 < summary.Joins[j].Tbl2 }) return nil } @@ -300,7 +336,7 @@ func gatherTableInfo(query keys.QueryAnalysisResult, tableSummaries map[string]* if _, exists := tableSummaries[table]; !exists { tableSummaries[table] = &TableSummary{ Table: table, - ColumnUses: make(map[ColumnInformation]ColumnUsage), + ColumnUses: make(map[string]ColumnUsage), } } @@ -327,9 +363,9 @@ func summarizeColumnUsage(tableSummary *TableSummary, query keys.QueryAnalysisRe columns = slices.Compact(columns) for _, col := range columns { - usage := tableSummary.ColumnUses[col] + usage := tableSummary.ColumnUses[col.String()] usage.Count += query.UsageCount - tableSummary.ColumnUses[col] = usage + tableSummary.ColumnUses[col.String()] = usage } } diff --git a/go/summarize/summarize-keys_test.go b/go/summarize/summarize-keys_test.go index ba6c0df..b0a649f 100644 --- a/go/summarize/summarize-keys_test.go +++ b/go/summarize/summarize-keys_test.go @@ -29,32 +29,32 @@ import ( func TestTableSummary(t *testing.T) { expected := []string{ - "l_orderkey " + Join.String() + " 72%", - "l_orderkey " + Grouping.String() + " 17%", - "l_suppkey " + Join.String() + " 39%", - "l_suppkey " + JoinRange.String() + " 17%", - "l_commitdate " + WhereRange.String() + " 28%", - "l_receiptdate " + WhereRange.String() + " 28%", - "l_shipdate " + WhereRange.String() + " 22%", - "l_partkey " + Join.String() + " 17%", - "l_returnflag " + Where.String() + " 6%", - "l_shipmode " + WhereRange.String() + " 6%", - "l_shipmode " + Grouping.String() + " 6%", + "l_orderkey/" + Join.String() + " 72%", + "l_orderkey/" + Grouping.String() + " 17%", + "l_suppkey/" + Join.String() + " 39%", + "l_suppkey/" + JoinRange.String() + " 17%", + "l_commitdate/" + WhereRange.String() + " 28%", + "l_receiptdate/" + WhereRange.String() + " 28%", + "l_shipdate/" + WhereRange.String() + " 22%", + "l_partkey/" + Join.String() + " 17%", + "l_returnflag/" + Where.String() + " 6%", + "l_shipmode/" + WhereRange.String() + " 6%", + "l_shipmode/" + Grouping.String() + " 6%", } ts := TableSummary{ - ColumnUses: map[ColumnInformation]ColumnUsage{ - {Name: "l_shipmode", Pos: WhereRange}: {Percentage: 6}, - {Name: "l_receiptdate", Pos: WhereRange}: {Percentage: 28}, - {Name: "l_shipdate", Pos: WhereRange}: {Percentage: 22}, - {Name: "l_orderkey", Pos: Grouping}: {Percentage: 17}, - {Name: "l_orderkey", Pos: Join}: {Percentage: 72}, - {Name: "l_suppkey", Pos: Join}: {Percentage: 39}, - {Name: "l_shipmode", Pos: Grouping}: {Percentage: 6}, - {Name: "l_returnflag", Pos: Where}: {Percentage: 6}, - {Name: "l_partkey", Pos: Join}: {Percentage: 17}, - {Name: "l_suppkey", Pos: JoinRange}: {Percentage: 17}, - {Name: "l_commitdate", Pos: WhereRange}: {Percentage: 28}, + ColumnUses: map[string]ColumnUsage{ + (&ColumnInformation{Name: "l_shipmode", Pos: WhereRange}).String(): {Percentage: 6}, + (&ColumnInformation{Name: "l_receiptdate", Pos: WhereRange}).String(): {Percentage: 28}, + (&ColumnInformation{Name: "l_shipdate", Pos: WhereRange}).String(): {Percentage: 22}, + (&ColumnInformation{Name: "l_orderkey", Pos: Grouping}).String(): {Percentage: 17}, + (&ColumnInformation{Name: "l_orderkey", Pos: Join}).String(): {Percentage: 72}, + (&ColumnInformation{Name: "l_suppkey", Pos: Join}).String(): {Percentage: 39}, + (&ColumnInformation{Name: "l_shipmode", Pos: Grouping}).String(): {Percentage: 6}, + (&ColumnInformation{Name: "l_returnflag", Pos: Where}).String(): {Percentage: 6}, + (&ColumnInformation{Name: "l_partkey", Pos: Join}).String(): {Percentage: 17}, + (&ColumnInformation{Name: "l_suppkey", Pos: JoinRange}).String(): {Percentage: 17}, + (&ColumnInformation{Name: "l_commitdate", Pos: WhereRange}).String(): {Percentage: 28}, }, } diff --git a/go/summarize/summarize-transactions.go b/go/summarize/summarize-transactions.go index 5e5cd9d..16fc00c 100644 --- a/go/summarize/summarize-transactions.go +++ b/go/summarize/summarize-transactions.go @@ -40,7 +40,7 @@ func summarizeTransactions(s *Summary, txs []transactions.Signature) error { } } - s.transactions = append(s.transactions, TransactionSummary{ + s.Transactions = append(s.Transactions, TransactionSummary{ Count: tx.Count, Queries: patterns, Joins: joins, diff --git a/go/summarize/summarize.go b/go/summarize/summarize.go index 6994f4f..928074d 100644 --- a/go/summarize/summarize.go +++ b/go/summarize/summarize.go @@ -17,10 +17,12 @@ limitations under the License. package summarize import ( + "encoding/json" "errors" "fmt" "io" "os" + "os/exec" "strings" "time" @@ -115,10 +117,45 @@ func printSummary(hotMetric string, workers []summaryWorker) (*Summary, error) { return nil, err } } - err = s.PrintMarkdown(os.Stdout, time.Now()) - if err != nil { - return nil, err + useWebSummary := true + //nolint:nestif // This is a temporary solution to avoid breaking the code + if useWebSummary { + // html, err := web.RenderFile("summarize.html", s) + // fmt.Printf("Summary: %v\n", s) + fmt.Println("Sending summary to server") + summaryJSON, err := json.Marshal(s) + if err != nil { + fmt.Println("Error marshalling summary:", err) + return nil, err + } + fmt.Printf("Summary JSON: %s\n", summaryJSON) + tmpFile, err := os.CreateTemp("/tmp/", "vt-summary-*.json") + if err != nil { + fmt.Println("Error creating temp file:", err) + return nil, err + } + _, err = tmpFile.WriteString(string(summaryJSON)) + if err != nil { + fmt.Println("Error writing to temp file:", err) + return nil, err + } + tmpFile.Close() + + url := "http://localhost:8080/summarize?file=" + tmpFile.Name() + err = exec.Command("open", url).Start() + if err != nil { + fmt.Println("Error launching browser:", err) + return nil, err + } + fmt.Println("URL launched in default browser:", url) + } else { + // Print the response + err = s.PrintMarkdown(os.Stdout, time.Now()) + if err != nil { + return nil, err + } } + return s, nil } diff --git a/go/summarize/summary.go b/go/summarize/summary.go index 32f62ca..3082a2d 100644 --- a/go/summarize/summary.go +++ b/go/summarize/summary.go @@ -32,23 +32,27 @@ import ( type ( Summary struct { + Tables []*TableSummary + Failures []FailuresSummary + Transactions []TransactionSummary + HotQueries []keys.QueryAnalysisResult tables []*TableSummary failures []FailuresSummary transactions []TransactionSummary planAnalysis PlanAnalysis hotQueries []keys.QueryAnalysisResult hotQueryFn getMetric - analyzedFiles []string + AnalyzedFiles []string queryGraph queryGraph - joins []joinDetails - hasRowCount bool + Joins []joinDetails + HasRowCount bool } TableSummary struct { Table string ReadQueryCount int WriteQueryCount int - ColumnUses map[ColumnInformation]ColumnUsage + ColumnUses map[string]ColumnUsage JoinPredicates []operators.JoinPredicate Failed bool RowCount int @@ -103,22 +107,22 @@ func (s *Summary) PrintMarkdown(out io.Writer, now time.Time) error { **Analyzed File%s**: ` + "%s" + ` ` - if len(s.analyzedFiles) > 1 { + if len(s.AnalyzedFiles) > 1 { filePlural = "s" } - for i, file := range s.analyzedFiles { - s.analyzedFiles[i] = "`" + file + "`" + for i, file := range s.AnalyzedFiles { + s.AnalyzedFiles[i] = "`" + file + "`" } - md.Printf(msg, now.Format(time.DateTime), filePlural, strings.Join(s.analyzedFiles, ", ")) + md.Printf(msg, now.Format(time.DateTime), filePlural, strings.Join(s.AnalyzedFiles, ", ")) err := renderPlansSection(md, s.planAnalysis) if err != nil { return err } - renderHotQueries(md, s.hotQueries, s.hotQueryFn) - renderTableUsage(md, s.tables, s.hasRowCount) + renderHotQueries(md, s.HotQueries, s.hotQueryFn) + renderTableUsage(md, s.Tables, s.HasRowCount) renderTablesJoined(md, s) - renderTransactions(md, s.transactions) - renderFailures(md, s.failures) + renderTransactions(md, s.Transactions) + renderFailures(md, s.Failures) _, err = md.WriteTo(out) if err != nil { @@ -128,7 +132,7 @@ func (s *Summary) PrintMarkdown(out io.Writer, now time.Time) error { } func (s *Summary) GetTable(name string) *TableSummary { - for _, table := range s.tables { + for _, table := range s.Tables { if table.Table == name { return table } @@ -137,7 +141,7 @@ func (s *Summary) GetTable(name string) *TableSummary { } func (s *Summary) AddTable(table *TableSummary) { - s.tables = append(s.tables, table) + s.Tables = append(s.Tables, table) } func (ts TableSummary) IsEmpty() bool { diff --git a/go/web/web.go b/go/web/web.go index 5c10fbe..d0f8df4 100644 --- a/go/web/web.go +++ b/go/web/web.go @@ -2,6 +2,7 @@ package web import ( "bytes" + "encoding/json" "fmt" "html/template" "io" @@ -10,9 +11,21 @@ import ( "os" "github.com/gin-gonic/gin" + + "github.com/vitessio/vt/go/summarize" ) -func renderFile(fileName string, c *gin.Context) { +func RenderFileToGin(fileName string, data any, c *gin.Context) { + buf, err := RenderFile(fileName, data) + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes()) +} + +func RenderFile(fileName string, data any) (*bytes.Buffer, error) { + _ = data tmpl := template.Must(template.ParseFiles( "go/web/templates/layout.html", "go/web/templates/footer.html", @@ -21,14 +34,11 @@ func renderFile(fileName string, c *gin.Context) { )) var buf bytes.Buffer - if err := tmpl.ExecuteTemplate(&buf, "layout.html", nil); err != nil { - // Return an error response if template execution fails - c.String(http.StatusInternalServerError, err.Error()) - return + err := tmpl.ExecuteTemplate(&buf, "layout.html", nil) + if err != nil { + return nil, fmt.Errorf("failed to render template: %v", err) } - - // Set the Content-Type to text/html and write the rendered content - c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes()) + return &buf, nil } func Run() { @@ -41,50 +51,29 @@ func Run() { r.Static("/images", "go/web/templates/images") r.GET("/", func(c *gin.Context) { - renderFile("index.html", c) + RenderFileToGin("index.html", nil, c) }) r.GET("/about", func(c *gin.Context) { - renderFile("about.html", c) + RenderFileToGin("about.html", nil, c) }) - r.GET("/render", func(c *gin.Context) { - action := c.Query("action") - param := c.Query("param") - - // Call the handler to process the action - tmpl := template.Must(template.ParseFiles("go/web/templates/layout.html", "go/web/templates/index.html", "go/web/templates/footer.html", "go/web/templates/header.html")) - - data, err := handleRenderAction(tmpl, action, param) + r.GET("/summarize", func(c *gin.Context) { + filePath := c.Query("file") + data, err := os.ReadFile(filePath) if err != nil { - c.String(http.StatusBadRequest, err.Error()) + c.String(http.StatusInternalServerError, err.Error()) return } - - // Return the rendered HTML - c.Data(http.StatusOK, "text/html; charset=utf-8", data) - }) - - r.POST("/render", func(c *gin.Context) { - var input struct { - Action string `json:"action"` - Param string `json:"param"` - } - - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"}) - return - } - tmpl := template.Must(template.ParseFiles("go/web/templates/layout.html", "go/web/templates/index.html", "go/web/templates/footer.html", "go/web/templates/header.html")) - - data, err := handleRenderAction(tmpl, input.Action, input.Param) + var summary summarize.Summary + err = json.Unmarshal(data, &summary) if err != nil { - c.String(http.StatusBadRequest, err.Error()) + c.String(http.StatusInternalServerError, err.Error()) return } - - c.Data(http.StatusOK, "text/html; charset=utf-8", data) + RenderFileToGin("summarize.html", summary, c) }) + if os.WriteFile("/dev/stderr", []byte("Starting web server on http://localhost:8080\n"), 0o600) != nil { panic("Failed to write to /dev/stderr") } @@ -92,38 +81,3 @@ func Run() { log.Fatalf("Failed to start server: %v", err) } } - -func handleRenderAction(tmpl *template.Template, action, param string) ([]byte, error) { - var data struct { - Title string - Body string - } - - switch action { - case "home": - data.Title = "Home Page" - data.Body = "Welcome to the homepage!" - case "about": - data.Title = "About Page" - data.Body = fmt.Sprintf("About us: %s", param) - case "dynamic": - data.Title = "Dynamic Content" - data.Body = generateDynamicContent(param) - default: - return nil, fmt.Errorf("invalid action: %s", action) - } - - var buf bytes.Buffer - if err := tmpl.ExecuteTemplate(&buf, "layout", data); err != nil { - return nil, fmt.Errorf("failed to render template: %v", err) - } - - return buf.Bytes(), nil -} - -func generateDynamicContent(param string) string { - if param == "" { - return "No parameter provided for dynamic content." - } - return fmt.Sprintf("Generated dynamic content with param: %s", param) -} From 997168dc8fcb7d3b9f3921abef986ba368de8ad1 Mon Sep 17 00:00:00 2001 From: Rohit Nayak Date: Sun, 8 Dec 2024 19:42:47 +0100 Subject: [PATCH 04/15] Summary working on browser launch with just Table count info Signed-off-by: Rohit Nayak --- go/summarize/summarize.go | 4 +- go/web/templates/css/styles.css | 49 ++++++++++-- go/web/templates/header.html | 2 +- go/web/templates/layout.html | 3 +- go/web/templates/summarize.html | 27 +++++++ go/web/templates/summarize2.html | 129 +++++++++++++++++++++++++++++++ go/web/web.go | 18 ++++- 7 files changed, 218 insertions(+), 14 deletions(-) create mode 100644 go/web/templates/summarize.html create mode 100644 go/web/templates/summarize2.html diff --git a/go/summarize/summarize.go b/go/summarize/summarize.go index 928074d..41894b7 100644 --- a/go/summarize/summarize.go +++ b/go/summarize/summarize.go @@ -123,12 +123,12 @@ func printSummary(hotMetric string, workers []summaryWorker) (*Summary, error) { // html, err := web.RenderFile("summarize.html", s) // fmt.Printf("Summary: %v\n", s) fmt.Println("Sending summary to server") - summaryJSON, err := json.Marshal(s) + summaryJSON, err := json.Marshal(*s) if err != nil { fmt.Println("Error marshalling summary:", err) return nil, err } - fmt.Printf("Summary JSON: %s\n", summaryJSON) + // fmt.Printf("Summary JSON: %s\n", summaryJSON) tmpFile, err := os.CreateTemp("/tmp/", "vt-summary-*.json") if err != nil { fmt.Println("Error creating temp file:", err) diff --git a/go/web/templates/css/styles.css b/go/web/templates/css/styles.css index a86fd98..4220fca 100644 --- a/go/web/templates/css/styles.css +++ b/go/web/templates/css/styles.css @@ -1,13 +1,12 @@ -/* General Styles */ html, body { - height: 100%; /* Ensure the entire viewport is used */ + height: 100%; margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f9f9f9; color: #333; - overflow: hidden; /* Prevent scrollbars for the whole page if not needed */ + overflow:auto; width: 100%; box-sizing: border-box; } @@ -21,7 +20,6 @@ a:hover { text-decoration: underline; } -/* Header Styles */ header { background-color: black; color: white; @@ -51,7 +49,6 @@ header nav a:hover { color: #FFD700; } -/* Main Content Styles */ main { height: calc(100% - 150px); /* Adjusted for header and footer heights */ overflow-y: auto; /* Enable vertical scrolling if content overflows */ @@ -80,4 +77,44 @@ footer p { margin: 0; padding: 0; font-size: 14px; -} \ No newline at end of file +} + +h1, h2, h3, h4 { + color: #333; + font-family: Arial, sans-serif; + text-align: center; +} + +table { + width: 80%; + border-collapse: collapse; + margin-bottom: 20px; + margin-left: 10%; + margin-right: 10%; +} + +th, td { + border: 1px solid #ddd; + padding: 8px; +} + +th { + background-color: orange; + text-align: center; +} + +td { + text-align: right; +} + +.fixed-header { + top: 60px; + position: fixed; + width: 100%; + background-color: white; + z-index: 1000; + text-align: center; + margin-bottom: 200px; + border-bottom: 1px solid #ddd; +} + diff --git a/go/web/templates/header.html b/go/web/templates/header.html index 905dd2b..f7903e5 100644 --- a/go/web/templates/header.html +++ b/go/web/templates/header.html @@ -1,7 +1,7 @@ {{define "header"}}
VT Logo -

vt utilities

+

Vitess vt utilities